@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +116 -0
  3. package/README.md +292 -2
  4. package/markdown/build/build-markdown.cjs +66 -0
  5. package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
  6. package/markdown/dist/markdown.css +1 -0
  7. package/markdown/dist/markdown.mjs +325 -0
  8. package/markdown/source/markdown-factory.mjs +613 -0
  9. package/markdown/source/markdown.css +288 -0
  10. package/markdown/source/table-factory.mjs +1588 -0
  11. package/markdown/source/table.css +487 -0
  12. package/package.json +96 -4
  13. package/src/activity-groups.mjs +74 -0
  14. package/src/browser-response-groups.mjs +57 -0
  15. package/src/command-output.mjs +57 -0
  16. package/src/components.mjs +9 -0
  17. package/src/context-markers.mjs +19 -0
  18. package/src/context-status.mjs +34 -0
  19. package/src/diff.mjs +240 -0
  20. package/src/explore-tool-call.mjs +139 -0
  21. package/src/file-preview.mjs +50 -0
  22. package/src/host/native-a2a.mjs +90 -0
  23. package/src/host/native-child-file-pane.mjs +49 -0
  24. package/src/host/native-child-pane.mjs +94 -0
  25. package/src/host/native-command-pane.mjs +64 -0
  26. package/src/host/native-run-panel.mjs +202 -0
  27. package/src/host/session-task-events.mjs +247 -0
  28. package/src/host/state/native-a2a.mjs +122 -0
  29. package/src/host/state/native-child-observation.mjs +35 -0
  30. package/src/host/state/native-children.mjs +93 -0
  31. package/src/host/state/native-outcome.mjs +35 -0
  32. package/src/host/state/native-recovery.mjs +187 -0
  33. package/src/host/state/native-runtime-route.mjs +26 -0
  34. package/src/host/state/native-session-stop.mjs +40 -0
  35. package/src/host/state/native-status-panel.mjs +23 -0
  36. package/src/host/state/native-task-read-model.mjs +89 -0
  37. package/src/host/state/task-links.mjs +40 -0
  38. package/src/host/task-timeline.mjs +185 -0
  39. package/src/index.mjs +52 -0
  40. package/src/input-preview.mjs +44 -0
  41. package/src/inspector.mjs +40 -0
  42. package/src/project.mjs +132 -0
  43. package/src/protocol.mjs +362 -0
  44. package/src/react-binding.mjs +486 -0
  45. package/src/reducer.mjs +355 -0
  46. package/src/renderers.mjs +644 -0
  47. package/src/status-panel.mjs +1156 -0
  48. package/src/styles.mjs +278 -0
  49. package/src/summary-playback.mjs +39 -0
  50. package/src/summary.mjs +160 -0
  51. package/src/terminal.mjs +157 -0
  52. package/src/tool-detail.mjs +208 -0
  53. package/src/tool-error.mjs +33 -0
  54. package/src/tool-identity.mjs +40 -0
  55. package/src/view.mjs +364 -0
  56. package/src/work-segments.mjs +41 -0
@@ -0,0 +1,247 @@
1
+ import { initialState, reduce } from '@kingsoftcloud/agentkit-harness-ui/reducer';
2
+ import { previewToolInput } from '@kingsoftcloud/agentkit-harness-ui/input-preview';
3
+ import { nativeRunIsInactive } from './state/native-outcome.mjs';
4
+
5
+ const textOf = content => (Array.isArray(content) ? content : []).filter(part => part.type === 'text').map(part => part.text).join('\n');
6
+ const nativeInterruptionMessages = Object.freeze({
7
+ NATIVE_BACKGROUND_RESULT_UNKNOWN: '已结束等待,原命令结果仍未知;不会重跑。',
8
+ NATIVE_READ_INTERRUPTED: '读取结果未确认;本轮已停止,需要获取新的观察结果。',
9
+ NATIVE_RESOURCE_RESULT_UNKNOWN: '旧浏览器已隔离,原操作结果仍未知;后续必须获取新观察。',
10
+ });
11
+
12
+ const outcomeFact = (fact, callId, turn, sessionId, status) => fact?.contract === 'agentkit.native-tool-outcome/v1'
13
+ && fact.status === status && typeof fact.runId === 'string' && fact.runId
14
+ && typeof fact.sessionId === 'string' && fact.sessionId && (!sessionId || fact.sessionId === sessionId)
15
+ && typeof fact.callId === 'string' && fact.callId && fact.callId === callId
16
+ && Number.isSafeInteger(fact.turn) && fact.turn === turn;
17
+ const uncertainFact = (fact, callId, turn, sessionId) => outcomeFact(fact, callId, turn, sessionId, 'uncertain');
18
+
19
+ /** Enrich old Session projections from scoped journal facts without rewriting them. */
20
+ export function nativeHistoricalToolState(state, status, turn, sessionId) {
21
+ const view = status?.uncertainTools;
22
+ if (view?.contract !== 'agentkit.native-session-tool-outcomes/v1' || view.sessionId !== sessionId || !Array.isArray(view.facts)) return state;
23
+ let changed = false;
24
+ const items = state.items.map(item => {
25
+ if (item.kind !== 'tool') return item;
26
+ const fact = view.facts.find(fact => ['uncertain', 'not_dispatched'].includes(fact.status)
27
+ && outcomeFact(fact, item.callId, turn, sessionId, fact.status));
28
+ if (!fact) return item;
29
+ changed = true;
30
+ return { ...item, status: fact.status, progress: fact.status === 'uncertain' ? '执行结果未知,需要核对回执' : null };
31
+ });
32
+ return changed ? { ...state, items } : state;
33
+ }
34
+ const commandTools = new Set(['run_command', 'read_command_output']);
35
+
36
+ function commandDisplay(name, value) {
37
+ if (!commandTools.has(name) || !value || typeof value !== 'object'
38
+ || typeof value.stdout !== 'string' && typeof value.stderr !== 'string') return undefined;
39
+ return { kind: 'command_output', stdout: value.stdout ?? '', stderr: value.stderr ?? '',
40
+ ...(Number.isInteger(value.exit_code) ? { exit: value.exit_code } : {}),
41
+ ...(typeof value.cwd === 'string' ? { cwd: value.cwd } : {}) };
42
+ }
43
+
44
+ /** Transport adapter only. The package owns lifecycle and presentation. */
45
+ function admittedContextMarker(event, sessionId) {
46
+ const data = event.data, marker = data?.marker;
47
+ if (!sessionId || event.ignorable !== true || Object.hasOwn(event, 'surfaceOp')
48
+ || data?.contract !== 'agentkit.native-context-presentation/v1' || data.sessionId !== sessionId
49
+ || typeof data.runId !== 'string' || !data.runId.trim() || data.runId.length > 256
50
+ || !Number.isSafeInteger(data.turn) || data.turn < 1 || !/^ak-native-[a-f0-9]{64}$/.test(marker?.id ?? '')
51
+ || !['completed', 'failed', 'interrupted'].includes(marker.status) || marker.origin !== 'auto'
52
+ || !['start', 'tool', 'message'].includes(marker.after?.kind)
53
+ || marker.after.kind !== 'start' && !/^ak-native-[a-f0-9]{64}$/.test(marker.after.id ?? '')) return null;
54
+ return data;
55
+ }
56
+
57
+ export function createSessionTaskProjector({ sessionId } = {}) {
58
+ let revision = -1, previousWindow = null, tasks = new Map(), result = [], activeTurn = null, settledSteps = new Set(), previews = new Map(), finalAnswers = new Map(), dispositions = new Map();
59
+ const apply = entry => {
60
+ const event = entry.event, data = event?.data;
61
+ if (!event || !data) return;
62
+ if (event.type === 'agentkit-native/tool-outcome') {
63
+ const previous = tasks.get(data.turn);
64
+ if (event.ignorable !== true || !outcomeFact(data, data.callId, data.turn, sessionId, 'not_dispatched')
65
+ || !previous || previous.runId && previous.runId !== data.runId
66
+ || !previous.items.some(item => item.kind === 'tool' && item.callId === data.callId)) return;
67
+ dispositions.set(`${data.turn}:${data.callId}`, data);
68
+ const next = reduce(previous, { v: 1, type: 'tool.state', id: `disposition:${data.runId}:${data.callId}:${event.seq}`,
69
+ callId: data.callId, status: data.status });
70
+ tasks.set(data.turn, { ...next, runId: data.runId });return;
71
+ }
72
+ if (event.type === 'agentkit-native/context-marker') {
73
+ const observation = admittedContextMarker(event, sessionId), previous = observation && tasks.get(observation.turn);
74
+ if (!previous || previous.runId && previous.runId !== observation.runId) return;
75
+ const next = reduce(previous, { v: 1, type: 'context.marker',
76
+ id: `context:${observation.runId}:${observation.marker.id}:${observation.marker.status}`, marker: observation.marker });
77
+ tasks.set(observation.turn, { ...next, runId: observation.runId });
78
+ return;
79
+ }
80
+ if (Number.isSafeInteger(data.turn)) activeTurn = data.turn;
81
+ const turn = Number.isSafeInteger(data.turn) ? data.turn : activeTurn;
82
+ if (!Number.isSafeInteger(turn)) return;
83
+ let task = tasks.get(turn);
84
+ if (!task) task = reduce(initialState(), { v: 1, type: 'task.started', id: `turn:${turn}`, taskId: String(turn),
85
+ ...(event.type === 'turn/start' ? { startedAt: event.time } : {}) });
86
+ const prefix = `${turn}:${data.step ?? 0}`, source = `${entry.type}:${event.seq}`;
87
+ const responseId = typeof sessionId === 'string' && sessionId && Number.isSafeInteger(data.step) && data.step > 0
88
+ ? JSON.stringify([sessionId, turn, data.step]) : null;
89
+ // The host retains successful attempt frames until step/end so its own
90
+ // tool dispatcher can still resolve preview identities. A committed
91
+ // message already owns our transcript. Its compact content positions may
92
+ // differ from provider stream positions (tools can arrive before text).
93
+ if (event.type === 'assistant/live-chunk' && settledSteps.has(prefix)) return;
94
+ let index = 0;
95
+ const emit = (type, value) => { task = reduce(task, { v: 1, type, id: `${source}:${index++}`, ts: event.time, ...value }); };
96
+ if (event.type === 'turn/start' && task.taskStartedAt === null) emit('task.started', { taskId: String(turn), startedAt: event.time });
97
+ const block = (value, position, done = true, sourceMessageId) => {
98
+ const itemId = `${prefix}:block:${position}`;
99
+ if (value.type === 'text') emit(done ? 'text.done' : 'text.delta', { itemId, ...(responseId ? { responseId } : {}), ...(sourceMessageId ? { sourceMessageId } : {}), ...(done ? { text: value.text } : { delta: value.text }) });
100
+ if (value.type === 'reasoning') emit(done ? 'thinking.done' : 'thinking.delta', { itemId, ...(responseId ? { responseId } : {}), ...(sourceMessageId ? { sourceMessageId } : {}), ...(done ? { text: value.text } : { delta: value.text }) });
101
+ };
102
+ if (event.type === 'user/message' && ['user', 'user-rpc'].includes(data.source?.kind))
103
+ emit('text.done', { itemId: `user:${data.id ?? event.seq}`, text: textOf(data.content), role: 'user' });
104
+ if (event.type === 'assistant/message') {
105
+ const content = data.message?.content ?? [];
106
+ const textIndex = content.findLastIndex(part => part.type === 'text' && part.text?.trim());
107
+ finalAnswers.set(turn, data.interrupted !== true && textIndex >= 0 && !content.some(part => part.type === 'tool-call')
108
+ ? `${prefix}:block:${textIndex}` : null);
109
+ content.forEach((part, i) => {
110
+ block(part, i, true, data.message.id);
111
+ // A host may publish the live dispatch before the committed assistant
112
+ // message. Its exact call identity anchors the row after the model's
113
+ // preceding content; arrival order does not define transcript order.
114
+ if (part.type === 'tool-call' && typeof part.id === 'string') {
115
+ const existing = task.items.find(item => item.kind === 'tool' && item.callId === part.id);
116
+ if (existing) task = { ...task, items: [...task.items.filter(item => item !== existing), existing] };
117
+ }
118
+ });
119
+ }
120
+ if (event.type === 'assistant/live-chunk') {
121
+ const chunk = data.chunk;
122
+ if (chunk?.type === 'text-delta') block({ type: 'text', text: chunk.text }, chunk.index, false);
123
+ if (chunk?.type === 'reasoning-delta') block({ type: 'reasoning', text: chunk.text }, chunk.index, false);
124
+ if (chunk?.type === 'block-end') block(chunk.block, chunk.index);
125
+ if (chunk?.type === 'tool-call-delta') {
126
+ const previewId = `preview:${data.attemptId}:${chunk.index}`;
127
+ const previous = previews.get(previewId) ?? { text: '', receivedChars: 0, name: '' };
128
+ const delta = typeof chunk.argumentsDelta === 'string' ? chunk.argumentsDelta : '';
129
+ const next = { text: (previous.text + delta).slice(0, 128 * 1024), receivedChars: previous.receivedChars + delta.length,
130
+ name: chunk.name || previous.name };
131
+ previews.set(previewId, next);
132
+ emit('tool.preview', { previewId, name: next.name, input: previewToolInput(next.text), receivedChars: next.receivedChars,
133
+ ...(responseId ? { responseId } : {}) });
134
+ }
135
+ }
136
+ if (event.type === 'tool/call') {
137
+ let input = {}; try { input = typeof data.arguments === 'string' ? JSON.parse(data.arguments) : data.arguments; } catch { /* incomplete arguments */ }
138
+ emit('tool.scheduled', { callId: data.callId, name: data.name, input, ...(responseId ? { responseId } : {}) });
139
+ }
140
+ if (event.type === 'tool/result') {
141
+ const message = data.message;
142
+ const text = textOf(message?.content); let value;
143
+ try { value = JSON.parse(text); } catch { /* plain tool result */ }
144
+ const display = data.meta?.contract === 'agentkit.workspace-display/v1' ? data.meta.display
145
+ : commandDisplay(task.items.find(item => item.callId === message?.toolCallId)?.name, value);
146
+ if (dispositions.has(`${turn}:${message?.toolCallId}`)) emit('tool.state', { callId: message.toolCallId, status: 'not_dispatched' });
147
+ else if (uncertainFact(data.meta?.nativeOutcome, message?.toolCallId, turn, sessionId) || value?.status === 'uncertain') emit('tool.state', { callId: message?.toolCallId, status: 'uncertain', message: '执行结果未知,需要核对回执' });
148
+ else emit(message?.isError ? 'tool.error' : 'tool.result', { callId: message?.toolCallId,
149
+ ...(message?.isError ? { error: text } : { result: text,
150
+ ...(display ? { display } : {}) }) });
151
+ }
152
+ if (event.type === 'turn/end') {
153
+ const reason = data.reason;
154
+ const interruption = reason?.kind === 'error' && typeof reason.error?.code === 'string'
155
+ && Object.hasOwn(nativeInterruptionMessages, reason.error.code) ? nativeInterruptionMessages[reason.error.code] : null;
156
+ const outcome = interruption ? 'unverified' : reason?.kind === 'completed' ? 'unverified' : reason?.kind === 'blocked' ? 'blocked'
157
+ : reason?.kind === 'aborted' ? 'cancelled' : reason?.kind === 'error' ? 'failed' : 'unverified';
158
+ emit('task.finished', { outcome, ...(interruption ? { executionState: 'interrupted', summary: interruption }
159
+ : { error: reason?.error?.message }), reason: reason?.error?.code, endedAt: event.time,
160
+ ...(reason?.kind === 'completed' && finalAnswers.get(turn) ? { finalAnswerId: finalAnswers.get(turn) } : {}) });
161
+ }
162
+ tasks.set(turn, task);
163
+ };
164
+ return {
165
+ project(window) {
166
+ if (window.revision === revision) return result;
167
+ let suffix = null;
168
+ if (window.change?.kind === 'append' && previousWindow && window.revision > revision) {
169
+ if (window.revision === revision + 1) suffix = window.change.entries;
170
+ else {
171
+ // React may observe several source publications in one render. The
172
+ // last delta alone misses frames; replaying the whole history is
173
+ // unnecessary when its immutable prefix is still exactly present.
174
+ const previous = previousWindow.entries ?? [], next = window.entries ?? [];
175
+ if (next.length >= previous.length && previous.every((entry, index) => next[index] === entry))
176
+ suffix = next.slice(previous.length);
177
+ }
178
+ }
179
+ const append = suffix !== null && !suffix.some(entry => entry.event?.type === 'assistant/message');
180
+ if (!append) {
181
+ tasks = new Map(); activeTurn = null; previews = new Map(); finalAnswers = new Map(); dispositions = new Map();
182
+ settledSteps = new Set((window.entries ?? []).filter(entry => entry.event?.type === 'assistant/message')
183
+ .map(entry => `${entry.event.data.turn}:${entry.event.data.step ?? 0}`));
184
+ }
185
+ for (const entry of append ? suffix : window.entries ?? []) apply(entry);
186
+ revision = window.revision;previousWindow = window;
187
+ result = [...tasks].sort(([a], [b]) => a - b).map(([turn, state]) => ({ turn, state }));
188
+ return result;
189
+ },
190
+ };
191
+ }
192
+
193
+ /** Native's committed outcome overrides the transport's coarser ending. */
194
+ export function nativeStatusForTurn(status, turn) {
195
+ return Number.isSafeInteger(turn) && status?.presentationTurn === turn ? status : null;
196
+ }
197
+
198
+ export function nativeTaskState(state, status, running) {
199
+ if (!status?.phase) return state;
200
+ const active = !nativeRunIsInactive(status, running);
201
+ let taskState = status.phase === 'completed' ? status.outcome === 'verified' ? 'completed'
202
+ : status.outcome === 'blocked' ? 'blocked' : 'unverified'
203
+ : status.phase === 'failed' ? 'failed' : status.phase === 'canceled' ? 'cancelled'
204
+ : status.phase === 'interrupted' || !active && !['paused', 'awaiting_interaction', 'awaiting_reconciliation'].includes(status.phase)
205
+ ? 'interrupted' : status.phase === 'paused' ? 'paused'
206
+ : ['awaiting_interaction', 'awaiting_reconciliation'].includes(status.phase) ? 'waiting' : 'running';
207
+ const toolStates = status.toolStates?.contract === 'agentkit.native-tool-states/v1' && status.toolStates.runId === status.runId
208
+ ? new Map(status.toolStates.calls.map(call => [call.callId, call.status])) : new Map();
209
+ const observations = status.commandPreviews;
210
+ const previews = status.phase === 'executing_tools' && observations?.contract === 'agentkit.native-command-previews/v1'
211
+ && observations.runId === status.runId && observations.sessionId === status.sessionId
212
+ && Array.isArray(observations.calls) && observations.calls.length <= 16
213
+ ? new Map(observations.calls.filter(call => typeof call.callId === 'string' && Number.isSafeInteger(call.sequence)
214
+ && call.sequence > 0 && call.display?.kind === 'command_preview' && typeof call.display.stdout === 'string'
215
+ && typeof call.display.stderr === 'string' && !Object.hasOwn(call.display, 'exit')
216
+ && new TextEncoder().encode(call.display.stdout + call.display.stderr).length <= 65536)
217
+ .map(call => [call.callId, { display: { kind: 'command_preview', stdout: call.display.stdout, stderr: call.display.stderr },
218
+ truncated: call.truncated === true }])) : new Map();
219
+ let presented = { ...state, items: state.items.map(item => item.kind === 'tool' && item.name === 'run_command'
220
+ && !['completed', 'failed'].includes(item.status) && previews.has(item.callId)
221
+ ? { ...item, status: 'running', display: previews.get(item.callId).display,
222
+ progress: previews.get(item.callId).truncated ? '运行中的输出预览已截断' : null } : item.kind === 'tool' && toolStates.has(item.callId)
223
+ ? { ...item, status: toolStates.get(item.callId) } : item.kind === 'tool-preview' && taskState !== 'running'
224
+ ? { ...item, streaming: false } : item.kind === 'tool' && ['scheduled', 'running'].includes(item.status)
225
+ && ['interrupted', 'waiting', 'paused', 'cancelled', 'failed', 'unverified', 'completed'].includes(taskState)
226
+ ? { ...item, status: taskState === 'waiting' ? 'waiting' : 'interrupted' } : item),
227
+ runId: status.runId, taskState, taskOutcome: status.outcome,
228
+ verificationRequired: typeof status.verificationRequired === 'boolean' ? status.verificationRequired : state.verificationRequired,
229
+ taskReason: status.reason, taskError: status.error?.message ?? null };
230
+ if (status.contextStatus?.contract === 'agentkit.native-context-status/v1' && status.contextStatus.runId === status.runId)
231
+ for (const marker of status.contextStatus.markers ?? []) presented = reduce(presented, {
232
+ v: 1, type: 'context.marker', id: `context:${status.runId}:${marker.id}:${marker.status}`, marker,
233
+ });
234
+ return presented;
235
+ }
236
+
237
+ /** Match children only by their authenticated owning tool call, never by order or role. */
238
+ export function bindNativeSubagents(state, tasks) {
239
+ let next = state;
240
+ for (const task of tasks ?? []) {
241
+ if (!task.parentToolCallId || !state.items.some(item => item.kind === 'tool' && item.callId === task.parentToolCallId)) continue;
242
+ next = reduce(next, { v: 1, type: 'subagent.bound', id: `child:${task.childRunId}:${task.snapshotRevision}`,
243
+ callId: task.parentToolCallId, child: { runId: task.childRunId, sessionId: task.sessionId, parentRunId: task.parentRunId,
244
+ role: task.role, title: task.goalPreview, phase: task.phase, activity: task.activity } });
245
+ }
246
+ return next;
247
+ }
@@ -0,0 +1,122 @@
1
+ const check = (condition, message) => { if (!condition) throw Error(message); };
2
+ const ended = new Set(['completed', 'failed', 'cancelled', 'rejected']);
3
+ const paused = value => ['input_required', 'auth_required'].includes(value);
4
+ async function observation(work, signal) {
5
+ let stop;
6
+ try {
7
+ signal.throwIfAborted();
8
+ const stopped = new Promise((resolve, reject) => {
9
+ stop = () => reject(signal.reason); signal.addEventListener('abort', stop, { once: true });
10
+ if (signal.aborted) stop();
11
+ });
12
+ const pending = new Promise(resolve => { signal.throwIfAborted(); resolve(work()); });
13
+ const result = await Promise.race([pending, stopped]);
14
+ signal.throwIfAborted(); return result;
15
+ } finally { signal.removeEventListener('abort', stop); }
16
+ }
17
+ const outcomes = new Set(['observed', 'submitted', 'already_terminal', 'remote_cancelled', 'local_cancelled_before_send', 'unknown', 'remote_task_not_bound',
18
+ 'cancel_requested_but_remote_unknown', 'cancel_requested_but_remote_unsupported']);
19
+ const rejectedBeforeEffect = new Set(['A2A_CONTINUATION_DEADLINE_EXCEEDED', 'A2A_CONTINUATION_NOT_ALLOWED',
20
+ 'A2A_CONTROL_TASK_ALREADY_TERMINAL', 'A2A_CANCEL_ALREADY_REQUESTED',
21
+ 'A2A_CONTROL_PERMISSION_DENIED', 'A2A_CONTROL_INPUT_INVALID', 'STUDIO_NATIVE_A2A_PERMISSION_UNAVAILABLE',
22
+ 'A2A_HOST_DECISION_REQUIRED', 'A2A_HOST_DECISION_RECEIPT_INVALID', 'A2A_APPROVAL_RESPONSE_INVALID', 'A2A_APPROVAL_ALREADY_DECIDED',
23
+ 'A2A_APPROVAL_REJECTION_NOT_ALLOWED', 'A2A_APPROVAL_REQUEST_MISSING', 'A2A_INPUT_REQUEST_MISSING',
24
+ 'A2A_INPUT_ALREADY_DECIDED', 'A2A_STRUCTURED_INPUT_REQUIRED', 'A2A_STRUCTURED_INPUT_INVALID', 'A2A_INPUT_SCHEMA_INVALID',
25
+ 'A2A_QUESTION_ANSWER_CONFLICT', 'A2A_QUESTION_EFFECT_ALREADY_ADMITTED']);
26
+
27
+ /** An uncertain response retains the exact command. Refresh is read-only;
28
+ * sending a fresh prompt is never a recovery operation. */
29
+ export function createNativeA2aTaskState({ hostRemote, task, commandId = () => globalThis.crypto.randomUUID() }) {
30
+ const dsh = task.ownerKind === 'dsh';
31
+ const keys = dsh ? ['agentId', 'sessionId', 'agentRevision', 'buildId', 'operationKey', 'callId']
32
+ : ['agentId', 'sessionId', 'runId', ...(task.childRunId === undefined ? [] : ['childRunId']), 'operationKey', 'callId'];
33
+ const identity = Object.fromEntries(keys.map(key => [key, task[key]]));
34
+ const child = task.childRunId === undefined ? null : { agentId: task.childAgentId, sessionId: task.childSessionId };
35
+ const control = dsh ? hostRemote.controlDshA2aTask : hostRemote.controlNativeA2aTask;
36
+ const lifetime = new AbortController(), listeners = new Set(); let disposed = false, watch;
37
+ let state = { task: null, loading: false, watching: false, sending: false, pending: null, outcome: '', error: '' };
38
+ const set = update => { if (!disposed) { state = { ...state, ...update }; for (const listener of listeners) listener(state); } };
39
+ const validate = value => {
40
+ const locator = value?.operation_locator;
41
+ check(value?.contract === 'agentkit.studio-a2a-task-control/v1' && (dsh
42
+ ? keys.every(key => locator?.[key] === identity[key]) && !['scope', 'runId', 'executionFence'].some(key => Object.hasOwn(locator, key))
43
+ : !child
44
+ ? locator?.scope?.agentId === identity.agentId && ['sessionId', 'runId', 'operationKey', 'callId'].every(key => locator[key] === identity[key])
45
+ : locator?.scope?.agentId === child.agentId && locator?.sessionId === child.sessionId && locator?.runId === identity.childRunId
46
+ && ['operationKey', 'callId'].every(key => locator[key] === identity[key]))
47
+ && typeof value.bindingDigest === 'string' && /^[a-f0-9]{64}$/u.test(value.bindingDigest)
48
+ && (!state.task || state.task.bindingDigest === value.bindingDigest)
49
+ && typeof value.task_id === 'string' && value.task_id.length > 0
50
+ && (value.remote_task_id === null || typeof value.remote_task_id === 'string')
51
+ && (value.remote_context_id === null || typeof value.remote_context_id === 'string')
52
+ && ['submitted', 'working', 'input_required', 'auth_required', ...ended].includes(value.state)
53
+ && outcomes.has(value.outcome) && (value.pauseDigest === null || /^[a-f0-9]{64}$/u.test(value.pauseDigest))
54
+ && (value.subscriptionContract === undefined || value.subscriptionContract === 'agentkit.studio-a2a-task-subscription/v1')
55
+ && (value.subscription === undefined || value.subscription?.contract === 'agentkit.studio-a2a-task-subscription/v1'
56
+ && Number.isSafeInteger(value.subscription.events) && value.subscription.events >= 0 && ['eof', 'boundary'].includes(value.subscription.ended)),
57
+ '远端任务回执与原调用不一致。');
58
+ const delivery = value.eventDelivery;
59
+ if (delivery !== undefined) {
60
+ const permanent = ['A2A_EVENT_BATCH_TOO_LARGE', 'A2A_KERNEL_FACT_INVALID', 'A2A_EVENT_REF_CAPACITY',
61
+ 'A2A_EVENT_REF_CONFLICT', 'A2A_EVENT_REF_INVALID', 'A2A_TASK_NOT_FOUND'];
62
+ const fields = ['contract', 'state', 'pendingBatches', 'acknowledgedBatches', 'failedBatches', 'errorCode'];
63
+ check(delivery && delivery.contract === 'agentkit.a2a-event-delivery-status/v1'
64
+ && Object.keys(delivery).length === fields.length && Object.keys(delivery).every(key => fields.includes(key))
65
+ && ['pendingBatches', 'acknowledgedBatches', 'failedBatches'].every(key => Number.isSafeInteger(delivery[key]) && delivery[key] >= 0)
66
+ && delivery.pendingBatches + delivery.acknowledgedBatches + delivery.failedBatches <= 8192
67
+ && (delivery.errorCode === null || [...permanent, 'A2A_EVENT_DELIVERY_UNAVAILABLE'].includes(delivery.errorCode))
68
+ && delivery.state === (delivery.failedBatches ? 'failed' : delivery.errorCode ? 'retrying' : delivery.pendingBatches ? 'pending' : 'delivered')
69
+ && (delivery.state !== 'retrying' || delivery.pendingBatches > 0), '任务事件投递记录无效,请重新核对。');
70
+ }
71
+ return value;
72
+ };
73
+ const send = async () => {
74
+ if (disposed || state.sending || state.loading || !state.pending) return;
75
+ const pending = structuredClone(state.pending); set({ sending: true, error: '' });
76
+ try {
77
+ const result = validate(await control(pending, lifetime.signal));
78
+ check(result.commandId === pending.commandId, '远端任务命令回执不一致。');
79
+ const unknown = ['unknown', 'cancel_requested_but_remote_unknown'].includes(result.outcome);
80
+ set({ task: result, pending: unknown ? pending : null, outcome: result.outcome });
81
+ } catch (error) {
82
+ if (error?.code === 'A2A_CONTROL_PERMISSION_DENIED' && pending.kind === 'cancel')
83
+ set({ error: '本次取消未获得批准。原取消命令已保留,重新确认将再次请求批准。' });
84
+ else if (rejectedBeforeEffect.has(error?.code)) set({ pending: null, error: '本次操作未执行,请核对输入、权限或原任务的截止时间。' });
85
+ else set({ error: '操作回执尚未确认,请核对远端任务或确认原请求。' });
86
+ }
87
+ finally { set({ sending: false }); }
88
+ };
89
+ const refresh = async (kind = 'get') => {
90
+ if (disposed || state.loading || state.sending) return;
91
+ const active = kind === 'subscribe' ? new AbortController() : null;
92
+ watch = active; set({ loading: true, watching: Boolean(active), error: '' });
93
+ try {
94
+ const request = { ...identity, kind, commandId: commandId() };
95
+ const signal = active ? AbortSignal.any([lifetime.signal, active.signal]) : lifetime.signal;
96
+ const result = validate(await observation(() => control(request, signal), signal));
97
+ check(result.commandId === request.commandId, '远端任务查询回执不一致。');
98
+ const pending = state.pending;
99
+ set({ task: result, pending: pending && !ended.has(result.state)
100
+ && !(['continue', 'reject'].includes(pending.kind) && paused(result.state) && pending.pauseDigest !== result.pauseDigest) ? pending : null });
101
+ } catch (error) { if (!active?.signal.aborted) set({ error: error?.code === 'A2A_SUBSCRIPTION_UNSUPPORTED'
102
+ ? '远端任务未提供流式订阅,可继续核对原任务。' : '远端任务核对失败,原调用不会自动重跑。' }); }
103
+ finally { watch = null; set({ loading: false, watching: false }); }
104
+ };
105
+ const prepare = (kind, parts, json) => {
106
+ if (disposed || state.pending || state.loading || state.sending || !state.task || ended.has(state.task.state)) return;
107
+ if (['continue', 'reject'].includes(kind) && state.task.question?.contract === 'agentkit.a2a-question-ledger/v1'
108
+ && state.task.question.status !== 'pending') return;
109
+ if (kind === 'continue' && !paused(state.task.state)) return;
110
+ if (kind === 'reject' && (!paused(state.task.state) || state.task.approval?.contract !== 'agentkit.a2a-remote-approval-control/v1'
111
+ || state.task.approval.decision)) return;
112
+ set({ pending: { ...identity, kind, commandId: commandId(), ...(kind === 'continue'
113
+ ? { ...(json === undefined ? { parts: structuredClone(parts) } : { json }), pauseDigest: state.task.pauseDigest }
114
+ : kind === 'reject' ? { pauseDigest: state.task.pauseDigest } : {}) } });
115
+ return send();
116
+ };
117
+ return { getSnapshot: () => state, subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
118
+ refresh: () => refresh(), watch: () => refresh('subscribe'), stopWatching: () => watch?.abort(),
119
+ retry: send, continue: parts => prepare('continue', parts), continueJson: json => prepare('continue', undefined, json),
120
+ reject: () => prepare('reject'), cancel: () => prepare('cancel'),
121
+ dispose() { disposed = true; lifetime.abort(); listeners.clear(); } };
122
+ }
@@ -0,0 +1,35 @@
1
+ import { initialState, reduce } from '@kingsoftcloud/agentkit-harness-ui/reducer';
2
+
3
+ /** Product transport mapping. The standalone UI never calls a Native store. */
4
+ export function nativeChildObservationState(detail) {
5
+ const value = detail?.observation, task = detail?.task;
6
+ if (value?.contract !== 'agentkit.native-child-observation/v1' || value.runId !== task?.childRunId
7
+ || value.sessionId !== task?.sessionId || !Array.isArray(value.rows)) throw Error('子会话过程与原任务不一致');
8
+ let state = initialState(), seq = 0;
9
+ const emit = (type, body) => { state = reduce(state, { v: 1, id: `${value.snapshotRevision}:${seq++}`, type, ...body }); };
10
+ emit('task.started', { taskId: value.runId, runId: value.runId });
11
+ if (value.prompt?.text) emit('text.done', { itemId: 'prompt', role: 'user', text: value.prompt.text });
12
+ for (const row of value.rows) {
13
+ if (row.kind === 'text' || row.kind === 'thought') emit(row.kind === 'text' ? 'text.done' : 'thinking.done', { itemId: row.id, text: row.text });
14
+ if (row.kind === 'tool') {
15
+ let input = {}; try { input = JSON.parse(row.input?.text); } catch { /* truncated preview remains an incomplete input */ }
16
+ emit('tool.scheduled', { callId: row.id, name: row.name, input });
17
+ if (row.status === 'completed') emit('tool.result', { callId: row.id, result: row.result?.text ?? null, display: row.display });
18
+ else if (row.status === 'failed') emit('tool.error', { callId: row.id, error: row.result?.text });
19
+ else if (['waiting', 'uncertain', 'interrupted'].includes(row.status)) emit('tool.state', { callId: row.id, status: row.status });
20
+ }
21
+ }
22
+ if (value.live?.thinking) emit('thinking.delta', { itemId: `live-thought:${value.live.operationKey}`, delta: value.live.thinking });
23
+ if (value.live?.text) emit('text.delta', { itemId: `live-text:${value.live.operationKey}`, delta: value.live.text });
24
+ if (['completed', 'failed', 'canceled'].includes(value.phase)) emit('task.finished', {
25
+ outcome: value.phase === 'failed' ? 'failed' : value.phase === 'canceled' ? 'cancelled' : value.outcome === 'verified' ? 'completed'
26
+ : value.outcome === 'blocked' ? 'blocked' : 'unverified', verificationRequired: false,
27
+ ...(value.phase === 'completed' && state.items.at(-1)?.kind === 'text'
28
+ ? { finalAnswerId: state.items.at(-1).id } : {}),
29
+ });
30
+ else if (['awaiting_interaction', 'awaiting_reconciliation', 'created'].includes(value.phase)) emit('task.state', { state: 'waiting' });
31
+ else if (!value.driving) emit('task.state', { state: 'interrupted' });
32
+ return { state, workspaceView: value.workspaceView, omittedRows: value.omittedRows, omittedChars: (value.prompt?.omittedChars ?? 0)
33
+ + value.rows.reduce((sum, row) => sum + (row.omittedChars ?? 0) + (row.input?.omittedChars ?? 0) + (row.result?.omittedChars ?? 0), 0)
34
+ + (value.live?.omittedChars ?? 0) };
35
+ }
@@ -0,0 +1,93 @@
1
+ const contract = 'agentkit.studio-native-child-history/v1';
2
+ const check = (value, message) => { if (!value) throw new Error(message); };
3
+
4
+ /** Own only product reads and explicit controls; execution stays in the Host. */
5
+ export function createNativeChildrenState({ hostRemote, agentId, sessionId, commandId = () => globalThis.crypto.randomUUID() }) {
6
+ const identity = { agentId, sessionId }, lifetime = new AbortController(), listeners = new Set();
7
+ let state = { page: null, detail: null, loading: false, reading: false, pending: null, pendingAction: null, sending: false, error: '' };
8
+ let disposed = false, reading = 0;
9
+ const set = changes => { if (!disposed) { state = { ...state, ...changes }; for (const listener of listeners) listener(state); } };
10
+ const valid = value => check(value?.contract === contract && value.sessionId === sessionId, '子任务记录与当前会话不一致,请刷新。');
11
+ const validTask = task => task && ['childRunId', 'sessionId', 'parentRunId', 'role', 'phase', 'goalPreview']
12
+ .every(key => typeof task[key] === 'string') && typeof task.resultAvailable === 'boolean'
13
+ && task.usage && Number.isSafeInteger(task.usage.modelCalls) && Number.isSafeInteger(task.usage.toolCalls)
14
+ && (task.a2aTasks === undefined || Array.isArray(task.a2aTasks) && task.a2aTasks.length <= 10000
15
+ && task.a2aTasks.every(item => item?.runId === task.parentRunId && item.childRunId === task.childRunId
16
+ && item.childSessionId === task.sessionId && ['childAgentId', 'operationKey', 'callId']
17
+ .every(key => typeof item[key] === 'string' && item[key].trim() && item[key].length <= (key === 'operationKey' ? 4096 : 256))));
18
+ async function load(next = false) {
19
+ if (disposed || state.loading || next && (!state.page || state.page.complete)) return;
20
+ const previous = state.page;
21
+ set({ loading: true, error: '' });
22
+ try {
23
+ const page = await hostRemote.nativeChildHistory({ ...identity,
24
+ ...(next ? { offset: previous.nextOffset, inventoryDigest: previous.inventoryDigest } : {}) }, lifetime.signal);
25
+ if (disposed) return;
26
+ valid(page);
27
+ check(Array.isArray(page.tasks) && page.tasks.every(task => validTask(task)
28
+ && (!(task.canPromote || task.canCancel || task.canSendInput) || task.parentRunId === page.runId)
29
+ && (!task.canSendInput || Number.isSafeInteger(task.goalRevision) && task.goalRevision >= 0))
30
+ && new Set(page.tasks.map(task => task.childRunId)).size === page.tasks.length
31
+ && Number.isSafeInteger(page.offset) && page.offset >= 0 && page.nextOffset === page.offset + page.tasks.length
32
+ && Number.isSafeInteger(page.totalTasks) && page.totalTasks >= page.nextOffset
33
+ && typeof page.complete === 'boolean' && page.complete === (page.nextOffset === page.totalTasks), '子任务目录不完整,请刷新。');
34
+ if (next && (page.inventoryDigest !== previous.inventoryDigest || page.runId !== previous.runId || page.offset !== previous.nextOffset))
35
+ throw Object.assign(new Error('子任务目录已变化,请刷新。'), { code: 'child_history_inventory_changed' });
36
+ if (next) check(!page.tasks.some(task => previous.tasks.some(old => old.childRunId === task.childRunId)), '子任务目录重复。');
37
+ set({ page: next ? { ...page, tasks: [...previous.tasks, ...page.tasks] } : page });
38
+ } catch (error) { set({ error: error.code === 'child_history_inventory_changed' ? '子任务目录已变化,请刷新。' : '子任务读取失败,请刷新重试。' }); }
39
+ finally { set({ loading: false }); }
40
+ }
41
+ async function read(task, more = false) {
42
+ if (disposed) return;
43
+ const token = ++reading, previous = state.detail;
44
+ set({ reading: true, error: '' });
45
+ try {
46
+ const detail = await hostRemote.nativeChildResult({ ...identity, runId: task.parentRunId, childRunId: task.childRunId,
47
+ ...(more ? { offset: previous.nextOffset } : {}) }, lifetime.signal);
48
+ if (disposed || token !== reading) return;
49
+ valid(detail);
50
+ check(validTask(detail.task) && detail.task.childRunId === task.childRunId && detail.runId === task.parentRunId
51
+ && detail.task.parentRunId === task.parentRunId
52
+ && typeof detail.text === 'string' && (detail.task.resultAvailable || detail.text === '')
53
+ && detail.offset === (more ? previous.nextOffset : 0) && detail.nextOffset === detail.offset + [...detail.text].length
54
+ && Number.isSafeInteger(detail.totalChars) && detail.totalChars >= detail.nextOffset
55
+ && detail.complete === (detail.nextOffset === detail.totalChars), '子任务结果与请求不一致。');
56
+ if (more) check(previous.task.childRunId === task.childRunId && previous.resultDigest === detail.resultDigest
57
+ && detail.offset === previous.nextOffset, '子任务结果已变化,请重新读取。');
58
+ set({ detail: more ? { ...detail, text: previous.text + detail.text } : detail });
59
+ } catch { if (token === reading) set({ error: '子任务结果读取失败,请重新读取。' }); }
60
+ finally { if (token === reading) set({ reading: false }); }
61
+ }
62
+ async function sendControl() {
63
+ if (disposed || !state.pending || state.sending) return;
64
+ const pending = state.pending;
65
+ set({ sending: true, error: '' });
66
+ try {
67
+ const method = { promote: 'promoteNativeChild', send: 'sendNativeChildInput', cancel: 'cancelNativeChild' }[state.pendingAction];
68
+ const receipt = await hostRemote[method](pending, lifetime.signal);
69
+ check(receipt?.commandId === pending.commandId && ['accepted', 'duplicate'].includes(receipt.status), '子任务操作回执未确认。');
70
+ if (disposed) return;
71
+ set({ pending: null, pendingAction: null }); await load();
72
+ } catch { set({ error: '子任务操作的接收结果尚未确认。请重试原请求;不会重新启动子任务。' }); }
73
+ finally { set({ sending: false }); }
74
+ }
75
+ function control(action, task, extra = {}) {
76
+ const allowed = { promote: task.canPromote, send: task.canSendInput, cancel: task.canCancel }[action];
77
+ if (disposed || state.pending || !allowed || task.parentRunId !== state.page?.runId) return;
78
+ if (action === 'send' && (typeof extra.text !== 'string' || !extra.text.trim() || extra.text.length > 32768
79
+ || !Number.isSafeInteger(task.goalRevision) || task.goalRevision < 0)) return;
80
+ set({ pendingAction: action, pending: { ...identity, runId: task.parentRunId, childRunId: task.childRunId,
81
+ commandId: commandId(), ...extra } });
82
+ return sendControl();
83
+ }
84
+ return {
85
+ getSnapshot: () => state,
86
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
87
+ refresh: () => load(), next: () => load(true), read,
88
+ promote: task => control('promote', task), cancel: task => control('cancel', task),
89
+ sendInput: (task, text) => control('send', task, { text, expectedGoalRevision: task.goalRevision }),
90
+ retryControl: sendControl, retryPromotion: sendControl,
91
+ dispose() { if (disposed) return; disposed = true; lifetime.abort(); reading++; listeners.clear(); },
92
+ };
93
+ }
@@ -0,0 +1,35 @@
1
+ const phases = { created: '已准备', preparing_context: '整理上下文', requesting_model: '正在生成回复',
2
+ compacting: '压缩上下文', verifying: '验证结果', executing_tools: '执行工具', awaiting_interaction: '等待审批或输入', paused: '已暂停',
3
+ awaiting_reconciliation: '等待核对执行结果', reconciling_tool: '正在核对执行结果', interrupted: '执行中断,需要核对', finalizing: '收尾中' };
4
+
5
+ /** The committed phase survives a crash; only the owner control state says work is still being driven. */
6
+ export function nativeRunIsInactive(status, transportRunning = true) {
7
+ return status?.state ? status.state !== 'recovering' : !transportRunning;
8
+ }
9
+
10
+ /** Execution phase and task evidence are separate facts. No inferred success. */
11
+ export function nativeRunPresentation(status) {
12
+ if (!status?.runId || !status.phase) return null;
13
+ if (status.phase === 'interrupted' && status.reason === 'effect_outcome_unknown'
14
+ && status.backgroundInterruptionReview?.contract === 'agentkit.background-interruption-review/v1'
15
+ && status.backgroundInterruptionReview.confirmedStopped === true && status.state === 'closed')
16
+ return { label: '已结束等待,原结果未知', detail: '已核对原操作停止并关闭本轮;未知结果与原回执保持不变,没有重跑原命令。' };
17
+ if (status.phase === 'canceled') return { label: '已取消', detail: '本轮执行已停止;已经发生的文件或外部修改不会因此自动撤销。' };
18
+ if (status.outcome === 'blocked') return { label: '任务受阻', detail: status.error?.message || '本轮未能完成任务。请查看回复中的缺项,检查 Agent 的「绑定能力」及审批要求。' };
19
+ if (status.phase === 'failed') return { label: '执行失败', detail: status.error?.message || '本轮执行未成功,请查看错误并核对结果后重试。' };
20
+ if (status.phase === 'interrupted' && status.reason === 'effect_outcome_unknown')
21
+ return { label: '执行中断,结果待核对', detail: '原操作结果尚未确认。请在运行与子任务中核对处置方式;不会自动重跑原操作。' };
22
+ if (nativeRunIsInactive(status) && !['completed', 'paused', 'awaiting_interaction', 'awaiting_reconciliation'].includes(status.phase))
23
+ return status.canResume === true
24
+ ? { label: '执行中断,等待恢复', detail: '原运行已没有活动执行者;恢复原任务后才会继续。已提交的回执保持不变。' }
25
+ : { label: '任务已中断', detail: status.error?.message || '原运行已停止。请查看运行记录和当前可用操作。' };
26
+ if (status.phase !== 'completed') return { label: phases[status.phase] || '处理中', detail: '' };
27
+ if (status.outcome === 'failed' || status.outcome === 'budget_exhausted') return { label: '任务未完成', detail: '本轮已结束,任务未完成。请查看错误、预算和结果记录。' };
28
+ if (status.outcome === 'verified') return { label: '任务已验证', detail: '任务已通过本次配置的验证检查;验证范围以结果证据为准。' };
29
+ if (status.outcome === 'partial' && status.reason === 'required_verification_incomplete')
30
+ return { label: '部分已验证', detail: '部分检查已通过,其余任务要求尚未获得验证证据;请核对回复和实际结果。' };
31
+ if (status.outcome === 'partial') return { label: '部分完成', detail: '部分要求已有通过证据,整体结果仍需核对;请查看回复及结果。' };
32
+ if (status.verificationRequired === true || status.verificationRequired !== false && status.reason === 'required_verification_incomplete')
33
+ return { label: '本轮已结束 · 结果未验证', detail: '本任务配置了必需的验证检查,目前尚无全部通过的证据。' };
34
+ return { label: '本轮已结束', detail: '本轮执行已结束,已返回回复。' };
35
+ }