@windowkit/agent-chat 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -2
- package/dist/core/driver.d.ts +183 -0
- package/dist/core/driver.d.ts.map +1 -0
- package/dist/core/driver.js +12 -0
- package/dist/core/driver.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +11 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/markdown.d.ts +7 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +53 -0
- package/dist/core/markdown.js.map +1 -0
- package/dist/core/policy.d.ts +18 -0
- package/dist/core/policy.d.ts.map +1 -0
- package/dist/core/policy.js +67 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/reducer.d.ts +61 -0
- package/dist/core/reducer.d.ts.map +1 -0
- package/dist/core/reducer.js +391 -0
- package/dist/core/reducer.js.map +1 -0
- package/dist/core/session.d.ts +105 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +648 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/tools.d.ts +120 -0
- package/dist/core/tools.d.ts.map +1 -0
- package/dist/core/tools.js +120 -0
- package/dist/core/tools.js.map +1 -0
- package/dist/core/types.d.ts +295 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +21 -0
- package/dist/core/types.js.map +1 -0
- package/dist/drivers/claude/index.d.ts +20 -0
- package/dist/drivers/claude/index.d.ts.map +1 -0
- package/dist/drivers/claude/index.js +727 -0
- package/dist/drivers/claude/index.js.map +1 -0
- package/dist/drivers/claude/input.d.ts +7 -0
- package/dist/drivers/claude/input.d.ts.map +1 -0
- package/dist/drivers/claude/input.js +46 -0
- package/dist/drivers/claude/input.js.map +1 -0
- package/dist/drivers/claude/peers.d.ts +16 -0
- package/dist/drivers/claude/peers.d.ts.map +1 -0
- package/dist/drivers/claude/peers.js +137 -0
- package/dist/drivers/claude/peers.js.map +1 -0
- package/dist/drivers/claude/schema.d.ts +5 -0
- package/dist/drivers/claude/schema.d.ts.map +1 -0
- package/dist/drivers/claude/schema.js +77 -0
- package/dist/drivers/claude/schema.js.map +1 -0
- package/dist/drivers/claude/sdk.d.ts +86 -0
- package/dist/drivers/claude/sdk.d.ts.map +1 -0
- package/dist/drivers/claude/sdk.js +47 -0
- package/dist/drivers/claude/sdk.js.map +1 -0
- package/dist/drivers/claude/tools.d.ts +18 -0
- package/dist/drivers/claude/tools.d.ts.map +1 -0
- package/dist/drivers/claude/tools.js +119 -0
- package/dist/drivers/claude/tools.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/fake.d.ts +63 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +270 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/view/AgentChat.d.ts +53 -0
- package/dist/view/AgentChat.d.ts.map +1 -0
- package/dist/view/AgentChat.js +87 -0
- package/dist/view/AgentChat.js.map +1 -0
- package/dist/view/CodeBlock.d.ts +9 -0
- package/dist/view/CodeBlock.d.ts.map +1 -0
- package/dist/view/CodeBlock.js +60 -0
- package/dist/view/CodeBlock.js.map +1 -0
- package/dist/view/Composer.d.ts +24 -0
- package/dist/view/Composer.d.ts.map +1 -0
- package/dist/view/Composer.js +144 -0
- package/dist/view/Composer.js.map +1 -0
- package/dist/view/Header.d.ts +3 -0
- package/dist/view/Header.d.ts.map +1 -0
- package/dist/view/Header.js +30 -0
- package/dist/view/Header.js.map +1 -0
- package/dist/view/MessageList.d.ts +9 -0
- package/dist/view/MessageList.d.ts.map +1 -0
- package/dist/view/MessageList.js +111 -0
- package/dist/view/MessageList.js.map +1 -0
- package/dist/view/QuestionCard.d.ts +6 -0
- package/dist/view/QuestionCard.d.ts.map +1 -0
- package/dist/view/QuestionCard.js +39 -0
- package/dist/view/QuestionCard.js.map +1 -0
- package/dist/view/TextBlock.d.ts +9 -0
- package/dist/view/TextBlock.d.ts.map +1 -0
- package/dist/view/TextBlock.js +22 -0
- package/dist/view/TextBlock.js.map +1 -0
- package/dist/view/ToolCard.d.ts +7 -0
- package/dist/view/ToolCard.d.ts.map +1 -0
- package/dist/view/ToolCard.js +155 -0
- package/dist/view/ToolCard.js.map +1 -0
- package/dist/view/Transcript.d.ts +5 -0
- package/dist/view/Transcript.d.ts.map +1 -0
- package/dist/view/Transcript.js +47 -0
- package/dist/view/Transcript.js.map +1 -0
- package/dist/view/context.d.ts +21 -0
- package/dist/view/context.d.ts.map +1 -0
- package/dist/view/context.js +37 -0
- package/dist/view/context.js.map +1 -0
- package/dist/view/fences.d.ts +13 -0
- package/dist/view/fences.d.ts.map +1 -0
- package/dist/view/fences.js +59 -0
- package/dist/view/fences.js.map +1 -0
- package/dist/view/files.d.ts +11 -0
- package/dist/view/files.d.ts.map +1 -0
- package/dist/view/files.js +109 -0
- package/dist/view/files.js.map +1 -0
- package/dist/view/look.d.ts +18 -0
- package/dist/view/look.d.ts.map +1 -0
- package/dist/view/look.js +37 -0
- package/dist/view/look.js.map +1 -0
- package/dist/view/types.d.ts +50 -0
- package/dist/view/types.d.ts.map +1 -0
- package/dist/view/types.js +14 -0
- package/dist/view/types.js.map +1 -0
- package/dist/view/use-agent.d.ts +11 -0
- package/dist/view/use-agent.d.ts.map +1 -0
- package/dist/view/use-agent.js +33 -0
- package/dist/view/use-agent.js.map +1 -0
- package/package.json +92 -4
- package/src/core/driver.ts +191 -0
- package/src/core/index.ts +54 -0
- package/src/core/markdown.ts +56 -0
- package/src/core/policy.ts +77 -0
- package/src/core/reducer.ts +490 -0
- package/src/core/session.ts +824 -0
- package/src/core/tools.ts +233 -0
- package/src/core/types.ts +321 -0
- package/src/drivers/claude/index.ts +825 -0
- package/src/drivers/claude/input.ts +47 -0
- package/src/drivers/claude/peers.ts +153 -0
- package/src/drivers/claude/schema.ts +99 -0
- package/src/drivers/claude/sdk.ts +138 -0
- package/src/drivers/claude/tools.ts +141 -0
- package/src/index.ts +51 -0
- package/src/testing/fake.ts +374 -0
- package/src/testing/index.ts +11 -0
- package/src/view/AgentChat.tsx +204 -0
- package/src/view/CodeBlock.tsx +108 -0
- package/src/view/Composer.tsx +230 -0
- package/src/view/Header.tsx +73 -0
- package/src/view/MessageList.tsx +290 -0
- package/src/view/QuestionCard.tsx +107 -0
- package/src/view/TextBlock.tsx +58 -0
- package/src/view/ToolCard.tsx +283 -0
- package/src/view/Transcript.tsx +81 -0
- package/src/view/context.ts +70 -0
- package/src/view/dom-globals.d.ts +10 -0
- package/src/view/fences.ts +73 -0
- package/src/view/files.ts +111 -0
- package/src/view/look.ts +53 -0
- package/src/view/types.ts +71 -0
- package/src/view/use-agent.ts +40 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// The transcript, as a pure function of what happened to it.
|
|
2
|
+
//
|
|
3
|
+
// Every change makes new objects along the path it touched and shares the
|
|
4
|
+
// rest: a streamed token replaces one text part, its message and the array,
|
|
5
|
+
// and every other message is the object it was. The view memoizes a message
|
|
6
|
+
// by identity, so a token re-renders one message.
|
|
7
|
+
|
|
8
|
+
import type { DriverEvent } from './driver.js';
|
|
9
|
+
import type {
|
|
10
|
+
AgentMessage,
|
|
11
|
+
ApprovalOption,
|
|
12
|
+
Part,
|
|
13
|
+
Question,
|
|
14
|
+
ToolOutput,
|
|
15
|
+
ToolPart,
|
|
16
|
+
ToolState,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
export type TranscriptAction =
|
|
20
|
+
| DriverEvent
|
|
21
|
+
/** The user sent a message. */
|
|
22
|
+
| { type: 'user'; message: AgentMessage }
|
|
23
|
+
/** A queued message is being sent now. */
|
|
24
|
+
| { type: 'unqueue'; id: string }
|
|
25
|
+
/** A tool call waits for the user. */
|
|
26
|
+
| {
|
|
27
|
+
type: 'approval';
|
|
28
|
+
id: string;
|
|
29
|
+
options: readonly ApprovalOption[];
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
/** The user, or the policy, answered it. */
|
|
33
|
+
| { type: 'approved'; id: string; allowed: boolean }
|
|
34
|
+
| { type: 'question'; id: string; questions: readonly Question[] }
|
|
35
|
+
| {
|
|
36
|
+
type: 'answered';
|
|
37
|
+
id: string;
|
|
38
|
+
answers: Readonly<Record<string, string>> | null;
|
|
39
|
+
}
|
|
40
|
+
/** The turn was stopped: what was open is cancelled. */
|
|
41
|
+
| { type: 'stopped' };
|
|
42
|
+
|
|
43
|
+
type Messages = readonly AgentMessage[];
|
|
44
|
+
|
|
45
|
+
const OPEN_TOOL: ReadonlySet<ToolState> = new Set([
|
|
46
|
+
'input-streaming',
|
|
47
|
+
'pending',
|
|
48
|
+
'awaiting-approval',
|
|
49
|
+
'running',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export function reduce(
|
|
53
|
+
messages: Messages,
|
|
54
|
+
action: TranscriptAction,
|
|
55
|
+
now: number = Date.now(),
|
|
56
|
+
): Messages {
|
|
57
|
+
switch (action.type) {
|
|
58
|
+
case 'user':
|
|
59
|
+
return [...messages, action.message];
|
|
60
|
+
case 'unqueue':
|
|
61
|
+
return mapMessage(messages, action.id, (m) => ({
|
|
62
|
+
...m,
|
|
63
|
+
queued: false,
|
|
64
|
+
}));
|
|
65
|
+
case 'text':
|
|
66
|
+
return action.parent
|
|
67
|
+
? inChildren(messages, action.parent, now, (kids) =>
|
|
68
|
+
text(kids, action, now),
|
|
69
|
+
)
|
|
70
|
+
: text(messages, action, now);
|
|
71
|
+
case 'tool-start':
|
|
72
|
+
return action.parent
|
|
73
|
+
? inChildren(messages, action.parent, now, (kids) =>
|
|
74
|
+
toolStart(kids, action, now),
|
|
75
|
+
)
|
|
76
|
+
: toolStart(messages, action, now);
|
|
77
|
+
case 'tool-input':
|
|
78
|
+
return mapPart(messages, action.id, (p) => {
|
|
79
|
+
if (p.type !== 'tool') return p;
|
|
80
|
+
const inputText = p.inputText + action.delta;
|
|
81
|
+
return { ...p, inputText, input: parseJson(inputText) ?? p.input };
|
|
82
|
+
});
|
|
83
|
+
case 'tool-update':
|
|
84
|
+
return mapPart(messages, action.id, (p) => {
|
|
85
|
+
if (p.type !== 'tool') return p;
|
|
86
|
+
const next: ToolPart = { ...p };
|
|
87
|
+
if (action.input !== undefined) {
|
|
88
|
+
next.input = action.input;
|
|
89
|
+
next.inputText = JSON.stringify(action.input);
|
|
90
|
+
if (next.state === 'input-streaming') next.state = 'pending';
|
|
91
|
+
}
|
|
92
|
+
if (action.title !== undefined) next.title = action.title;
|
|
93
|
+
if (action.kind !== undefined) next.kind = action.kind;
|
|
94
|
+
if (action.state !== undefined) next.state = action.state;
|
|
95
|
+
if (action.output) next.output = action.output;
|
|
96
|
+
if (action.append) next.output = [...next.output, action.append];
|
|
97
|
+
if (action.progress !== undefined) {
|
|
98
|
+
next.output = [
|
|
99
|
+
...next.output.filter((o) => !isProgress(o)),
|
|
100
|
+
{ type: 'text', text: action.progress, progress: true },
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
case 'tool-end':
|
|
106
|
+
return mapPart(messages, action.id, (p) => {
|
|
107
|
+
if (p.type !== 'tool') return p;
|
|
108
|
+
const kept = p.output.filter((o) => !isProgress(o));
|
|
109
|
+
const next: ToolPart = {
|
|
110
|
+
...p,
|
|
111
|
+
state: action.state,
|
|
112
|
+
output: action.output ? [...kept, ...action.output] : kept,
|
|
113
|
+
};
|
|
114
|
+
delete next.approval;
|
|
115
|
+
if (action.error !== undefined) next.error = action.error;
|
|
116
|
+
if (next.state === 'done' && next.inputText === '') {
|
|
117
|
+
next.inputText = JSON.stringify(next.input ?? {});
|
|
118
|
+
}
|
|
119
|
+
return next;
|
|
120
|
+
});
|
|
121
|
+
case 'message':
|
|
122
|
+
return incoming(messages, action, now);
|
|
123
|
+
case 'plan':
|
|
124
|
+
return action.parent
|
|
125
|
+
? inChildren(messages, action.parent, now, (kids) =>
|
|
126
|
+
plan(kids, action.entries, now),
|
|
127
|
+
)
|
|
128
|
+
: plan(messages, action.entries, now);
|
|
129
|
+
case 'notice':
|
|
130
|
+
return appendPart(messages, now, (m) => ({
|
|
131
|
+
type: 'notice',
|
|
132
|
+
id: `${m.id}:n${m.parts.length}`,
|
|
133
|
+
level: action.level,
|
|
134
|
+
text: action.text,
|
|
135
|
+
}));
|
|
136
|
+
case 'approval':
|
|
137
|
+
return mapPart(messages, action.id, (p) =>
|
|
138
|
+
p.type === 'tool'
|
|
139
|
+
? {
|
|
140
|
+
...p,
|
|
141
|
+
state: 'awaiting-approval',
|
|
142
|
+
approval: { options: action.options, reason: action.reason },
|
|
143
|
+
}
|
|
144
|
+
: p,
|
|
145
|
+
);
|
|
146
|
+
case 'approved':
|
|
147
|
+
return mapPart(messages, action.id, (p) => {
|
|
148
|
+
if (p.type !== 'tool') return p;
|
|
149
|
+
const next: ToolPart = {
|
|
150
|
+
...p,
|
|
151
|
+
state: action.allowed
|
|
152
|
+
? p.state === 'awaiting-approval'
|
|
153
|
+
? 'running'
|
|
154
|
+
: p.state
|
|
155
|
+
: 'denied',
|
|
156
|
+
};
|
|
157
|
+
delete next.approval;
|
|
158
|
+
return next;
|
|
159
|
+
});
|
|
160
|
+
case 'question':
|
|
161
|
+
return appendPart(messages, now, () => ({
|
|
162
|
+
type: 'question',
|
|
163
|
+
id: action.id,
|
|
164
|
+
questions: action.questions,
|
|
165
|
+
state: 'open',
|
|
166
|
+
}));
|
|
167
|
+
case 'answered':
|
|
168
|
+
return mapPart(messages, action.id, (p) =>
|
|
169
|
+
p.type === 'question'
|
|
170
|
+
? action.answers
|
|
171
|
+
? { ...p, state: 'answered', answers: action.answers }
|
|
172
|
+
: { ...p, state: 'cancelled' }
|
|
173
|
+
: p,
|
|
174
|
+
);
|
|
175
|
+
case 'stopped':
|
|
176
|
+
return closeTurn(messages, true);
|
|
177
|
+
case 'turn-end':
|
|
178
|
+
return closeTurn(messages, action.reason === 'stopped');
|
|
179
|
+
case 'info':
|
|
180
|
+
case 'usage':
|
|
181
|
+
return messages;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- the assistant message a turn writes to --------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The index of the message the current turn writes to, making one if the
|
|
189
|
+
* turn has none yet. A queued user message sits after the running turn's
|
|
190
|
+
* message, so it is passed over.
|
|
191
|
+
*/
|
|
192
|
+
function current(messages: Messages, now: number): [Messages, number] {
|
|
193
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
194
|
+
const m = messages[i]!;
|
|
195
|
+
if (m.role === 'user' && m.queued) continue;
|
|
196
|
+
if (m.role === 'assistant' && !m.done) return [messages, i];
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const fresh: AgentMessage = {
|
|
200
|
+
id: `a${messages.length}:${now.toString(36)}`,
|
|
201
|
+
role: 'assistant',
|
|
202
|
+
parts: [],
|
|
203
|
+
createdAt: now,
|
|
204
|
+
};
|
|
205
|
+
const at = beforeQueued(messages);
|
|
206
|
+
return [[...messages.slice(0, at), fresh, ...messages.slice(at)], at];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Where something the harness is acting on now goes: before any queued
|
|
210
|
+
* user messages, which wait for the turn to end. */
|
|
211
|
+
function beforeQueued(messages: Messages): number {
|
|
212
|
+
let at = messages.length;
|
|
213
|
+
while (
|
|
214
|
+
at > 0 &&
|
|
215
|
+
messages[at - 1]!.role === 'user' &&
|
|
216
|
+
messages[at - 1]!.queued
|
|
217
|
+
) {
|
|
218
|
+
at--;
|
|
219
|
+
}
|
|
220
|
+
return at;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A message from someone other than the user, which the harness took. A
|
|
224
|
+
* driver that reports one twice is reporting the same message. */
|
|
225
|
+
function incoming(
|
|
226
|
+
messages: Messages,
|
|
227
|
+
ev: Extract<DriverEvent, { type: 'message' }>,
|
|
228
|
+
now: number,
|
|
229
|
+
): Messages {
|
|
230
|
+
if (messages.some((m) => m.id === ev.id)) return messages;
|
|
231
|
+
const message: AgentMessage = {
|
|
232
|
+
id: ev.id,
|
|
233
|
+
role: 'user',
|
|
234
|
+
parts: [
|
|
235
|
+
{ type: 'text', id: `${ev.id}:t`, text: ev.text, streaming: false },
|
|
236
|
+
],
|
|
237
|
+
createdAt: now,
|
|
238
|
+
from: ev.from,
|
|
239
|
+
};
|
|
240
|
+
const at = beforeQueued(messages);
|
|
241
|
+
return [...messages.slice(0, at), message, ...messages.slice(at)];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function appendPart(
|
|
245
|
+
messages: Messages,
|
|
246
|
+
now: number,
|
|
247
|
+
make: (m: AgentMessage) => Part,
|
|
248
|
+
): Messages {
|
|
249
|
+
const [all, i] = current(messages, now);
|
|
250
|
+
const m = all[i]!;
|
|
251
|
+
return replaceAt(all, i, { ...m, parts: [...m.parts, make(m)] });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function text(
|
|
255
|
+
messages: Messages,
|
|
256
|
+
ev: Extract<DriverEvent, { type: 'text' }>,
|
|
257
|
+
now: number,
|
|
258
|
+
): Messages {
|
|
259
|
+
const kind = ev.reasoning ? 'reasoning' : 'text';
|
|
260
|
+
const found = findLivePart(messages, ev.id);
|
|
261
|
+
if (found) {
|
|
262
|
+
return mapPart(messages, ev.id, (p) =>
|
|
263
|
+
p.type === 'text' || p.type === 'reasoning'
|
|
264
|
+
? {
|
|
265
|
+
...p,
|
|
266
|
+
text: ev.text ?? p.text + (ev.delta ?? ''),
|
|
267
|
+
streaming: !ev.done,
|
|
268
|
+
}
|
|
269
|
+
: p,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return appendPart(messages, now, () => ({
|
|
273
|
+
type: kind,
|
|
274
|
+
id: ev.id,
|
|
275
|
+
text: ev.text ?? ev.delta ?? '',
|
|
276
|
+
streaming: !ev.done,
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function toolStart(
|
|
281
|
+
messages: Messages,
|
|
282
|
+
ev: Extract<DriverEvent, { type: 'tool-start' }>,
|
|
283
|
+
now: number,
|
|
284
|
+
): Messages {
|
|
285
|
+
if (findLivePart(messages, ev.id)) {
|
|
286
|
+
// a driver that saw the call twice (a stream event, then the block)
|
|
287
|
+
return mapPart(messages, ev.id, (p) =>
|
|
288
|
+
p.type === 'tool'
|
|
289
|
+
? {
|
|
290
|
+
...p,
|
|
291
|
+
...(ev.input !== undefined
|
|
292
|
+
? { input: ev.input, inputText: JSON.stringify(ev.input) }
|
|
293
|
+
: {}),
|
|
294
|
+
...(ev.title ? { title: ev.title } : {}),
|
|
295
|
+
...(ev.kind ? { kind: ev.kind } : {}),
|
|
296
|
+
}
|
|
297
|
+
: p,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const label = ev.label ?? ev.name;
|
|
301
|
+
return appendPart(messages, now, () => ({
|
|
302
|
+
type: 'tool',
|
|
303
|
+
id: ev.id,
|
|
304
|
+
name: ev.name,
|
|
305
|
+
label,
|
|
306
|
+
kind: ev.kind ?? 'other',
|
|
307
|
+
title: ev.title ?? label,
|
|
308
|
+
input: ev.input,
|
|
309
|
+
inputText: ev.input === undefined ? '' : JSON.stringify(ev.input),
|
|
310
|
+
state: ev.input === undefined ? 'input-streaming' : 'pending',
|
|
311
|
+
output: [],
|
|
312
|
+
app: ev.app ?? false,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function plan(
|
|
317
|
+
messages: Messages,
|
|
318
|
+
entries: Extract<DriverEvent, { type: 'plan' }>['entries'],
|
|
319
|
+
now: number,
|
|
320
|
+
): Messages {
|
|
321
|
+
const [all, i] = current(messages, now);
|
|
322
|
+
const m = all[i]!;
|
|
323
|
+
// one plan a turn, updated in place: a checklist, not a log of checklists
|
|
324
|
+
const at = m.parts.findIndex((p) => p.type === 'plan');
|
|
325
|
+
const part: Part = {
|
|
326
|
+
type: 'plan',
|
|
327
|
+
id: at >= 0 ? m.parts[at]!.id : `${m.id}:plan`,
|
|
328
|
+
entries,
|
|
329
|
+
};
|
|
330
|
+
const parts =
|
|
331
|
+
at >= 0
|
|
332
|
+
? [...m.parts.slice(0, at), part, ...m.parts.slice(at + 1)]
|
|
333
|
+
: [...m.parts, part];
|
|
334
|
+
return replaceAt(all, i, { ...m, parts });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Apply `fn` to the children of the tool call `parent` — a subagent. */
|
|
338
|
+
function inChildren(
|
|
339
|
+
messages: Messages,
|
|
340
|
+
parent: string,
|
|
341
|
+
now: number,
|
|
342
|
+
fn: (children: Messages) => Messages,
|
|
343
|
+
): Messages {
|
|
344
|
+
if (!findLivePart(messages, parent)) return fn(messages);
|
|
345
|
+
return mapPart(messages, parent, (p) =>
|
|
346
|
+
p.type === 'tool' ? { ...p, children: fn(p.children ?? []) } : p,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* A transcript saved from an earlier session, made ready to show again:
|
|
352
|
+
* whatever was queued was never sent and is dropped, and whatever was still
|
|
353
|
+
* open ends as a stopped turn does — a stream stops streaming, a tool call
|
|
354
|
+
* is cancelled, a question is closed. Nothing in it is live any more.
|
|
355
|
+
*/
|
|
356
|
+
export function restore(messages: Messages): Messages {
|
|
357
|
+
return closeTurn(
|
|
358
|
+
messages.filter((m) => !(m.role === 'user' && m.queued)),
|
|
359
|
+
true,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function closeTurn(messages: Messages, stopped: boolean): Messages {
|
|
364
|
+
let changed = false;
|
|
365
|
+
const out = messages.map((m) => {
|
|
366
|
+
if (m.role !== 'assistant' || m.done) return m;
|
|
367
|
+
changed = true;
|
|
368
|
+
return { ...m, done: true, parts: m.parts.map((p) => settle(p, stopped)) };
|
|
369
|
+
});
|
|
370
|
+
return changed ? out : messages;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function settle(p: Part, stopped: boolean): Part {
|
|
374
|
+
if ((p.type === 'text' || p.type === 'reasoning') && p.streaming) {
|
|
375
|
+
return { ...p, streaming: false };
|
|
376
|
+
}
|
|
377
|
+
if (p.type === 'tool') {
|
|
378
|
+
const children = p.children?.map((m) =>
|
|
379
|
+
m.done
|
|
380
|
+
? m
|
|
381
|
+
: { ...m, done: true, parts: m.parts.map((c) => settle(c, stopped)) },
|
|
382
|
+
);
|
|
383
|
+
if (OPEN_TOOL.has(p.state)) {
|
|
384
|
+
const next: ToolPart = {
|
|
385
|
+
...p,
|
|
386
|
+
state: stopped ? 'cancelled' : 'done',
|
|
387
|
+
output: p.output.filter((o) => !isProgress(o)),
|
|
388
|
+
...(children ? { children } : {}),
|
|
389
|
+
};
|
|
390
|
+
delete next.approval;
|
|
391
|
+
return next;
|
|
392
|
+
}
|
|
393
|
+
return children ? { ...p, children } : p;
|
|
394
|
+
}
|
|
395
|
+
if (p.type === 'question' && p.state === 'open') {
|
|
396
|
+
return { ...p, state: 'cancelled' };
|
|
397
|
+
}
|
|
398
|
+
return p;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// --- finding and replacing, children included ------------------------------
|
|
402
|
+
|
|
403
|
+
/** The part with this id, in any message. */
|
|
404
|
+
export function findPart(messages: Messages, id: string): Part | undefined {
|
|
405
|
+
return find(messages, id, false);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The part with this id in a turn still under way: what an event may touch.
|
|
410
|
+
* A driver's ids are unique within a turn, not across a session — a script
|
|
411
|
+
* that names its calls runs the same names every turn — so a finished turn's
|
|
412
|
+
* parts are never the ones an event means. They keep the state they ended in.
|
|
413
|
+
*/
|
|
414
|
+
export function findLivePart(messages: Messages, id: string): Part | undefined {
|
|
415
|
+
return find(messages, id, true);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function find(messages: Messages, id: string, live: boolean): Part | undefined {
|
|
419
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
420
|
+
const m = messages[i]!;
|
|
421
|
+
if (live && m.done) continue;
|
|
422
|
+
for (const p of m.parts) {
|
|
423
|
+
if (p.id === id) return p;
|
|
424
|
+
if (p.type === 'tool' && p.children) {
|
|
425
|
+
const inner = find(p.children, id, live);
|
|
426
|
+
if (inner) return inner;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Replace the live part with this id (`findLivePart`'s), and the path to it. */
|
|
434
|
+
function mapPart(
|
|
435
|
+
messages: Messages,
|
|
436
|
+
id: string,
|
|
437
|
+
fn: (p: Part) => Part,
|
|
438
|
+
): Messages {
|
|
439
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
440
|
+
const m = messages[i]!;
|
|
441
|
+
if (m.done) continue;
|
|
442
|
+
for (let j = 0; j < m.parts.length; j++) {
|
|
443
|
+
const p = m.parts[j]!;
|
|
444
|
+
let next: Part | undefined;
|
|
445
|
+
if (p.id === id) next = fn(p);
|
|
446
|
+
else if (
|
|
447
|
+
p.type === 'tool' &&
|
|
448
|
+
p.children &&
|
|
449
|
+
findLivePart(p.children, id)
|
|
450
|
+
) {
|
|
451
|
+
next = { ...p, children: mapPart(p.children, id, fn) };
|
|
452
|
+
}
|
|
453
|
+
if (next !== undefined) {
|
|
454
|
+
if (next === p) return messages;
|
|
455
|
+
return replaceAt(messages, i, {
|
|
456
|
+
...m,
|
|
457
|
+
parts: [...m.parts.slice(0, j), next, ...m.parts.slice(j + 1)],
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return messages;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function mapMessage(
|
|
466
|
+
messages: Messages,
|
|
467
|
+
id: string,
|
|
468
|
+
fn: (m: AgentMessage) => AgentMessage,
|
|
469
|
+
): Messages {
|
|
470
|
+
const i = messages.findIndex((m) => m.id === id);
|
|
471
|
+
return i < 0 ? messages : replaceAt(messages, i, fn(messages[i]!));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function replaceAt(messages: Messages, i: number, m: AgentMessage): Messages {
|
|
475
|
+
return [...messages.slice(0, i), m, ...messages.slice(i + 1)];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function parseJson(text: string): unknown {
|
|
479
|
+
try {
|
|
480
|
+
return JSON.parse(text);
|
|
481
|
+
} catch {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** A tool's progress line: a text output it replaces as it goes, and drops
|
|
487
|
+
* when the tool ends. */
|
|
488
|
+
export function isProgress(o: ToolOutput): boolean {
|
|
489
|
+
return o.type === 'text' && o.progress === true;
|
|
490
|
+
}
|