@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.
Files changed (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -2
  3. package/dist/core/driver.d.ts +183 -0
  4. package/dist/core/driver.d.ts.map +1 -0
  5. package/dist/core/driver.js +12 -0
  6. package/dist/core/driver.js.map +1 -0
  7. package/dist/core/index.d.ts +8 -0
  8. package/dist/core/index.d.ts.map +1 -0
  9. package/dist/core/index.js +11 -0
  10. package/dist/core/index.js.map +1 -0
  11. package/dist/core/markdown.d.ts +7 -0
  12. package/dist/core/markdown.d.ts.map +1 -0
  13. package/dist/core/markdown.js +53 -0
  14. package/dist/core/markdown.js.map +1 -0
  15. package/dist/core/policy.d.ts +18 -0
  16. package/dist/core/policy.d.ts.map +1 -0
  17. package/dist/core/policy.js +67 -0
  18. package/dist/core/policy.js.map +1 -0
  19. package/dist/core/reducer.d.ts +61 -0
  20. package/dist/core/reducer.d.ts.map +1 -0
  21. package/dist/core/reducer.js +391 -0
  22. package/dist/core/reducer.js.map +1 -0
  23. package/dist/core/session.d.ts +105 -0
  24. package/dist/core/session.d.ts.map +1 -0
  25. package/dist/core/session.js +648 -0
  26. package/dist/core/session.js.map +1 -0
  27. package/dist/core/tools.d.ts +120 -0
  28. package/dist/core/tools.d.ts.map +1 -0
  29. package/dist/core/tools.js +120 -0
  30. package/dist/core/tools.js.map +1 -0
  31. package/dist/core/types.d.ts +295 -0
  32. package/dist/core/types.d.ts.map +1 -0
  33. package/dist/core/types.js +21 -0
  34. package/dist/core/types.js.map +1 -0
  35. package/dist/drivers/claude/index.d.ts +20 -0
  36. package/dist/drivers/claude/index.d.ts.map +1 -0
  37. package/dist/drivers/claude/index.js +727 -0
  38. package/dist/drivers/claude/index.js.map +1 -0
  39. package/dist/drivers/claude/input.d.ts +7 -0
  40. package/dist/drivers/claude/input.d.ts.map +1 -0
  41. package/dist/drivers/claude/input.js +46 -0
  42. package/dist/drivers/claude/input.js.map +1 -0
  43. package/dist/drivers/claude/peers.d.ts +16 -0
  44. package/dist/drivers/claude/peers.d.ts.map +1 -0
  45. package/dist/drivers/claude/peers.js +137 -0
  46. package/dist/drivers/claude/peers.js.map +1 -0
  47. package/dist/drivers/claude/schema.d.ts +5 -0
  48. package/dist/drivers/claude/schema.d.ts.map +1 -0
  49. package/dist/drivers/claude/schema.js +77 -0
  50. package/dist/drivers/claude/schema.js.map +1 -0
  51. package/dist/drivers/claude/sdk.d.ts +86 -0
  52. package/dist/drivers/claude/sdk.d.ts.map +1 -0
  53. package/dist/drivers/claude/sdk.js +47 -0
  54. package/dist/drivers/claude/sdk.js.map +1 -0
  55. package/dist/drivers/claude/tools.d.ts +18 -0
  56. package/dist/drivers/claude/tools.d.ts.map +1 -0
  57. package/dist/drivers/claude/tools.js +119 -0
  58. package/dist/drivers/claude/tools.js.map +1 -0
  59. package/dist/index.d.ts +8 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +12 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/testing/fake.d.ts +63 -0
  64. package/dist/testing/fake.d.ts.map +1 -0
  65. package/dist/testing/fake.js +270 -0
  66. package/dist/testing/fake.js.map +1 -0
  67. package/dist/testing/index.d.ts +2 -0
  68. package/dist/testing/index.d.ts.map +1 -0
  69. package/dist/testing/index.js +4 -0
  70. package/dist/testing/index.js.map +1 -0
  71. package/dist/view/AgentChat.d.ts +53 -0
  72. package/dist/view/AgentChat.d.ts.map +1 -0
  73. package/dist/view/AgentChat.js +87 -0
  74. package/dist/view/AgentChat.js.map +1 -0
  75. package/dist/view/CodeBlock.d.ts +9 -0
  76. package/dist/view/CodeBlock.d.ts.map +1 -0
  77. package/dist/view/CodeBlock.js +60 -0
  78. package/dist/view/CodeBlock.js.map +1 -0
  79. package/dist/view/Composer.d.ts +24 -0
  80. package/dist/view/Composer.d.ts.map +1 -0
  81. package/dist/view/Composer.js +144 -0
  82. package/dist/view/Composer.js.map +1 -0
  83. package/dist/view/Header.d.ts +3 -0
  84. package/dist/view/Header.d.ts.map +1 -0
  85. package/dist/view/Header.js +30 -0
  86. package/dist/view/Header.js.map +1 -0
  87. package/dist/view/MessageList.d.ts +9 -0
  88. package/dist/view/MessageList.d.ts.map +1 -0
  89. package/dist/view/MessageList.js +111 -0
  90. package/dist/view/MessageList.js.map +1 -0
  91. package/dist/view/QuestionCard.d.ts +6 -0
  92. package/dist/view/QuestionCard.d.ts.map +1 -0
  93. package/dist/view/QuestionCard.js +39 -0
  94. package/dist/view/QuestionCard.js.map +1 -0
  95. package/dist/view/TextBlock.d.ts +9 -0
  96. package/dist/view/TextBlock.d.ts.map +1 -0
  97. package/dist/view/TextBlock.js +22 -0
  98. package/dist/view/TextBlock.js.map +1 -0
  99. package/dist/view/ToolCard.d.ts +7 -0
  100. package/dist/view/ToolCard.d.ts.map +1 -0
  101. package/dist/view/ToolCard.js +155 -0
  102. package/dist/view/ToolCard.js.map +1 -0
  103. package/dist/view/Transcript.d.ts +5 -0
  104. package/dist/view/Transcript.d.ts.map +1 -0
  105. package/dist/view/Transcript.js +47 -0
  106. package/dist/view/Transcript.js.map +1 -0
  107. package/dist/view/context.d.ts +21 -0
  108. package/dist/view/context.d.ts.map +1 -0
  109. package/dist/view/context.js +37 -0
  110. package/dist/view/context.js.map +1 -0
  111. package/dist/view/fences.d.ts +13 -0
  112. package/dist/view/fences.d.ts.map +1 -0
  113. package/dist/view/fences.js +59 -0
  114. package/dist/view/fences.js.map +1 -0
  115. package/dist/view/files.d.ts +11 -0
  116. package/dist/view/files.d.ts.map +1 -0
  117. package/dist/view/files.js +109 -0
  118. package/dist/view/files.js.map +1 -0
  119. package/dist/view/look.d.ts +18 -0
  120. package/dist/view/look.d.ts.map +1 -0
  121. package/dist/view/look.js +37 -0
  122. package/dist/view/look.js.map +1 -0
  123. package/dist/view/types.d.ts +50 -0
  124. package/dist/view/types.d.ts.map +1 -0
  125. package/dist/view/types.js +14 -0
  126. package/dist/view/types.js.map +1 -0
  127. package/dist/view/use-agent.d.ts +11 -0
  128. package/dist/view/use-agent.d.ts.map +1 -0
  129. package/dist/view/use-agent.js +33 -0
  130. package/dist/view/use-agent.js.map +1 -0
  131. package/package.json +92 -4
  132. package/src/core/driver.ts +191 -0
  133. package/src/core/index.ts +54 -0
  134. package/src/core/markdown.ts +56 -0
  135. package/src/core/policy.ts +77 -0
  136. package/src/core/reducer.ts +490 -0
  137. package/src/core/session.ts +824 -0
  138. package/src/core/tools.ts +233 -0
  139. package/src/core/types.ts +321 -0
  140. package/src/drivers/claude/index.ts +825 -0
  141. package/src/drivers/claude/input.ts +47 -0
  142. package/src/drivers/claude/peers.ts +153 -0
  143. package/src/drivers/claude/schema.ts +99 -0
  144. package/src/drivers/claude/sdk.ts +138 -0
  145. package/src/drivers/claude/tools.ts +141 -0
  146. package/src/index.ts +51 -0
  147. package/src/testing/fake.ts +374 -0
  148. package/src/testing/index.ts +11 -0
  149. package/src/view/AgentChat.tsx +204 -0
  150. package/src/view/CodeBlock.tsx +108 -0
  151. package/src/view/Composer.tsx +230 -0
  152. package/src/view/Header.tsx +73 -0
  153. package/src/view/MessageList.tsx +290 -0
  154. package/src/view/QuestionCard.tsx +107 -0
  155. package/src/view/TextBlock.tsx +58 -0
  156. package/src/view/ToolCard.tsx +283 -0
  157. package/src/view/Transcript.tsx +81 -0
  158. package/src/view/context.ts +70 -0
  159. package/src/view/dom-globals.d.ts +10 -0
  160. package/src/view/fences.ts +73 -0
  161. package/src/view/files.ts +111 -0
  162. package/src/view/look.ts +53 -0
  163. package/src/view/types.ts +71 -0
  164. 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