@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,648 @@
|
|
|
1
|
+
// A session: the transcript, the harness behind it, and every question the
|
|
2
|
+
// harness asks the client — answered by the app's policy, or by the user.
|
|
3
|
+
//
|
|
4
|
+
// It is a store in the `useSyncExternalStore` sense: `snapshot` is one
|
|
5
|
+
// immutable object, replaced on every change, and `subscribe` hears each
|
|
6
|
+
// replacement. Nothing in it is React's, so a terminal or DOM front end can
|
|
7
|
+
// drive the same object.
|
|
8
|
+
import { AgentUnavailableError, } from './driver.js';
|
|
9
|
+
import { transcriptMarkdown } from './markdown.js';
|
|
10
|
+
import { policyDecision } from './policy.js';
|
|
11
|
+
import { reduce, restore, findLivePart, } from './reducer.js';
|
|
12
|
+
import { errorResult, normalizeResult, validateInput, } from './tools.js';
|
|
13
|
+
import { NO_CAPABILITIES, } from './types.js';
|
|
14
|
+
const APPROVAL_OPTIONS = Object.freeze([
|
|
15
|
+
{ id: 'allow-once', label: 'Allow', kind: 'allow' },
|
|
16
|
+
{ id: 'allow-session', label: 'Allow for this session', kind: 'allow' },
|
|
17
|
+
{ id: 'deny', label: 'Deny', kind: 'deny' },
|
|
18
|
+
]);
|
|
19
|
+
const BUSY = new Set([
|
|
20
|
+
'starting',
|
|
21
|
+
'running',
|
|
22
|
+
'waiting',
|
|
23
|
+
'paused',
|
|
24
|
+
]);
|
|
25
|
+
export function createAgentSession(options = {}) {
|
|
26
|
+
return new Session(options);
|
|
27
|
+
}
|
|
28
|
+
/** Lines that tell the agent what the UI can draw — see `describe` on a
|
|
29
|
+
* fence or a component. */
|
|
30
|
+
export function describeRenderers(renderers = {}) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (const [lang, entry] of Object.entries(renderers.fences ?? {})) {
|
|
33
|
+
const describe = describeOf(entry);
|
|
34
|
+
if (describe) {
|
|
35
|
+
lines.push(`The UI draws a fenced code block tagged \`${lang}\` as: ${describe}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [name, entry] of Object.entries(renderers.components ?? {})) {
|
|
39
|
+
const describe = describeOf(entry);
|
|
40
|
+
if (describe) {
|
|
41
|
+
lines.push(`The UI draws a <${name}/> tag, alone on its line, as: ${describe}. ` +
|
|
42
|
+
'Its attributes are strings or JSON.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return lines.length
|
|
46
|
+
? ['You are shown in a chat UI that renders markdown.', ...lines]
|
|
47
|
+
: [];
|
|
48
|
+
}
|
|
49
|
+
function describeOf(entry) {
|
|
50
|
+
return entry && typeof entry === 'object' && 'describe' in entry
|
|
51
|
+
? String(entry.describe)
|
|
52
|
+
: undefined;
|
|
53
|
+
}
|
|
54
|
+
class Session {
|
|
55
|
+
#options;
|
|
56
|
+
#config;
|
|
57
|
+
#snapshot;
|
|
58
|
+
#listeners = new Set();
|
|
59
|
+
#driver = null;
|
|
60
|
+
#starting = null;
|
|
61
|
+
#turn = null;
|
|
62
|
+
#queue = [];
|
|
63
|
+
#ids = 0;
|
|
64
|
+
#decisions = new Map();
|
|
65
|
+
#sessionAllowed = new Set();
|
|
66
|
+
#approvals = new Map();
|
|
67
|
+
#questions = new Map();
|
|
68
|
+
#paused = false;
|
|
69
|
+
#resumers = new Set();
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.#options = options;
|
|
72
|
+
this.#config = resolveConfig(options);
|
|
73
|
+
const messages = restore(options.messages ?? []);
|
|
74
|
+
// the user's next message continues the saved ones' numbering, so its
|
|
75
|
+
// id is not one already in the transcript
|
|
76
|
+
for (const m of messages) {
|
|
77
|
+
const n = /^u(\d+)$/.exec(m.id);
|
|
78
|
+
if (n)
|
|
79
|
+
this.#ids = Math.max(this.#ids, Number(n[1]));
|
|
80
|
+
}
|
|
81
|
+
const driverName = typeof options.agent === 'object' ? options.agent.name : 'claude';
|
|
82
|
+
this.#snapshot = Object.freeze({
|
|
83
|
+
status: 'idle',
|
|
84
|
+
messages,
|
|
85
|
+
info: {
|
|
86
|
+
agent: driverName,
|
|
87
|
+
cwd: this.#config.cwd,
|
|
88
|
+
models: [],
|
|
89
|
+
modes: [],
|
|
90
|
+
commands: [],
|
|
91
|
+
},
|
|
92
|
+
capabilities: NO_CAPABILITIES,
|
|
93
|
+
paused: false,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
get snapshot() {
|
|
97
|
+
return this.#snapshot;
|
|
98
|
+
}
|
|
99
|
+
subscribe = (listener) => {
|
|
100
|
+
this.#listeners.add(listener);
|
|
101
|
+
return () => {
|
|
102
|
+
this.#listeners.delete(listener);
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
get tools() {
|
|
106
|
+
return this.#config.tools;
|
|
107
|
+
}
|
|
108
|
+
#set(patch) {
|
|
109
|
+
this.#snapshot = Object.freeze({ ...this.#snapshot, ...patch });
|
|
110
|
+
for (const l of [...this.#listeners])
|
|
111
|
+
l();
|
|
112
|
+
}
|
|
113
|
+
#dispatch(action) {
|
|
114
|
+
const messages = reduce(this.#snapshot.messages, action);
|
|
115
|
+
if (messages !== this.#snapshot.messages)
|
|
116
|
+
this.#set({ messages });
|
|
117
|
+
}
|
|
118
|
+
// --- the harness -----------------------------------------------------------
|
|
119
|
+
start() {
|
|
120
|
+
return this.#start().then(() => undefined);
|
|
121
|
+
}
|
|
122
|
+
#start() {
|
|
123
|
+
if (this.#driver)
|
|
124
|
+
return Promise.resolve(this.#driver);
|
|
125
|
+
if (this.#starting)
|
|
126
|
+
return this.#starting;
|
|
127
|
+
this.#set({ status: 'starting', error: undefined });
|
|
128
|
+
this.#starting = (async () => {
|
|
129
|
+
try {
|
|
130
|
+
const driver = await loadDriver(this.#options.agent);
|
|
131
|
+
const session = await driver.start(this.#host());
|
|
132
|
+
this.#driver = session;
|
|
133
|
+
this.#set({
|
|
134
|
+
capabilities: session.capabilities,
|
|
135
|
+
status: this.#turn ? 'running' : 'idle',
|
|
136
|
+
});
|
|
137
|
+
return session;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const unavailable = err instanceof AgentUnavailableError;
|
|
141
|
+
this.#set({
|
|
142
|
+
status: unavailable ? 'unavailable' : 'error',
|
|
143
|
+
error: err instanceof Error ? err.message : String(err),
|
|
144
|
+
});
|
|
145
|
+
this.#turn = null;
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
this.#starting = null;
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
return this.#starting;
|
|
153
|
+
}
|
|
154
|
+
#host() {
|
|
155
|
+
return {
|
|
156
|
+
config: this.#config,
|
|
157
|
+
emit: (event) => this.#onEvent(event),
|
|
158
|
+
gate: (call) => this.#gate(call, true),
|
|
159
|
+
approve: (call, signal) => this.#approve(call, signal),
|
|
160
|
+
ask: (id, questions, signal) => this.#ask(id, questions, signal),
|
|
161
|
+
runTool: (name, input, ctx) => this.#runTool(name, input, ctx),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
#onEvent(event) {
|
|
165
|
+
switch (event.type) {
|
|
166
|
+
case 'info':
|
|
167
|
+
this.#set({ info: { ...this.#snapshot.info, ...event.info } });
|
|
168
|
+
return;
|
|
169
|
+
case 'usage':
|
|
170
|
+
this.#set({
|
|
171
|
+
info: {
|
|
172
|
+
...this.#snapshot.info,
|
|
173
|
+
usage: { ...this.#snapshot.info.usage, ...event.usage },
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
case 'message':
|
|
178
|
+
// the harness took it and is acting on it: a turn, if none runs
|
|
179
|
+
this.#dispatch(event);
|
|
180
|
+
if (!this.#turn) {
|
|
181
|
+
this.#turn = new AbortController();
|
|
182
|
+
this.#set({ status: this.#paused ? 'paused' : 'running' });
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
case 'turn-end':
|
|
186
|
+
this.#dispatch(event);
|
|
187
|
+
if (event.reason === 'error' && event.error) {
|
|
188
|
+
this.#dispatch({ type: 'notice', level: 'error', text: event.error });
|
|
189
|
+
}
|
|
190
|
+
this.#endTurn();
|
|
191
|
+
return;
|
|
192
|
+
default:
|
|
193
|
+
this.#dispatch(event);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
#endTurn() {
|
|
197
|
+
this.#turn = null;
|
|
198
|
+
this.#decisions.clear();
|
|
199
|
+
const next = this.#queue.shift();
|
|
200
|
+
if (next) {
|
|
201
|
+
this.#dispatch({ type: 'unqueue', id: next.id });
|
|
202
|
+
void this.#deliver(next);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (BUSY.has(this.#snapshot.status))
|
|
206
|
+
this.#set({ status: 'idle' });
|
|
207
|
+
}
|
|
208
|
+
// --- the user's side -------------------------------------------------------
|
|
209
|
+
send(input) {
|
|
210
|
+
const composed = typeof input === 'string' ? { text: input } : input;
|
|
211
|
+
const text = composed.text;
|
|
212
|
+
const attachments = composed.attachments ?? [];
|
|
213
|
+
const from = composed.from;
|
|
214
|
+
if (!text.trim() && !attachments.length)
|
|
215
|
+
return;
|
|
216
|
+
if (this.#snapshot.status === 'closed')
|
|
217
|
+
return;
|
|
218
|
+
const id = `u${++this.#ids}`;
|
|
219
|
+
const parts = [];
|
|
220
|
+
if (text)
|
|
221
|
+
parts.push({ type: 'text', id: `${id}:t`, text, streaming: false });
|
|
222
|
+
attachments.forEach((a, i) => parts.push({
|
|
223
|
+
type: 'file',
|
|
224
|
+
id: `${id}:f${i}`,
|
|
225
|
+
label: a.label,
|
|
226
|
+
mime: a.mime ?? (a.kind === 'image' ? 'image/png' : 'text/plain'),
|
|
227
|
+
...(a.path ? { path: a.path } : {}),
|
|
228
|
+
...(a.data !== undefined ? { data: a.data } : {}),
|
|
229
|
+
}));
|
|
230
|
+
const busy = !!this.#turn;
|
|
231
|
+
const steer = busy && this.#snapshot.capabilities.steer;
|
|
232
|
+
const queued = busy && !steer;
|
|
233
|
+
this.#dispatch({
|
|
234
|
+
type: 'user',
|
|
235
|
+
message: {
|
|
236
|
+
id,
|
|
237
|
+
role: 'user',
|
|
238
|
+
parts,
|
|
239
|
+
createdAt: Date.now(),
|
|
240
|
+
...(from ? { from } : {}),
|
|
241
|
+
...(queued ? { queued: true } : {}),
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const out = {
|
|
245
|
+
id,
|
|
246
|
+
text,
|
|
247
|
+
attachments,
|
|
248
|
+
steer,
|
|
249
|
+
...(from ? { from } : {}),
|
|
250
|
+
};
|
|
251
|
+
if (queued)
|
|
252
|
+
this.#queue.push(out);
|
|
253
|
+
else
|
|
254
|
+
void this.#deliver(out);
|
|
255
|
+
}
|
|
256
|
+
async #deliver(message) {
|
|
257
|
+
if (!message.steer)
|
|
258
|
+
this.#turn = new AbortController();
|
|
259
|
+
if (!this.#paused)
|
|
260
|
+
this.#set({ status: 'running', restored: undefined });
|
|
261
|
+
const driver = await this.#start();
|
|
262
|
+
if (!driver)
|
|
263
|
+
return;
|
|
264
|
+
if (!this.#turn && !message.steer)
|
|
265
|
+
return; // stopped while starting
|
|
266
|
+
if (!BUSY.has(this.#snapshot.status))
|
|
267
|
+
this.#set({ status: 'running' });
|
|
268
|
+
try {
|
|
269
|
+
driver.send(message);
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
this.#onEvent({
|
|
273
|
+
type: 'turn-end',
|
|
274
|
+
reason: 'error',
|
|
275
|
+
error: err instanceof Error ? err.message : String(err),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async stop() {
|
|
280
|
+
if (!this.#turn && !this.#queue.length)
|
|
281
|
+
return;
|
|
282
|
+
// what the user typed goes back to the composer; what another session
|
|
283
|
+
// sent is not theirs to edit, and goes with the turn
|
|
284
|
+
const restored = this.#queue
|
|
285
|
+
.filter((m) => !m.from)
|
|
286
|
+
.map((m) => m.text)
|
|
287
|
+
.join('\n\n');
|
|
288
|
+
const dropped = new Set(this.#queue.map((m) => m.id));
|
|
289
|
+
this.#queue.length = 0;
|
|
290
|
+
this.#turn?.abort();
|
|
291
|
+
for (const [id, resolve] of this.#approvals) {
|
|
292
|
+
this.#approvals.delete(id);
|
|
293
|
+
resolve({
|
|
294
|
+
behavior: 'deny',
|
|
295
|
+
message: 'Stopped by the user.',
|
|
296
|
+
interrupt: true,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
for (const [id, resolve] of this.#questions) {
|
|
300
|
+
this.#questions.delete(id);
|
|
301
|
+
resolve(null);
|
|
302
|
+
}
|
|
303
|
+
this.#wake();
|
|
304
|
+
if (dropped.size) {
|
|
305
|
+
this.#set({
|
|
306
|
+
messages: this.#snapshot.messages.filter((m) => !dropped.has(m.id)),
|
|
307
|
+
restored: restored || undefined,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const driver = this.#driver;
|
|
311
|
+
this.#dispatch({ type: 'stopped' });
|
|
312
|
+
this.#turn = null;
|
|
313
|
+
this.#decisions.clear();
|
|
314
|
+
this.#set({ status: 'idle' });
|
|
315
|
+
try {
|
|
316
|
+
await driver?.interrupt();
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// the harness is already gone; the transcript says stopped either way
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
takeRestored() {
|
|
323
|
+
const restored = this.#snapshot.restored;
|
|
324
|
+
if (restored !== undefined)
|
|
325
|
+
this.#set({ restored: undefined });
|
|
326
|
+
return restored;
|
|
327
|
+
}
|
|
328
|
+
pause() {
|
|
329
|
+
if (this.#paused)
|
|
330
|
+
return;
|
|
331
|
+
this.#paused = true;
|
|
332
|
+
this.#set({
|
|
333
|
+
paused: true,
|
|
334
|
+
...(this.#turn ? { status: 'paused' } : {}),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
resume() {
|
|
338
|
+
if (!this.#paused)
|
|
339
|
+
return;
|
|
340
|
+
this.#paused = false;
|
|
341
|
+
this.#set({
|
|
342
|
+
paused: false,
|
|
343
|
+
...(this.#snapshot.status === 'paused'
|
|
344
|
+
? {
|
|
345
|
+
status: this.#approvals.size || this.#questions.size
|
|
346
|
+
? 'waiting'
|
|
347
|
+
: 'running',
|
|
348
|
+
}
|
|
349
|
+
: {}),
|
|
350
|
+
});
|
|
351
|
+
this.#wake();
|
|
352
|
+
}
|
|
353
|
+
#wake() {
|
|
354
|
+
const resumers = [...this.#resumers];
|
|
355
|
+
this.#resumers.clear();
|
|
356
|
+
for (const r of resumers)
|
|
357
|
+
r();
|
|
358
|
+
}
|
|
359
|
+
#unpaused(signal) {
|
|
360
|
+
if (!this.#paused || signal?.aborted || !this.#turn)
|
|
361
|
+
return Promise.resolve();
|
|
362
|
+
return new Promise((resolve) => {
|
|
363
|
+
const done = () => {
|
|
364
|
+
this.#resumers.delete(done);
|
|
365
|
+
signal?.removeEventListener('abort', done);
|
|
366
|
+
resolve();
|
|
367
|
+
};
|
|
368
|
+
this.#resumers.add(done);
|
|
369
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
respond(toolId, choice, note) {
|
|
373
|
+
const resolve = this.#approvals.get(toolId);
|
|
374
|
+
if (!resolve)
|
|
375
|
+
return;
|
|
376
|
+
this.#approvals.delete(toolId);
|
|
377
|
+
if (choice === 'deny') {
|
|
378
|
+
resolve({
|
|
379
|
+
behavior: 'deny',
|
|
380
|
+
message: note?.trim()
|
|
381
|
+
? `Denied by the user: ${note.trim()}`
|
|
382
|
+
: 'Denied by the user.',
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (choice === 'allow-session') {
|
|
387
|
+
const part = findLivePart(this.#snapshot.messages, toolId);
|
|
388
|
+
if (part?.type === 'tool')
|
|
389
|
+
this.#sessionAllowed.add(part.label);
|
|
390
|
+
}
|
|
391
|
+
resolve({ behavior: 'allow' });
|
|
392
|
+
}
|
|
393
|
+
answer(questionId, answers) {
|
|
394
|
+
const resolve = this.#questions.get(questionId);
|
|
395
|
+
if (!resolve)
|
|
396
|
+
return;
|
|
397
|
+
this.#questions.delete(questionId);
|
|
398
|
+
resolve(answers);
|
|
399
|
+
}
|
|
400
|
+
async setModel(id) {
|
|
401
|
+
const driver = this.#driver ?? (await this.#start());
|
|
402
|
+
if (!driver?.setModel)
|
|
403
|
+
return;
|
|
404
|
+
await driver.setModel(id);
|
|
405
|
+
this.#set({ info: { ...this.#snapshot.info, model: id } });
|
|
406
|
+
}
|
|
407
|
+
async setMode(id) {
|
|
408
|
+
const driver = this.#driver ?? (await this.#start());
|
|
409
|
+
if (!driver?.setMode)
|
|
410
|
+
return;
|
|
411
|
+
await driver.setMode(id);
|
|
412
|
+
this.#set({ info: { ...this.#snapshot.info, mode: id } });
|
|
413
|
+
}
|
|
414
|
+
notify(level, text) {
|
|
415
|
+
// a notice goes in the turn under way, or in a message of its own
|
|
416
|
+
if (!this.#turn) {
|
|
417
|
+
this.#dispatch({
|
|
418
|
+
type: 'user',
|
|
419
|
+
message: {
|
|
420
|
+
id: `n${++this.#ids}`,
|
|
421
|
+
role: 'assistant',
|
|
422
|
+
parts: [{ type: 'notice', id: `n${this.#ids}:0`, level, text }],
|
|
423
|
+
createdAt: Date.now(),
|
|
424
|
+
done: true,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.#dispatch({ type: 'notice', level, text });
|
|
430
|
+
}
|
|
431
|
+
toMarkdown() {
|
|
432
|
+
return transcriptMarkdown(this.#snapshot.messages);
|
|
433
|
+
}
|
|
434
|
+
async close() {
|
|
435
|
+
if (this.#snapshot.status === 'closed')
|
|
436
|
+
return;
|
|
437
|
+
await this.stop();
|
|
438
|
+
const driver = this.#driver;
|
|
439
|
+
this.#driver = null;
|
|
440
|
+
this.#set({ status: 'closed' });
|
|
441
|
+
await driver?.close();
|
|
442
|
+
}
|
|
443
|
+
// --- the harness's questions -------------------------------------------------
|
|
444
|
+
async #gate(call, remember) {
|
|
445
|
+
if (this.#paused && this.#turn)
|
|
446
|
+
return { behavior: 'ask' };
|
|
447
|
+
const known = this.#decisions.get(call.id);
|
|
448
|
+
if (known)
|
|
449
|
+
return known;
|
|
450
|
+
if (this.#sessionAllowed.has(call.label))
|
|
451
|
+
return { behavior: 'allow' };
|
|
452
|
+
let decision;
|
|
453
|
+
let fromApp;
|
|
454
|
+
try {
|
|
455
|
+
fromApp = await this.#options.onToolCall?.(call);
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
fromApp = {
|
|
459
|
+
deny: `The app's check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (fromApp === undefined)
|
|
463
|
+
decision = policyDecision(call, this.#config);
|
|
464
|
+
else if (fromApp === 'allow')
|
|
465
|
+
decision = { behavior: 'allow' };
|
|
466
|
+
else if (fromApp === 'allow-session') {
|
|
467
|
+
this.#sessionAllowed.add(call.label);
|
|
468
|
+
decision = { behavior: 'allow' };
|
|
469
|
+
}
|
|
470
|
+
else if (fromApp === 'deny') {
|
|
471
|
+
decision = { behavior: 'deny', message: 'Denied by the app.' };
|
|
472
|
+
}
|
|
473
|
+
else if (fromApp === 'ask')
|
|
474
|
+
decision = { behavior: 'ask' };
|
|
475
|
+
else if ('deny' in fromApp)
|
|
476
|
+
decision = { behavior: 'deny', message: fromApp.deny };
|
|
477
|
+
else
|
|
478
|
+
decision = { behavior: 'allow', input: fromApp.input };
|
|
479
|
+
if (remember)
|
|
480
|
+
this.#decisions.set(call.id, decision);
|
|
481
|
+
if (decision.behavior === 'deny') {
|
|
482
|
+
this.#dispatch({ type: 'approved', id: call.id, allowed: false });
|
|
483
|
+
}
|
|
484
|
+
return decision;
|
|
485
|
+
}
|
|
486
|
+
async #approve(call, signal) {
|
|
487
|
+
await this.#unpaused(signal);
|
|
488
|
+
if (signal?.aborted || !this.#turn) {
|
|
489
|
+
return {
|
|
490
|
+
behavior: 'deny',
|
|
491
|
+
message: 'Stopped by the user.',
|
|
492
|
+
interrupt: true,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const decision = await this.#gate(call, true);
|
|
496
|
+
if (decision.behavior === 'allow') {
|
|
497
|
+
return { behavior: 'allow', input: decision.input };
|
|
498
|
+
}
|
|
499
|
+
if (decision.behavior === 'deny') {
|
|
500
|
+
return { behavior: 'deny', message: decision.message };
|
|
501
|
+
}
|
|
502
|
+
if (!findLivePart(this.#snapshot.messages, call.id)) {
|
|
503
|
+
this.#dispatch({
|
|
504
|
+
type: 'tool-start',
|
|
505
|
+
id: call.id,
|
|
506
|
+
name: call.name,
|
|
507
|
+
label: call.label,
|
|
508
|
+
kind: call.kind,
|
|
509
|
+
title: call.title,
|
|
510
|
+
input: call.input,
|
|
511
|
+
app: call.app,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
this.#dispatch({
|
|
515
|
+
type: 'approval',
|
|
516
|
+
id: call.id,
|
|
517
|
+
options: APPROVAL_OPTIONS,
|
|
518
|
+
});
|
|
519
|
+
this.#set({ status: 'waiting' });
|
|
520
|
+
const result = await new Promise((resolve) => {
|
|
521
|
+
this.#approvals.set(call.id, resolve);
|
|
522
|
+
signal?.addEventListener('abort', () => {
|
|
523
|
+
if (!this.#approvals.delete(call.id))
|
|
524
|
+
return;
|
|
525
|
+
resolve({ behavior: 'deny', message: 'Stopped.', interrupt: true });
|
|
526
|
+
}, { once: true });
|
|
527
|
+
});
|
|
528
|
+
this.#decisions.set(call.id, result.behavior === 'allow'
|
|
529
|
+
? { behavior: 'allow', input: result.input }
|
|
530
|
+
: { behavior: 'deny', message: result.message });
|
|
531
|
+
this.#dispatch({
|
|
532
|
+
type: 'approved',
|
|
533
|
+
id: call.id,
|
|
534
|
+
allowed: result.behavior === 'allow',
|
|
535
|
+
});
|
|
536
|
+
this.#settleWaiting();
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
async #ask(id, questions, signal) {
|
|
540
|
+
this.#dispatch({ type: 'question', id, questions });
|
|
541
|
+
this.#set({ status: 'waiting' });
|
|
542
|
+
const answers = await new Promise((resolve) => {
|
|
543
|
+
this.#questions.set(id, resolve);
|
|
544
|
+
signal?.addEventListener('abort', () => {
|
|
545
|
+
if (this.#questions.delete(id))
|
|
546
|
+
resolve(null);
|
|
547
|
+
}, { once: true });
|
|
548
|
+
});
|
|
549
|
+
this.#dispatch({ type: 'answered', id, answers });
|
|
550
|
+
this.#settleWaiting();
|
|
551
|
+
return answers;
|
|
552
|
+
}
|
|
553
|
+
#settleWaiting() {
|
|
554
|
+
if (this.#approvals.size || this.#questions.size)
|
|
555
|
+
return;
|
|
556
|
+
if (this.#snapshot.status === 'waiting') {
|
|
557
|
+
this.#set({
|
|
558
|
+
status: this.#turn ? (this.#paused ? 'paused' : 'running') : 'idle',
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async #runTool(name, input, ctx) {
|
|
563
|
+
const tool = this.#config.tools.get(name);
|
|
564
|
+
if (!tool)
|
|
565
|
+
return errorResult(new Error(`No tool named ${name}.`));
|
|
566
|
+
const checked = await validateInput(tool.parameters, input);
|
|
567
|
+
if (!checked.ok)
|
|
568
|
+
return errorResult(new Error(checked.message));
|
|
569
|
+
const callId = ctx.callId ?? `${name}:${++this.#ids}`;
|
|
570
|
+
const signal = ctx.signal ?? this.#turn?.signal ?? new AbortController().signal;
|
|
571
|
+
if (ctx.callId) {
|
|
572
|
+
this.#dispatch({ type: 'tool-update', id: callId, state: 'running' });
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const returned = await tool.run(checked.value, {
|
|
576
|
+
signal,
|
|
577
|
+
callId,
|
|
578
|
+
progress: (text) => {
|
|
579
|
+
if (ctx.callId) {
|
|
580
|
+
this.#dispatch({ type: 'tool-update', id: callId, progress: text });
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
const { result, output } = normalizeResult(returned);
|
|
585
|
+
if (ctx.callId) {
|
|
586
|
+
this.#dispatch({ type: 'tool-update', id: callId, output });
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
if (ctx.callId) {
|
|
592
|
+
this.#dispatch({
|
|
593
|
+
type: 'tool-update',
|
|
594
|
+
id: callId,
|
|
595
|
+
output: [],
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return errorResult(err);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function resolveConfig(options) {
|
|
603
|
+
const tools = new Map(Object.entries(options.tools ?? {}));
|
|
604
|
+
const removed = [];
|
|
605
|
+
const asking = [];
|
|
606
|
+
const replaced = {};
|
|
607
|
+
for (const [key, setting] of Object.entries(options.coreTools ?? {})) {
|
|
608
|
+
if (setting === undefined || setting === true)
|
|
609
|
+
continue;
|
|
610
|
+
if (setting === false)
|
|
611
|
+
removed.push(key);
|
|
612
|
+
else if (setting === 'ask')
|
|
613
|
+
asking.push(key);
|
|
614
|
+
else {
|
|
615
|
+
tools.set(key, setting);
|
|
616
|
+
replaced[key] = key;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const instructions = Array.isArray(options.instructions)
|
|
620
|
+
? options.instructions.filter(Boolean).join('\n\n')
|
|
621
|
+
: (options.instructions ?? '');
|
|
622
|
+
return {
|
|
623
|
+
cwd: options.cwd ?? process.cwd(),
|
|
624
|
+
folders: options.folders ?? [],
|
|
625
|
+
instructions,
|
|
626
|
+
...(options.systemPrompt !== undefined
|
|
627
|
+
? { systemPrompt: options.systemPrompt }
|
|
628
|
+
: {}),
|
|
629
|
+
removed,
|
|
630
|
+
asking,
|
|
631
|
+
replaced,
|
|
632
|
+
tools,
|
|
633
|
+
permissions: options.permissions ?? 'ask',
|
|
634
|
+
...(options.model ? { model: options.model } : {}),
|
|
635
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
636
|
+
...(options.resume ? { resume: options.resume } : {}),
|
|
637
|
+
settings: options.settings ?? 'cli',
|
|
638
|
+
driverOptions: options.driverOptions ?? {},
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async function loadDriver(agent) {
|
|
642
|
+
if (agent && typeof agent === 'object')
|
|
643
|
+
return agent;
|
|
644
|
+
// A dynamic import, so a bundle that never names Claude carries none of it.
|
|
645
|
+
const { claudeDriver } = await import('../drivers/claude/index.js');
|
|
646
|
+
return claudeDriver();
|
|
647
|
+
}
|
|
648
|
+
//# sourceMappingURL=session.js.map
|