@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +116 -0
- package/README.md +292 -2
- package/markdown/build/build-markdown.cjs +66 -0
- package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
- package/markdown/dist/markdown.css +1 -0
- package/markdown/dist/markdown.mjs +325 -0
- package/markdown/source/markdown-factory.mjs +613 -0
- package/markdown/source/markdown.css +288 -0
- package/markdown/source/table-factory.mjs +1588 -0
- package/markdown/source/table.css +487 -0
- package/package.json +96 -4
- package/src/activity-groups.mjs +74 -0
- package/src/browser-response-groups.mjs +57 -0
- package/src/command-output.mjs +57 -0
- package/src/components.mjs +9 -0
- package/src/context-markers.mjs +19 -0
- package/src/context-status.mjs +34 -0
- package/src/diff.mjs +240 -0
- package/src/explore-tool-call.mjs +139 -0
- package/src/file-preview.mjs +50 -0
- package/src/host/native-a2a.mjs +90 -0
- package/src/host/native-child-file-pane.mjs +49 -0
- package/src/host/native-child-pane.mjs +94 -0
- package/src/host/native-command-pane.mjs +64 -0
- package/src/host/native-run-panel.mjs +202 -0
- package/src/host/session-task-events.mjs +247 -0
- package/src/host/state/native-a2a.mjs +122 -0
- package/src/host/state/native-child-observation.mjs +35 -0
- package/src/host/state/native-children.mjs +93 -0
- package/src/host/state/native-outcome.mjs +35 -0
- package/src/host/state/native-recovery.mjs +187 -0
- package/src/host/state/native-runtime-route.mjs +26 -0
- package/src/host/state/native-session-stop.mjs +40 -0
- package/src/host/state/native-status-panel.mjs +23 -0
- package/src/host/state/native-task-read-model.mjs +89 -0
- package/src/host/state/task-links.mjs +40 -0
- package/src/host/task-timeline.mjs +185 -0
- package/src/index.mjs +52 -0
- package/src/input-preview.mjs +44 -0
- package/src/inspector.mjs +40 -0
- package/src/project.mjs +132 -0
- package/src/protocol.mjs +362 -0
- package/src/react-binding.mjs +486 -0
- package/src/reducer.mjs +355 -0
- package/src/renderers.mjs +644 -0
- package/src/status-panel.mjs +1156 -0
- package/src/styles.mjs +278 -0
- package/src/summary-playback.mjs +39 -0
- package/src/summary.mjs +160 -0
- package/src/terminal.mjs +157 -0
- package/src/tool-detail.mjs +208 -0
- package/src/tool-error.mjs +33 -0
- package/src/tool-identity.mjs +40 -0
- package/src/view.mjs +364 -0
- package/src/work-segments.mjs +41 -0
package/src/reducer.mjs
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// @kingsoftcloud/agentkit-harness-ui — state reducer
|
|
2
|
+
//
|
|
3
|
+
// Pure reducer over the versioned event protocol. It owns:
|
|
4
|
+
// - streaming assistant text (append-on-delta, finalize-on-done)
|
|
5
|
+
// - provider-supplied thinking (placeholder created only on the first
|
|
6
|
+
// non-empty delta, never on `thinking.start` alone — no invented reasoning)
|
|
7
|
+
// - single keyed tool calls, updated in place through
|
|
8
|
+
// scheduled/started/progress/result/error for the same callId
|
|
9
|
+
// - task-state interruption/failure/recovery via a validated transition table
|
|
10
|
+
// - ordered items preserving the provider stream order, with deduplication by
|
|
11
|
+
// event id for safe replay and a fallback tool part for out-of-order results
|
|
12
|
+
//
|
|
13
|
+
// No invented thinking or success: the reducer never synthesizes reasoning
|
|
14
|
+
// text, never marks a task completed without a host `task.finished` event, and
|
|
15
|
+
// never attributes aggregate usage to an individual segment.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
EventType, TaskState, TaskOutcome, ToolStatus, canTransition, normalizeEvent,
|
|
19
|
+
} from "./protocol.mjs";
|
|
20
|
+
|
|
21
|
+
/** @returns {object} a fresh empty harness state. */
|
|
22
|
+
export function initialState() {
|
|
23
|
+
return {
|
|
24
|
+
protocolVersion: 1,
|
|
25
|
+
taskId: null,
|
|
26
|
+
taskTitle: null,
|
|
27
|
+
runId: null,
|
|
28
|
+
taskState: TaskState.Idle,
|
|
29
|
+
taskReason: null,
|
|
30
|
+
taskMessage: null,
|
|
31
|
+
taskOutcome: null,
|
|
32
|
+
taskSummary: null, // host-supplied only; never synthesized
|
|
33
|
+
taskError: null,
|
|
34
|
+
taskStartedAt: null, taskEndedAt: null, finalAnswerId: null,
|
|
35
|
+
verificationRequired: null,
|
|
36
|
+
contextMarkers: [],
|
|
37
|
+
subagents: new Map(),
|
|
38
|
+
items: [], // ordered TimelineItem[]
|
|
39
|
+
seen: new Set(), // event ids already applied (dedup/replay)
|
|
40
|
+
pendingThinking: new Set(), // itemIds that had thinking.start but no delta yet
|
|
41
|
+
plan: null, // PlanItem[] | null
|
|
42
|
+
lastSeq: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Apply one (raw or normalized) event. Replay-safe: an event id that was already
|
|
48
|
+
* applied is a no-op. Returns the previous state for malformed envelopes the
|
|
49
|
+
* reducer cannot normalize, so a bad event never corrupts the timeline.
|
|
50
|
+
* @param {object} state
|
|
51
|
+
* @param {object} event
|
|
52
|
+
* @returns {object}
|
|
53
|
+
*/
|
|
54
|
+
export function reduce(state, event) {
|
|
55
|
+
if (!state) state = initialState();
|
|
56
|
+
let normalized;
|
|
57
|
+
try {
|
|
58
|
+
normalized = normalizeEvent(event);
|
|
59
|
+
} catch {
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
if (state.seen.has(normalized.id)) return state; // idempotent replay
|
|
63
|
+
const next = shallowClone(state);
|
|
64
|
+
next.seen = new Set(state.seen);
|
|
65
|
+
next.seen.add(normalized.id);
|
|
66
|
+
next.items = state.items.slice();
|
|
67
|
+
next.pendingThinking = new Set(state.pendingThinking);
|
|
68
|
+
if (normalized.seq != null && (next.lastSeq == null || normalized.seq > next.lastSeq)) {
|
|
69
|
+
next.lastSeq = normalized.seq;
|
|
70
|
+
}
|
|
71
|
+
apply(next, normalized);
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Apply a batch in arrival order. @param {object[]} events */
|
|
76
|
+
export function reduceAll(state, events) {
|
|
77
|
+
let current = state || initialState();
|
|
78
|
+
for (const event of Array.isArray(events) ? events : []) current = reduce(current, event);
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function apply(state, e) {
|
|
83
|
+
switch (e.type) {
|
|
84
|
+
case EventType.TaskStarted: return applyTaskStarted(state, e);
|
|
85
|
+
case EventType.TaskState: return applyTaskState(state, e);
|
|
86
|
+
case EventType.TaskFinished: return applyTaskFinished(state, e);
|
|
87
|
+
case EventType.TextDelta: return applyTextDelta(state, e);
|
|
88
|
+
case EventType.TextDone: return applyTextDone(state, e);
|
|
89
|
+
case EventType.ThinkingStart: return applyThinkingStart(state, e);
|
|
90
|
+
case EventType.ThinkingDelta: return applyThinkingDelta(state, e);
|
|
91
|
+
case EventType.ThinkingDone: return applyThinkingDone(state, e);
|
|
92
|
+
case EventType.SubagentBound: {
|
|
93
|
+
if (!e.callId) return;
|
|
94
|
+
state.subagents = new Map(state.subagents ?? []); state.subagents.set(e.callId, e.child);
|
|
95
|
+
const index = findToolIndex(state, e.callId);
|
|
96
|
+
if (index >= 0) itemAt(state, index).child = e.child;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
case EventType.ToolPreview: {
|
|
100
|
+
if (!e.previewId) return;
|
|
101
|
+
settlePriorReasoning(state, e);
|
|
102
|
+
const previous = state.items.findIndex(item => item.kind === 'tool-preview' && item.id === e.previewId);
|
|
103
|
+
const preview = { kind: 'tool-preview', id: e.previewId, name: e.name, input: e.input,
|
|
104
|
+
receivedChars: e.receivedChars, streaming: true, ...(e.responseId ? { responseId: e.responseId } : {}) };
|
|
105
|
+
if (previous >= 0) state.items[previous] = preview; else state.items.push(preview);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
case EventType.ToolScheduled: return applyTool(state, e, ToolStatus.Scheduled);
|
|
109
|
+
case EventType.ToolStarted: return applyTool(state, e, ToolStatus.Running);
|
|
110
|
+
case EventType.ToolState: return applyTool(state, e, e.status, e);
|
|
111
|
+
case EventType.ToolProgress: return applyTool(state, e, ToolStatus.Running, e);
|
|
112
|
+
case EventType.ToolResult: return applyTool(state, e, ToolStatus.Completed, e);
|
|
113
|
+
case EventType.ToolError: return applyTool(state, e, ToolStatus.Failed, e);
|
|
114
|
+
case EventType.ContextMarker: {
|
|
115
|
+
const markers = [...state.contextMarkers ?? []];
|
|
116
|
+
const index = markers.findIndex(marker => marker.id === e.marker.id);
|
|
117
|
+
if (index < 0) markers.push(e.marker); else markers[index] = e.marker;
|
|
118
|
+
state.contextMarkers = markers; return;
|
|
119
|
+
}
|
|
120
|
+
case EventType.PlanUpdated: return applyPlan(state, e);
|
|
121
|
+
default: return state;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function applyTaskStarted(state, e) {
|
|
126
|
+
state.taskId = e.taskId || state.taskId;
|
|
127
|
+
state.taskTitle = e.title || state.taskTitle;
|
|
128
|
+
state.runId = e.runId || state.runId;
|
|
129
|
+
// started always (re)enters running from idle or a non-terminal state.
|
|
130
|
+
if (canTransition(state.taskState, TaskState.Running) || state.taskState === TaskState.Idle) {
|
|
131
|
+
state.taskState = TaskState.Running;
|
|
132
|
+
} else {
|
|
133
|
+
return; // A delayed start cannot reopen a terminal task.
|
|
134
|
+
}
|
|
135
|
+
state.taskOutcome = null;
|
|
136
|
+
state.taskSummary = null;
|
|
137
|
+
state.taskError = null;
|
|
138
|
+
state.taskReason = null;
|
|
139
|
+
state.taskMessage = null;
|
|
140
|
+
if (e.startedAt !== undefined) state.taskStartedAt = e.startedAt;
|
|
141
|
+
state.taskEndedAt = null; state.finalAnswerId = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function applyTaskState(state, e) {
|
|
145
|
+
if (!canTransition(state.taskState, e.state)) return; // ignore invalid transitions
|
|
146
|
+
state.taskState = e.state;
|
|
147
|
+
state.taskReason = e.reason;
|
|
148
|
+
state.taskMessage = e.message;
|
|
149
|
+
if (e.state === TaskState.Running || e.state === TaskState.Recovered) {
|
|
150
|
+
state.taskOutcome = null;
|
|
151
|
+
state.taskError = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function applyTaskFinished(state, e) {
|
|
156
|
+
state.verificationRequired = e.verificationRequired;
|
|
157
|
+
state.taskOutcome = e.outcome;
|
|
158
|
+
state.taskSummary = e.summary; // host-supplied; may be null — never invented
|
|
159
|
+
state.taskError = e.error || null;
|
|
160
|
+
state.taskReason = e.reason || null;
|
|
161
|
+
state.taskEndedAt = e.endedAt ?? null;
|
|
162
|
+
state.finalAnswerId = ['completed', 'unverified'].includes(e.outcome) && !e.error && e.executionState !== 'interrupted'
|
|
163
|
+
? e.finalAnswerId ?? null : null;
|
|
164
|
+
if (e.outcome === TaskOutcome.Completed) state.taskState = TaskState.Completed;
|
|
165
|
+
else if (e.outcome === TaskOutcome.Cancelled) state.taskState = TaskState.Cancelled;
|
|
166
|
+
else if (e.outcome === TaskOutcome.Failed) state.taskState = TaskState.Failed;
|
|
167
|
+
else if (e.outcome === TaskOutcome.Blocked) state.taskState = TaskState.Blocked;
|
|
168
|
+
else if (e.outcome === TaskOutcome.Unverified) state.taskState = TaskState.Unverified;
|
|
169
|
+
if (e.executionState === TaskState.Interrupted) state.taskState = TaskState.Interrupted;
|
|
170
|
+
// finalize any still-streaming items: a finished task has no live streams.
|
|
171
|
+
// Clone each affected item so prior state snapshots are not mutated.
|
|
172
|
+
for (let i = 0; i < state.items.length; i += 1) {
|
|
173
|
+
const item = state.items[i];
|
|
174
|
+
const liveText = item.streaming;
|
|
175
|
+
const liveTool = item.kind === "tool" && (item.status === ToolStatus.Scheduled || item.status === ToolStatus.Running);
|
|
176
|
+
if (liveText || liveTool) state.items[i] = { ...item, streaming: false, ...(liveTool ? { status: ToolStatus.Interrupted } : {}) };
|
|
177
|
+
}
|
|
178
|
+
state.pendingThinking = new Set();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyTextDelta(state, e) {
|
|
182
|
+
if (!e.itemId) return;
|
|
183
|
+
if (e.delta) settlePriorReasoning(state, e);
|
|
184
|
+
const index = findItemIndex(state, e.itemId);
|
|
185
|
+
if (index >= 0) {
|
|
186
|
+
const item = itemAt(state, index);
|
|
187
|
+
item.text = (item.text || "") + e.delta;
|
|
188
|
+
item.streaming = true;
|
|
189
|
+
if (e.responseId) item.responseId = e.responseId;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
state.items.push({ kind: "text", id: e.itemId, text: e.delta, streaming: true, ...(e.responseId ? { responseId: e.responseId } : {}) });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function applyTextDone(state, e) {
|
|
196
|
+
if (e.text) settlePriorReasoning(state, e);
|
|
197
|
+
const index = findItemIndex(state, e.itemId);
|
|
198
|
+
if (index >= 0 && state.items[index].kind === "text") {
|
|
199
|
+
const item = itemAt(state, index);
|
|
200
|
+
if (e.text !== undefined) item.text = e.text;
|
|
201
|
+
item.streaming = false;
|
|
202
|
+
if (e.sourceMessageId) item.sourceMessageId = e.sourceMessageId;
|
|
203
|
+
if (e.responseId) item.responseId = e.responseId;
|
|
204
|
+
} else if (e.itemId && e.text) state.items.push({ kind: "text", id: e.itemId, text: e.text, role: e.role ?? "assistant", streaming: false,
|
|
205
|
+
...(e.responseId ? { responseId: e.responseId } : {}), ...(e.sourceMessageId ? { sourceMessageId: e.sourceMessageId } : {}) });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function applyThinkingStart(state, e) {
|
|
209
|
+
// reasoning_start alone must not create visible transcript noise: record the
|
|
210
|
+
// pending id without emitting an item.
|
|
211
|
+
if (e.itemId) state.pendingThinking.add(e.itemId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function applyThinkingDelta(state, e) {
|
|
215
|
+
if (!e.itemId) return;
|
|
216
|
+
if (!e.delta) {
|
|
217
|
+
// An empty delta with a prior start keeps the pending marker; no item yet.
|
|
218
|
+
if (state.pendingThinking.has(e.itemId)) return;
|
|
219
|
+
}
|
|
220
|
+
const index = findItemIndex(state, e.itemId);
|
|
221
|
+
if (index >= 0) {
|
|
222
|
+
const item = itemAt(state, index);
|
|
223
|
+
item.text = (item.text || "") + e.delta;
|
|
224
|
+
item.streaming = true;
|
|
225
|
+
if (e.responseId) item.responseId = e.responseId;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// First non-empty delta creates the placeholder. A provider that emits a delta
|
|
229
|
+
// without a matching start still gets a placeholder (spec fallback).
|
|
230
|
+
if (!e.delta) return;
|
|
231
|
+
state.pendingThinking.delete(e.itemId);
|
|
232
|
+
state.items.push({ kind: "thought", id: e.itemId, text: e.delta, streaming: true, provider: true,
|
|
233
|
+
...(e.responseId ? { responseId: e.responseId } : {}) });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function applyThinkingDone(state, e) {
|
|
237
|
+
state.pendingThinking.delete(e.itemId);
|
|
238
|
+
const index = findItemIndex(state, e.itemId);
|
|
239
|
+
if (index >= 0 && state.items[index].kind === "thought") {
|
|
240
|
+
const item = itemAt(state, index);
|
|
241
|
+
if (e.text !== undefined) item.text = e.text;
|
|
242
|
+
item.streaming = false;
|
|
243
|
+
if (e.sourceMessageId) item.sourceMessageId = e.sourceMessageId;
|
|
244
|
+
if (e.responseId) item.responseId = e.responseId;
|
|
245
|
+
// Host-supplied real duration (seconds); never invented by the reducer.
|
|
246
|
+
if (e.duration != null) item.duration = e.duration;
|
|
247
|
+
} else if (e.itemId && e.text) state.items.push({ kind: "thought", id: e.itemId, text: e.text, streaming: false, provider: true,
|
|
248
|
+
...(e.responseId ? { responseId: e.responseId } : {}), ...(e.sourceMessageId ? { sourceMessageId: e.sourceMessageId } : {}), ...(e.duration != null ? { duration: e.duration } : {}) });
|
|
249
|
+
// If only a start arrived, no item was ever created — nothing to finalize.
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function applyTool(state, e, status, payload) {
|
|
253
|
+
if (!e.callId) return;
|
|
254
|
+
if (status === ToolStatus.Scheduled) settlePriorReasoning(state, e);
|
|
255
|
+
const index = findToolIndex(state, e.callId);
|
|
256
|
+
if (index >= 0) {
|
|
257
|
+
const item = itemAt(state, index);
|
|
258
|
+
// Late schedule/progress frames can supply missing metadata but cannot
|
|
259
|
+
// reopen a settled call. A conflicting terminal is retained as received.
|
|
260
|
+
const settled = [ToolStatus.Completed, ToolStatus.Failed, ToolStatus.NotDispatched].includes(item.status);
|
|
261
|
+
if (settled && ![ToolStatus.Completed, ToolStatus.Failed, ToolStatus.NotDispatched].includes(status)) {
|
|
262
|
+
if (e.type === EventType.ToolScheduled) { item.name = e.name || item.name; item.input = e.input;
|
|
263
|
+
if (e.responseId) item.responseId = e.responseId; }
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Update the existing part in place: stable position, same callId.
|
|
267
|
+
if (e.type === EventType.ToolScheduled) {
|
|
268
|
+
item.name = e.name || item.name;
|
|
269
|
+
item.input = e.input;
|
|
270
|
+
if (e.responseId) item.responseId = e.responseId;
|
|
271
|
+
}
|
|
272
|
+
if (state.subagents?.has(e.callId)) item.child = state.subagents.get(e.callId);
|
|
273
|
+
if (payload && e.type === EventType.ToolProgress) {
|
|
274
|
+
item.progress = payload.progress || payload.message || null;
|
|
275
|
+
// A running execute may stream its partial output via tool.progress with
|
|
276
|
+
// a command_preview display; keep it on the item so the renderer shows
|
|
277
|
+
// live output without a result. The reducer never invents output: an
|
|
278
|
+
// empty display leaves the previous value untouched.
|
|
279
|
+
if (payload.display && payload.display.kind) item.display = payload.display;
|
|
280
|
+
}
|
|
281
|
+
if (payload && e.type === EventType.ToolResult) {
|
|
282
|
+
item.result = payload.result;
|
|
283
|
+
item.display = payload.display;
|
|
284
|
+
}
|
|
285
|
+
if (payload && e.type === EventType.ToolError) {
|
|
286
|
+
item.error = payload.error;
|
|
287
|
+
}
|
|
288
|
+
if (e.type === EventType.ToolState) item.progress = e.message;
|
|
289
|
+
item.status = status;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// Out-of-order terminal event: append a minimal fallback tool part. Missing
|
|
293
|
+
// input details are acceptable; missing transcript rendering is not.
|
|
294
|
+
const fallback = { kind: "tool", callId: e.callId,
|
|
295
|
+
...(e.responseId ? { responseId: e.responseId } : {}),
|
|
296
|
+
...(state.subagents?.has(e.callId) ? { child: state.subagents.get(e.callId) } : {}), name: e.name || "tool", input: e.input || {}, status };
|
|
297
|
+
if (payload && e.type === EventType.ToolResult) { fallback.result = payload.result; fallback.display = payload.display; }
|
|
298
|
+
if (payload && e.type === EventType.ToolError) { fallback.error = payload.error; }
|
|
299
|
+
if (payload && e.type === EventType.ToolProgress) {
|
|
300
|
+
fallback.progress = payload.progress || payload.message || null;
|
|
301
|
+
if (payload.display && payload.display.kind) fallback.display = payload.display;
|
|
302
|
+
}
|
|
303
|
+
state.items.push(fallback);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The active channel is scoped to a model response, not the whole task. A
|
|
307
|
+
* public content/tool delta ends the preceding reasoning presentation; a later
|
|
308
|
+
* real reasoning delta can reopen it. This supplies no synthetic content or
|
|
309
|
+
* duration, and cannot settle another response or a child stream. */
|
|
310
|
+
function settlePriorReasoning(state, event) {
|
|
311
|
+
if (!event.responseId) return;
|
|
312
|
+
for (let i = 0; i < state.items.length; i++) {
|
|
313
|
+
const item = state.items[i];
|
|
314
|
+
if (item.kind === 'thought' && item.streaming && item.responseId === event.responseId)
|
|
315
|
+
itemAt(state, i).streaming = false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function applyPlan(state, e) {
|
|
320
|
+
state.plan = Array.isArray(e.items) && e.items.length ? e.items : null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function findItemIndex(state, id) {
|
|
324
|
+
for (let i = 0; i < state.items.length; i += 1) if (state.items[i].id === id) return i;
|
|
325
|
+
return -1;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function findToolIndex(state, callId) {
|
|
329
|
+
for (let i = 0; i < state.items.length; i += 1) {
|
|
330
|
+
const item = state.items[i];
|
|
331
|
+
if (item.kind === "tool" && item.callId === callId) return i;
|
|
332
|
+
}
|
|
333
|
+
return -1;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Return a shallow-cloned item at `index` and store the clone back into the
|
|
338
|
+
* (already shallow-copied) items array. Callers then mutate the clone, so the
|
|
339
|
+
* previous state's item objects stay intact and snapshots a host holds are
|
|
340
|
+
* not corrupted by later events.
|
|
341
|
+
*/
|
|
342
|
+
function itemAt(state, index) {
|
|
343
|
+
const clone = { ...state.items[index] };
|
|
344
|
+
state.items[index] = clone;
|
|
345
|
+
return clone;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function shallowClone(state) {
|
|
349
|
+
return Object.assign({}, state, {
|
|
350
|
+
items: state.items,
|
|
351
|
+
seen: state.seen,
|
|
352
|
+
pendingThinking: state.pendingThinking,
|
|
353
|
+
plan: state.plan,
|
|
354
|
+
});
|
|
355
|
+
}
|