@acprotocol/server 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.
@@ -0,0 +1,713 @@
1
+ // @acprotocol/server — Apache 2.0
2
+
3
+ // src/prompt.ts
4
+ function buildSystemPrompt(manifest) {
5
+ const parts = [];
6
+ if (manifest.persona?.name) {
7
+ let identity = `You are ${manifest.persona.name}`;
8
+ if (manifest.persona.role) identity += `, ${manifest.persona.role}`;
9
+ identity += ".";
10
+ parts.push(identity);
11
+ } else {
12
+ parts.push(
13
+ "You are an AI assistant embedded in a software application."
14
+ );
15
+ }
16
+ if (manifest.persona?.instructions) {
17
+ parts.push(manifest.persona.instructions);
18
+ }
19
+ if (manifest.user) {
20
+ const lines = ["## User"];
21
+ if (manifest.user.name) lines.push(`- Name: ${manifest.user.name}`);
22
+ if (manifest.user.org) lines.push(`- Organization: ${manifest.user.org}`);
23
+ if (manifest.user.role) lines.push(`- Role: ${manifest.user.role}`);
24
+ if (lines.length > 1) parts.push(lines.join("\n"));
25
+ }
26
+ if (manifest.context && Object.keys(manifest.context).length > 0) {
27
+ parts.push(
28
+ "## Application Context\n" + JSON.stringify(manifest.context, null, 2)
29
+ );
30
+ }
31
+ const screenLines = [
32
+ "## Application Screens",
33
+ "You can control the application UI using the available tools. Here are the screens and their fields:",
34
+ ""
35
+ ];
36
+ for (const [id, screen] of Object.entries(manifest.screens)) {
37
+ screenLines.push(`### Screen: ${id} (${screen.label})`);
38
+ if (screen.route) screenLines.push(`Route: ${screen.route}`);
39
+ if (screen.fields?.length) {
40
+ screenLines.push("Fields:");
41
+ for (const f of screen.fields) {
42
+ const req = f.required ? " [REQUIRED]" : "";
43
+ screenLines.push(` - \`${f.id}\` (${f.type}): ${f.label}${req}`);
44
+ if (f.options?.length) {
45
+ const opts = f.options.map((o) => `${o.value}=${o.label}`).join(", ");
46
+ screenLines.push(` Options: ${opts}`);
47
+ }
48
+ }
49
+ }
50
+ if (screen.actions?.length) {
51
+ screenLines.push("Actions:");
52
+ for (const act of screen.actions) {
53
+ let flags = "";
54
+ if (act.requiresConfirmation) flags += " [REQUIRES_CONFIRMATION]";
55
+ if (act.destructive) flags += " [DESTRUCTIVE]";
56
+ screenLines.push(` - \`${act.id}\`: ${act.label}${flags}`);
57
+ }
58
+ }
59
+ if (screen.modals?.length) {
60
+ screenLines.push("Modals:");
61
+ for (const md of screen.modals) {
62
+ screenLines.push(` - \`${md.id}\`: ${md.label}`);
63
+ }
64
+ }
65
+ screenLines.push("");
66
+ }
67
+ parts.push(screenLines.join("\n"));
68
+ parts.push(
69
+ [
70
+ "## Rules",
71
+ '- Use `fill_field` with animate="typewriter" so the user can see values being entered.',
72
+ "- ALWAYS call `ask_confirm` before clicking any action marked [REQUIRES_CONFIRMATION].",
73
+ "- If the user's request is missing essential information, ask briefly and generically \u2014 do NOT list specific field names.",
74
+ "- Do NOT narrate individual fields being filled. Just confirm the action briefly when done.",
75
+ "- If a command fails (you'll see the error in the next message), explain and try to fix it.",
76
+ "- Respond in the same language the user speaks.",
77
+ "- Be concise. Keep responses short \u2014 prefer brief confirmations.",
78
+ "- Navigate to the correct screen before filling fields.",
79
+ "- When filling multiple fields on the same screen, combine ALL fill_field calls in a single response.",
80
+ "- Do NOT fill one field at a time \u2014 batch them together."
81
+ ].join("\n")
82
+ );
83
+ return parts.join("\n\n");
84
+ }
85
+
86
+ // src/session.ts
87
+ var MAX_HISTORY = 40;
88
+ var Session = class {
89
+ /** Unique session identifier. */
90
+ id;
91
+ /** The current ACP manifest, or `null` if not yet received. */
92
+ manifest = null;
93
+ /** The current screen ID. */
94
+ currentScreen = "";
95
+ history = [];
96
+ _seq = 0;
97
+ constructor(id) {
98
+ this.id = id;
99
+ }
100
+ /**
101
+ * Sets the manifest and rebuilds the system prompt.
102
+ *
103
+ * If the manifest includes a `currentScreen`, it is applied.
104
+ * The system prompt is built from the manifest and added or replaced
105
+ * in the message history.
106
+ *
107
+ * @param manifest - The ACP manifest message describing the application UI.
108
+ */
109
+ setManifest(manifest) {
110
+ this.manifest = manifest;
111
+ if (manifest.currentScreen) {
112
+ this.currentScreen = manifest.currentScreen;
113
+ }
114
+ this.updateSystemPrompt(buildSystemPrompt(manifest));
115
+ }
116
+ /**
117
+ * Updates the current screen ID.
118
+ * @param screen - The screen ID to switch to.
119
+ */
120
+ setScreen(screen) {
121
+ this.currentScreen = screen;
122
+ }
123
+ /**
124
+ * Adds a message to the conversation history.
125
+ *
126
+ * If the history exceeds {@link MAX_HISTORY} (40), the oldest messages
127
+ * after the system prompt are trimmed to maintain a sliding window.
128
+ *
129
+ * @param msg - An OpenAI-compatible chat message.
130
+ */
131
+ addMessage(msg) {
132
+ this.history.push(msg);
133
+ if (this.history.length > MAX_HISTORY) {
134
+ const system = this.history[0];
135
+ const start = this.history.length - MAX_HISTORY + 1;
136
+ this.history = [system, ...this.history.slice(start)];
137
+ }
138
+ }
139
+ /**
140
+ * Returns a shallow copy of the message history.
141
+ * Safe to iterate without affecting the session's internal state.
142
+ */
143
+ getHistory() {
144
+ return [...this.history];
145
+ }
146
+ /**
147
+ * Replaces the first system message in history, or prepends one.
148
+ * @param prompt - The new system prompt content.
149
+ */
150
+ updateSystemPrompt(prompt) {
151
+ const idx = this.history.findIndex((m) => m.role === "system");
152
+ if (idx >= 0) {
153
+ this.history[idx] = { role: "system", content: prompt };
154
+ } else {
155
+ this.history.unshift({ role: "system", content: prompt });
156
+ }
157
+ }
158
+ /**
159
+ * Returns and increments the sequence counter.
160
+ *
161
+ * Each command sent to the client carries a `seq` number which the client
162
+ * echoes back in the `result` or `confirm` message. This counter is
163
+ * monotonically increasing per session.
164
+ */
165
+ nextSeq() {
166
+ return this._seq++;
167
+ }
168
+ };
169
+
170
+ // src/tools.ts
171
+ function manifestToTools(manifest) {
172
+ const screenIDs = [];
173
+ const screenLabels = [];
174
+ for (const [id, s] of Object.entries(manifest.screens)) {
175
+ screenIDs.push(id);
176
+ screenLabels.push(`${id} (${s.label})`);
177
+ }
178
+ const allFieldIDs = collectFieldIDs(manifest);
179
+ const allActionIDs = collectActionIDs(manifest);
180
+ const allModalIDs = collectModalIDs(manifest);
181
+ const tools = [
182
+ navigateTool(screenIDs, screenLabels),
183
+ fillFieldTool(allFieldIDs),
184
+ clearFieldTool(allFieldIDs),
185
+ clickActionTool(allActionIDs),
186
+ highlightTool(allFieldIDs),
187
+ focusTool(allFieldIDs),
188
+ askConfirmTool(),
189
+ showToastTool()
190
+ ];
191
+ if (allModalIDs.length > 0) {
192
+ tools.push(openModalTool(allModalIDs), closeModalTool());
193
+ }
194
+ return tools;
195
+ }
196
+ function toolCallToUIAction(name, argsJSON) {
197
+ let args;
198
+ try {
199
+ args = JSON.parse(argsJSON);
200
+ } catch {
201
+ args = {};
202
+ }
203
+ switch (name) {
204
+ case "navigate":
205
+ return { do: "navigate", screen: str(args.screen) };
206
+ case "fill_field":
207
+ return {
208
+ do: "fill",
209
+ field: str(args.field),
210
+ value: args.value,
211
+ animate: str(args.animate) || "typewriter",
212
+ speed: num(args.speed) || void 0
213
+ };
214
+ case "clear_field":
215
+ return { do: "clear", field: str(args.field) };
216
+ case "click_action":
217
+ return { do: "click", action: str(args.action) };
218
+ case "highlight":
219
+ return {
220
+ do: "highlight",
221
+ field: str(args.field),
222
+ duration: num(args.duration) || void 0
223
+ };
224
+ case "focus":
225
+ return { do: "focus", field: str(args.field) };
226
+ case "open_modal":
227
+ return {
228
+ do: "open_modal",
229
+ modal: str(args.modal),
230
+ query: str(args.query) || void 0
231
+ };
232
+ case "close_modal":
233
+ return { do: "close_modal" };
234
+ case "ask_confirm":
235
+ return { do: "ask_confirm", message: str(args.message) };
236
+ case "show_toast":
237
+ return {
238
+ do: "show_toast",
239
+ message: str(args.message),
240
+ level: str(args.level) || void 0,
241
+ duration: num(args.duration) || void 0
242
+ };
243
+ default:
244
+ throw new Error(`Unknown tool: ${name}`);
245
+ }
246
+ }
247
+ function makeTool(name, description, parameters) {
248
+ return {
249
+ type: "function",
250
+ function: { name, description, parameters }
251
+ };
252
+ }
253
+ function navigateTool(screenIDs, screenLabels) {
254
+ return makeTool(
255
+ "navigate",
256
+ `Navigate to a screen. Available: ${screenLabels.join(", ")}`,
257
+ {
258
+ type: "object",
259
+ properties: {
260
+ screen: { type: "string", enum: screenIDs, description: "Screen ID to navigate to" }
261
+ },
262
+ required: ["screen"]
263
+ }
264
+ );
265
+ }
266
+ function fillFieldTool(fieldIDs) {
267
+ return makeTool(
268
+ "fill_field",
269
+ `Fill a form field with a value. The field animates as the value is typed. Available fields: ${fieldIDs.join(", ")}`,
270
+ {
271
+ type: "object",
272
+ properties: {
273
+ field: { type: "string", description: "Field ID to fill" },
274
+ value: { description: "Value to set (string, number, or boolean depending on field type)" },
275
+ animate: {
276
+ type: "string",
277
+ enum: ["typewriter", "count_up", "fade_in", "none"],
278
+ default: "typewriter",
279
+ description: "Animation style for filling"
280
+ }
281
+ },
282
+ required: ["field", "value"]
283
+ }
284
+ );
285
+ }
286
+ function clearFieldTool(fieldIDs) {
287
+ return makeTool("clear_field", "Clear a form field value", {
288
+ type: "object",
289
+ properties: {
290
+ field: { type: "string", description: "Field ID to clear" }
291
+ },
292
+ required: ["field"]
293
+ });
294
+ }
295
+ function clickActionTool(actionIDs) {
296
+ return makeTool(
297
+ "click_action",
298
+ `Click a button or trigger an action. Available: ${actionIDs.join(", ")}. IMPORTANT: if the action has requiresConfirmation=true, you MUST call ask_confirm first and wait for the user's response before clicking.`,
299
+ {
300
+ type: "object",
301
+ properties: {
302
+ action: { type: "string", description: "Action ID to click" }
303
+ },
304
+ required: ["action"]
305
+ }
306
+ );
307
+ }
308
+ function highlightTool(fieldIDs) {
309
+ return makeTool("highlight", "Temporarily highlight a field to draw the user's attention", {
310
+ type: "object",
311
+ properties: {
312
+ field: { type: "string", description: "Field ID to highlight" },
313
+ duration: { type: "integer", default: 2e3, description: "Highlight duration in milliseconds" }
314
+ },
315
+ required: ["field"]
316
+ });
317
+ }
318
+ function focusTool(fieldIDs) {
319
+ return makeTool("focus", "Set keyboard focus on a field", {
320
+ type: "object",
321
+ properties: {
322
+ field: { type: "string", description: "Field ID to focus" }
323
+ },
324
+ required: ["field"]
325
+ });
326
+ }
327
+ function openModalTool(modalIDs) {
328
+ return makeTool(
329
+ "open_modal",
330
+ `Open a modal/dialog. Available: ${modalIDs.join(", ")}`,
331
+ {
332
+ type: "object",
333
+ properties: {
334
+ modal: { type: "string", description: "Modal ID to open" },
335
+ query: { type: "string", description: "Optional search query to pre-fill in the modal" }
336
+ },
337
+ required: ["modal"]
338
+ }
339
+ );
340
+ }
341
+ function closeModalTool() {
342
+ return makeTool("close_modal", "Close the currently open modal", {
343
+ type: "object",
344
+ properties: {}
345
+ });
346
+ }
347
+ function askConfirmTool() {
348
+ return makeTool(
349
+ "ask_confirm",
350
+ "Ask the user for confirmation before proceeding with a destructive or important action.",
351
+ {
352
+ type: "object",
353
+ properties: {
354
+ message: { type: "string", description: "Confirmation question to ask the user" }
355
+ },
356
+ required: ["message"]
357
+ }
358
+ );
359
+ }
360
+ function showToastTool() {
361
+ return makeTool("show_toast", "Show a temporary notification/toast message in the app", {
362
+ type: "object",
363
+ properties: {
364
+ message: { type: "string", description: "Toast message text" },
365
+ level: { type: "string", enum: ["info", "success", "warning", "error"], default: "info" },
366
+ duration: { type: "integer", default: 3e3 }
367
+ },
368
+ required: ["message"]
369
+ });
370
+ }
371
+ function collectFieldIDs(m) {
372
+ const seen = /* @__PURE__ */ new Set();
373
+ const ids = [];
374
+ for (const s of Object.values(m.screens)) {
375
+ for (const f of s.fields ?? []) {
376
+ if (!seen.has(f.id)) {
377
+ ids.push(f.id);
378
+ seen.add(f.id);
379
+ }
380
+ }
381
+ }
382
+ return ids;
383
+ }
384
+ function collectActionIDs(m) {
385
+ const seen = /* @__PURE__ */ new Set();
386
+ const ids = [];
387
+ for (const s of Object.values(m.screens)) {
388
+ for (const a of s.actions ?? []) {
389
+ if (!seen.has(a.id)) {
390
+ ids.push(a.id);
391
+ seen.add(a.id);
392
+ }
393
+ }
394
+ }
395
+ return ids;
396
+ }
397
+ function collectModalIDs(m) {
398
+ const seen = /* @__PURE__ */ new Set();
399
+ const ids = [];
400
+ for (const s of Object.values(m.screens)) {
401
+ for (const md of s.modals ?? []) {
402
+ if (!seen.has(md.id)) {
403
+ ids.push(md.id);
404
+ seen.add(md.id);
405
+ }
406
+ }
407
+ }
408
+ return ids;
409
+ }
410
+ function str(v) {
411
+ return typeof v === "string" ? v : "";
412
+ }
413
+ function num(v) {
414
+ return typeof v === "number" ? v : 0;
415
+ }
416
+
417
+ // src/agent.ts
418
+ var MAX_ROUNDS = 5;
419
+ async function runAgentLoop(openai, model, session, text, execute, send) {
420
+ session.addMessage({ role: "user", content: text });
421
+ const tools = session.manifest ? manifestToTools(session.manifest) : void 0;
422
+ let lastResponseText = "";
423
+ for (let round = 0; round < MAX_ROUNDS; round++) {
424
+ const history = session.getHistory();
425
+ const stream = await openai.chat.completions.create({
426
+ model,
427
+ messages: history,
428
+ tools: tools?.length ? tools : void 0,
429
+ stream: true
430
+ });
431
+ let contentBuf = "";
432
+ const accToolCalls = [];
433
+ for await (const chunk of stream) {
434
+ if (!chunk.choices?.length) continue;
435
+ const delta = chunk.choices[0].delta;
436
+ if (delta.content) {
437
+ contentBuf += delta.content;
438
+ send({ type: "chat_token", token: delta.content });
439
+ }
440
+ if (delta.tool_calls) {
441
+ for (const tc of delta.tool_calls) {
442
+ const idx = tc.index ?? 0;
443
+ while (accToolCalls.length <= idx) {
444
+ accToolCalls.push({ id: "", type: "", function: { name: "", arguments: "" } });
445
+ }
446
+ if (tc.id) accToolCalls[idx].id = tc.id;
447
+ if (tc.type) accToolCalls[idx].type = tc.type;
448
+ if (tc.function?.name) accToolCalls[idx].function.name = tc.function.name;
449
+ if (tc.function?.arguments) accToolCalls[idx].function.arguments += tc.function.arguments;
450
+ }
451
+ }
452
+ }
453
+ const assistantMsg = {
454
+ role: "assistant",
455
+ content: contentBuf || null
456
+ };
457
+ if (accToolCalls.length > 0) {
458
+ assistantMsg.tool_calls = accToolCalls.map((tc) => ({
459
+ id: tc.id,
460
+ type: "function",
461
+ function: { name: tc.function.name, arguments: tc.function.arguments }
462
+ }));
463
+ }
464
+ session.addMessage(assistantMsg);
465
+ if (accToolCalls.length === 0) {
466
+ if (contentBuf) {
467
+ send({ type: "chat", from: "agent", message: contentBuf, final: true });
468
+ }
469
+ return;
470
+ }
471
+ if (contentBuf) {
472
+ lastResponseText = contentBuf;
473
+ }
474
+ const roundActions = [];
475
+ const mappings = [];
476
+ for (const tc of accToolCalls) {
477
+ try {
478
+ const action = toolCallToUIAction(tc.function.name, tc.function.arguments);
479
+ roundActions.push(action);
480
+ mappings.push({ action, callId: tc.id });
481
+ } catch (err) {
482
+ session.addMessage({
483
+ role: "tool",
484
+ content: JSON.stringify({ error: String(err) }),
485
+ tool_call_id: tc.id
486
+ });
487
+ }
488
+ }
489
+ if (roundActions.length === 0) continue;
490
+ const isConfirmOnly = roundActions.length === 1 && roundActions[0].do === "ask_confirm";
491
+ const seq = session.nextSeq();
492
+ send({ type: "status", status: "executing" });
493
+ let resultMsg;
494
+ try {
495
+ resultMsg = await execute(seq, roundActions);
496
+ } catch (err) {
497
+ for (const m of mappings) {
498
+ session.addMessage({
499
+ role: "tool",
500
+ content: JSON.stringify({ success: false, error: String(err) }),
501
+ tool_call_id: m.callId
502
+ });
503
+ }
504
+ send({ type: "status", status: "thinking" });
505
+ continue;
506
+ }
507
+ send({ type: "status", status: "thinking" });
508
+ const resultsByIndex = /* @__PURE__ */ new Map();
509
+ for (const r of resultMsg.results) {
510
+ resultsByIndex.set(r.index, r);
511
+ }
512
+ for (let i = 0; i < mappings.length; i++) {
513
+ const m = mappings[i];
514
+ const result = { success: true, action: m.action.do };
515
+ const r = resultsByIndex.get(i);
516
+ if (r) {
517
+ result.success = r.success;
518
+ if (!r.success && r.error) result.error = r.error;
519
+ }
520
+ if (m.action.do === "navigate") {
521
+ session.setScreen(m.action.screen);
522
+ result.screen = m.action.screen;
523
+ }
524
+ if (m.action.do === "fill") {
525
+ result.field = m.action.field;
526
+ result.value = m.action.value;
527
+ }
528
+ if (isConfirmOnly && m.action.do === "ask_confirm") {
529
+ const confirmed = resultMsg.results[0]?.success ?? false;
530
+ result.user_response = confirmed ? "Yes" : "No";
531
+ }
532
+ session.addMessage({
533
+ role: "tool",
534
+ content: JSON.stringify(result),
535
+ tool_call_id: m.callId
536
+ });
537
+ }
538
+ }
539
+ if (lastResponseText) {
540
+ send({ type: "chat", from: "agent", message: lastResponseText, final: true });
541
+ } else {
542
+ send({ type: "chat", from: "agent", message: "Done.", final: true });
543
+ }
544
+ }
545
+
546
+ // src/server.ts
547
+ import { randomUUID } from "crypto";
548
+ import { WebSocketServer, WebSocket } from "ws";
549
+ var EXECUTE_TIMEOUT_MS = 3e4;
550
+ function createServer(options) {
551
+ const { openai, model, port } = options;
552
+ let wss = null;
553
+ return {
554
+ async start() {
555
+ wss = new WebSocketServer({ port, path: "/connect" });
556
+ wss.on("connection", (ws, req) => {
557
+ handleConnection(ws, openai, model);
558
+ });
559
+ },
560
+ async stop() {
561
+ if (!wss) return;
562
+ for (const ws of wss.clients) {
563
+ ws.close(1001, "Server shutting down");
564
+ }
565
+ await new Promise((resolve) => wss.close(() => resolve()));
566
+ wss = null;
567
+ }
568
+ };
569
+ }
570
+ function handleConnection(ws, openai, model) {
571
+ const sessionId = randomUUID();
572
+ const session = new Session(sessionId);
573
+ const pendingResults = /* @__PURE__ */ new Map();
574
+ const pendingConfirms = /* @__PURE__ */ new Map();
575
+ let processing = false;
576
+ const send = (msg) => {
577
+ if (ws.readyState === WebSocket.OPEN) {
578
+ ws.send(JSON.stringify(msg));
579
+ }
580
+ };
581
+ send({
582
+ type: "config",
583
+ sessionId,
584
+ features: { chat: true },
585
+ providers: [{ id: "default", name: "Default", model }],
586
+ current_provider: "default"
587
+ });
588
+ const pingInterval = setInterval(() => {
589
+ if (ws.readyState === WebSocket.OPEN) ws.ping();
590
+ }, 3e4);
591
+ ws.on("close", () => {
592
+ clearInterval(pingInterval);
593
+ for (const [, reject] of pendingResults) {
594
+ }
595
+ pendingResults.clear();
596
+ pendingConfirms.clear();
597
+ });
598
+ ws.on("message", (data) => {
599
+ let msg;
600
+ try {
601
+ msg = JSON.parse(data.toString());
602
+ } catch {
603
+ send({ type: "error", code: "parse_error", message: "Invalid JSON" });
604
+ return;
605
+ }
606
+ switch (msg.type) {
607
+ case "manifest":
608
+ handleManifest(msg, session, send, openai, model);
609
+ break;
610
+ case "text":
611
+ if (!msg.message) return;
612
+ if (processing) {
613
+ send({ type: "error", code: "busy", message: "Already processing a request" });
614
+ return;
615
+ }
616
+ processing = true;
617
+ handleText(msg.message, session, openai, model, send, makeExecuteFn(send, session, pendingResults, pendingConfirms)).finally(() => {
618
+ processing = false;
619
+ });
620
+ break;
621
+ case "state":
622
+ session.setScreen(msg.screen);
623
+ break;
624
+ case "result":
625
+ deliverResult(msg, pendingResults);
626
+ break;
627
+ case "confirm":
628
+ deliverConfirm(msg, pendingConfirms, session, openai, model, send, makeExecuteFn(send, session, pendingResults, pendingConfirms));
629
+ break;
630
+ case "llm_config":
631
+ break;
632
+ case "response_lang_config":
633
+ break;
634
+ }
635
+ });
636
+ }
637
+ function handleManifest(msg, session, send, openai, model) {
638
+ session.setManifest(msg);
639
+ send({ type: "status", status: "idle" });
640
+ const name = msg.persona?.name ?? "assistant";
641
+ send({
642
+ type: "chat",
643
+ from: "agent",
644
+ message: `Connected. How can I help?`,
645
+ final: true
646
+ });
647
+ }
648
+ async function handleText(text, session, openai, model, send, execute) {
649
+ send({ type: "status", status: "thinking" });
650
+ try {
651
+ await runAgentLoop(openai, model, session, text, execute, send);
652
+ } catch (err) {
653
+ console.error("[acp-server] Agent error:", err);
654
+ send({ type: "error", code: "agent_error", message: String(err) });
655
+ }
656
+ send({ type: "status", status: "idle" });
657
+ }
658
+ function deliverResult(msg, pendingResults) {
659
+ const resolver = pendingResults.get(msg.seq);
660
+ if (resolver) {
661
+ pendingResults.delete(msg.seq);
662
+ resolver(msg);
663
+ }
664
+ }
665
+ function deliverConfirm(msg, pendingConfirms, session, openai, model, send, execute) {
666
+ const resolver = pendingConfirms.get(msg.seq);
667
+ if (resolver) {
668
+ pendingConfirms.delete(msg.seq);
669
+ resolver(msg.confirmed);
670
+ }
671
+ }
672
+ function makeExecuteFn(send, session, pendingResults, pendingConfirms) {
673
+ return (seq, actions) => {
674
+ const isConfirmOnly = actions.length === 1 && actions[0].do === "ask_confirm";
675
+ send({ type: "command", seq, actions });
676
+ if (isConfirmOnly) {
677
+ return new Promise((resolve, reject) => {
678
+ const timer = setTimeout(() => {
679
+ pendingConfirms.delete(seq);
680
+ reject(new Error(`Timeout waiting for confirm seq=${seq}`));
681
+ }, EXECUTE_TIMEOUT_MS);
682
+ pendingConfirms.set(seq, (confirmed) => {
683
+ clearTimeout(timer);
684
+ resolve({
685
+ type: "result",
686
+ seq,
687
+ results: [{ index: 0, success: confirmed }]
688
+ });
689
+ });
690
+ });
691
+ }
692
+ return new Promise((resolve, reject) => {
693
+ const timer = setTimeout(() => {
694
+ pendingResults.delete(seq);
695
+ reject(new Error(`Timeout waiting for result seq=${seq}`));
696
+ }, EXECUTE_TIMEOUT_MS);
697
+ pendingResults.set(seq, (result) => {
698
+ clearTimeout(timer);
699
+ resolve(result);
700
+ });
701
+ });
702
+ };
703
+ }
704
+
705
+ export {
706
+ buildSystemPrompt,
707
+ Session,
708
+ manifestToTools,
709
+ toolCallToUIAction,
710
+ runAgentLoop,
711
+ createServer
712
+ };
713
+ //# sourceMappingURL=chunk-DB5IW3GZ.js.map