@wrongstack/acp 0.257.2 → 0.264.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/dist/agent.js CHANGED
@@ -205,182 +205,366 @@ function toolToPriority(tool) {
205
205
  return "low";
206
206
  }
207
207
 
208
+ // src/types/acp-v1.ts
209
+ var ACP_PROTOCOL_VERSION = 1;
210
+
208
211
  // src/agent/protocol-handler.ts
209
- var WRONGSTACK_VERSION = "0.1.0";
210
- var WRONGSTACK_CAPABILITIES = [
211
- "code-generation",
212
- "async-tools",
213
- "streaming",
214
- "progress"
212
+ function toWire(msg) {
213
+ return msg;
214
+ }
215
+ var WRONGSTACK_VERSION = "0.263.0";
216
+ var DEFAULT_MODE_ID = "code";
217
+ var DEFAULT_MODES = [
218
+ {
219
+ id: DEFAULT_MODE_ID,
220
+ name: "Code",
221
+ description: "Default agent mode for code-generation tasks."
222
+ }
215
223
  ];
216
224
  var ACPProtocolHandler = class {
217
- constructor(transport, registry, context) {
218
- this.transport = transport;
219
- this.registry = registry;
220
- this.context = context;
221
- }
222
225
  transport;
223
- registry;
224
- context;
226
+ defaultCwd;
227
+ runTurn;
228
+ onSessionNew;
229
+ modes;
230
+ configOptions;
231
+ agentName;
225
232
  initialized = false;
226
- signal = new AbortController();
227
- pendingCalls = /* @__PURE__ */ new Map();
228
- /** Wire an external abort signal from the ACP client */
229
- wireAbortController(abortController) {
230
- abortController.signal.addEventListener("abort", () => {
231
- for (const id of this.pendingCalls.keys()) {
232
- this.transport.send({ id, method: "cancel", result: { ok: true } }).catch((err) => console.debug(`[protocol-handler] cancel send failed: ${err}`));
233
- }
233
+ sessions = /* @__PURE__ */ new Map();
234
+ nextId = 1;
235
+ constructor(opts) {
236
+ this.transport = opts.transport;
237
+ this.defaultCwd = opts.defaultCwd;
238
+ this.runTurn = opts.runTurn;
239
+ this.onSessionNew = opts.onSessionNew ?? (() => {
234
240
  });
241
+ this.modes = opts.modes ?? DEFAULT_MODES;
242
+ this.configOptions = opts.configOptions ?? [];
243
+ this.agentName = opts.agentName ?? "wrongstack";
235
244
  }
236
- /** Process one inbound message. Returns true if this was a terminal message. */
245
+ /**
246
+ * Process one inbound message. Returns true if this was a terminal
247
+ * message (rare; reserved for future use by the server's own
248
+ * shutdown signal).
249
+ */
237
250
  async handleMessage(msg) {
238
- if (msg.id !== void 0) {
239
- return this.handleRequest(msg);
251
+ if (typeof msg !== "object" || msg === null) return false;
252
+ const m = msg;
253
+ if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
254
+ return false;
240
255
  }
241
- return this.handleNotification(msg);
256
+ if (m.id !== void 0 && typeof m.method === "string") {
257
+ return this.handleRequest(m.id, m.method, m.params);
258
+ }
259
+ if (typeof m.method === "string") {
260
+ return this.handleNotification(m.method, m.params);
261
+ }
262
+ return false;
242
263
  }
243
- async handleRequest(req) {
244
- if (req.method !== "initialize" && !this.initialized) {
245
- await this.sendError(req.id ?? null, -32e3, "Not initialized");
264
+ /** Abort all active turns and drop session state. */
265
+ close() {
266
+ for (const [, session] of this.sessions) {
267
+ session.abort.abort();
268
+ }
269
+ this.sessions.clear();
270
+ }
271
+ // ────────────────────────────────────────────────────────────────────
272
+ // Requests
273
+ // ────────────────────────────────────────────────────────────────────
274
+ async handleRequest(id, method, params) {
275
+ if (method !== "initialize" && !this.initialized) {
276
+ await this.sendError(id, -32e3, "Not initialized");
246
277
  return false;
247
278
  }
248
- const id = req.id;
249
- switch (req.method) {
250
- case "initialize":
251
- return this.handleInitialize(req, id);
252
- case "ping":
253
- await this.transport.send({ id, method: "ping", result: { pong: true } });
254
- return false;
255
- case "tools/call":
256
- return this.handleToolCall(req, id);
257
- case "tools/list":
258
- return this.handleToolsList(id);
259
- case "cancel":
260
- return this.handleCancel(id);
261
- case "session/list":
262
- return this.handleSessionList(id);
263
- case "sessionInfoUpdate":
264
- await this.transport.send({ id, method: "sessionInfoUpdate", result: { ok: true } });
265
- return false;
266
- default:
267
- await this.sendError(id, -32601, `Unknown method: ${req.method}`);
268
- return false;
279
+ try {
280
+ switch (method) {
281
+ case "initialize":
282
+ return await this.handleInitialize(id, params);
283
+ case "authenticate":
284
+ return await this.handleAuthenticate(id, params);
285
+ case "session/new":
286
+ return await this.handleSessionNew(id, params);
287
+ case "session/load":
288
+ return await this.handleSessionLoad(id, params);
289
+ case "session/prompt":
290
+ return await this.handleSessionPrompt(id, params);
291
+ case "session/set_mode":
292
+ return await this.handleSetMode(id, params);
293
+ case "session/set_config_option":
294
+ return await this.handleSetConfigOption(id, params);
295
+ case "session/list":
296
+ return await this.handleSessionList(id);
297
+ default:
298
+ await this.sendError(id, -32601, `Unknown method: ${method}`);
299
+ return false;
300
+ }
301
+ } catch (err) {
302
+ const { code, message, data } = errorToJsonRpc(err);
303
+ await this.sendError(id, code, message, data);
304
+ return false;
269
305
  }
270
306
  }
271
- async handleNotification(n) {
272
- if (n.method === "cancel") {
273
- this.handleCancelNotification(n);
307
+ async handleInitialize(id, params) {
308
+ const p = params ?? {};
309
+ const requested = typeof p.protocolVersion === "number" ? p.protocolVersion : 1;
310
+ if (requested !== ACP_PROTOCOL_VERSION) {
311
+ await this.sendError(
312
+ id,
313
+ -32e3,
314
+ `server speaks protocolVersion=${ACP_PROTOCOL_VERSION}, client requested ${requested}`
315
+ );
316
+ return false;
274
317
  }
275
- return false;
276
- }
277
- async handleInitialize(req, id) {
278
318
  this.initialized = true;
279
- const result = {
280
- capabilities: WRONGSTACK_CAPABILITIES,
281
- agentName: "WrongStack",
282
- agentVersion: WRONGSTACK_VERSION,
283
- protocolVersion: req.params?.protocolVersion ?? "2024-11",
284
- ...this.registry.buildToolList()
285
- };
286
- await this.transport.send({ id, method: "initialize", result });
319
+ await this.transport.send(toWire({
320
+ jsonrpc: "2.0",
321
+ id,
322
+ result: {
323
+ protocolVersion: ACP_PROTOCOL_VERSION,
324
+ agentCapabilities: {
325
+ loadSession: true,
326
+ promptCapabilities: {
327
+ image: false,
328
+ audio: false,
329
+ embeddedContext: true
330
+ }
331
+ },
332
+ agentInfo: {
333
+ name: this.agentName,
334
+ title: "WrongStack",
335
+ version: WRONGSTACK_VERSION
336
+ },
337
+ // Static options advertised at handshake. They are also
338
+ // re-sent on every `current_mode_update` / `config_option_update`
339
+ // notification so late-joining clients see them.
340
+ authMethods: [],
341
+ modes: this.modes,
342
+ configOptions: this.configOptions
343
+ }
344
+ }));
287
345
  return false;
288
346
  }
289
- async handleToolsList(id) {
290
- await this.transport.send({
347
+ async handleAuthenticate(id, _params) {
348
+ await this.transport.send(toWire({
349
+ jsonrpc: "2.0",
291
350
  id,
292
- method: "tools/list",
293
- result: this.registry.buildToolList()
294
- });
351
+ result: { outcome: "unauthenticated" }
352
+ }));
295
353
  return false;
296
354
  }
297
- async handleToolCall(req, id) {
298
- const { name, arguments: args } = req.params;
299
- const runPromise = (async () => {
300
- if (!this.registry.has(name)) {
301
- return {
302
- content: [{ type: "text", text: `Tool not found: ${name}` }],
303
- isError: true
304
- };
355
+ async handleSessionNew(id, params) {
356
+ const p = params ?? {};
357
+ const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
358
+ const sessionId = `sess_${this.allocId()}`;
359
+ const now = (/* @__PURE__ */ new Date()).toISOString();
360
+ const state = {
361
+ id: sessionId,
362
+ cwd,
363
+ abort: new AbortController(),
364
+ modeId: DEFAULT_MODE_ID,
365
+ createdAt: now,
366
+ updatedAt: now
367
+ };
368
+ this.sessions.set(sessionId, state);
369
+ this.onSessionNew(state);
370
+ await this.sendNotification({
371
+ sessionId,
372
+ update: {
373
+ sessionUpdate: "current_mode_update",
374
+ modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
305
375
  }
306
- const result = await this.registry.execute(
307
- name,
308
- args,
309
- this.context,
310
- this.signal.signal
311
- );
312
- return result ?? { content: [{ type: "text", text: "Tool returned null" }], isError: false };
313
- })();
314
- this.pendingCalls.set(id, runPromise);
376
+ });
377
+ if (this.configOptions.length > 0) {
378
+ await this.sendNotification({
379
+ sessionId,
380
+ update: {
381
+ sessionUpdate: "config_option_update",
382
+ configOptions: [...this.configOptions]
383
+ }
384
+ });
385
+ }
386
+ await this.transport.send(toWire({
387
+ jsonrpc: "2.0",
388
+ id,
389
+ result: {
390
+ sessionId,
391
+ modes: this.modes,
392
+ configOptions: this.configOptions
393
+ }
394
+ }));
395
+ return false;
396
+ }
397
+ async handleSessionLoad(id, params) {
398
+ return this.handleSessionNew(id, params);
399
+ }
400
+ async handleSessionPrompt(id, params) {
401
+ const p = params ?? {};
402
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
403
+ if (!sessionId || !this.sessions.has(sessionId)) {
404
+ await this.sendError(id, -32e3, "unknown or missing sessionId");
405
+ return false;
406
+ }
407
+ if (!Array.isArray(p.prompt)) {
408
+ await this.sendError(id, -32602, "prompt must be an array of content blocks");
409
+ return false;
410
+ }
411
+ const session = this.sessions.get(sessionId);
412
+ if (session.abort.signal.aborted) {
413
+ session.abort = new AbortController();
414
+ }
415
+ const turnSignal = new AbortController();
416
+ const onCancel = () => turnSignal.abort();
417
+ session.abort.signal.addEventListener("abort", onCancel, { once: true });
418
+ let result;
315
419
  try {
316
- const toolResult = await runPromise;
317
- this.pendingCalls.delete(id);
318
- const response = { method: "tools/call", id, result: toolResult };
319
- await this.transport.send(response);
420
+ result = await this.runTurn(
421
+ { sessionId, prompt: p.prompt, signal: turnSignal.signal },
422
+ (update) => this.sendNotification({ sessionId, update })
423
+ );
320
424
  } catch (err) {
321
- this.pendingCalls.delete(id);
322
- const msg = err instanceof Error ? err.message : String(err);
323
- await this.transport.send({
324
- id,
325
- method: "tools/call",
326
- result: { content: [{ type: "text", text: msg }], isError: true }
327
- });
425
+ session.abort.signal.removeEventListener("abort", onCancel);
426
+ const { code, message, data } = errorToJsonRpc(err);
427
+ await this.sendError(id, code, message, data);
428
+ return false;
328
429
  }
430
+ session.abort.signal.removeEventListener("abort", onCancel);
431
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
432
+ await this.transport.send(toWire({
433
+ jsonrpc: "2.0",
434
+ id,
435
+ result: { stopReason: result.stopReason }
436
+ }));
329
437
  return false;
330
438
  }
331
- async handleCancel(id) {
332
- this.pendingCalls.delete(id);
333
- await this.transport.send({ id, method: "cancel", result: { ok: true } });
439
+ async handleSetMode(id, params) {
440
+ const p = params ?? {};
441
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
442
+ const modeId = typeof p.modeId === "string" ? p.modeId : null;
443
+ const session = sessionId ? this.sessions.get(sessionId) : void 0;
444
+ if (!session || !modeId || !this.modes.some((m) => m.id === modeId)) {
445
+ await this.sendError(id, -32602, "invalid sessionId or modeId");
446
+ return false;
447
+ }
448
+ session.modeId = modeId;
449
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
450
+ await this.sendNotification({
451
+ sessionId,
452
+ update: { sessionUpdate: "current_mode_update", modeId }
453
+ });
454
+ await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
334
455
  return false;
335
456
  }
336
- handleCancelNotification(_n) {
457
+ async handleSetConfigOption(id, params) {
458
+ const p = params ?? {};
459
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
460
+ const optionId = typeof p.configOptionId === "string" ? p.configOptionId : null;
461
+ const value = typeof p.value === "string" ? p.value : null;
462
+ const session = sessionId ? this.sessions.get(sessionId) : void 0;
463
+ const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
464
+ if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
465
+ await this.sendError(id, -32602, "invalid sessionId, configOptionId, or value");
466
+ return false;
467
+ }
468
+ option.currentValue = value;
469
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
470
+ await this.sendNotification({
471
+ sessionId,
472
+ update: {
473
+ sessionUpdate: "config_option_update",
474
+ configOptions: [...this.configOptions]
475
+ }
476
+ });
477
+ await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
478
+ return false;
337
479
  }
338
480
  async handleSessionList(id) {
339
- await this.transport.send({
340
- id,
341
- method: "session/list",
342
- result: { sessions: [] }
481
+ const sessions = Array.from(this.sessions.values()).map((s) => {
482
+ const out = {
483
+ sessionId: s.id,
484
+ cwd: s.cwd,
485
+ updatedAt: s.updatedAt
486
+ };
487
+ if (s.title !== void 0) out.title = s.title;
488
+ return out;
343
489
  });
490
+ await this.transport.send(toWire({
491
+ jsonrpc: "2.0",
492
+ id,
493
+ result: { sessions }
494
+ }));
344
495
  return false;
345
496
  }
346
- async sendError(id, code, message) {
347
- if (id === null) return;
348
- await this.transport.send({ id, method: "", error: { code, message } });
497
+ // ────────────────────────────────────────────────────────────────────
498
+ // Notifications
499
+ // ────────────────────────────────────────────────────────────────────
500
+ async handleNotification(method, params) {
501
+ switch (method) {
502
+ case "session/cancel": {
503
+ const p = params ?? {};
504
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
505
+ const session = sessionId ? this.sessions.get(sessionId) : void 0;
506
+ if (session) {
507
+ session.abort.abort();
508
+ }
509
+ return false;
510
+ }
511
+ case "exit":
512
+ this.close();
513
+ return true;
514
+ default:
515
+ return false;
516
+ }
517
+ }
518
+ // ────────────────────────────────────────────────────────────────────
519
+ // Wire helpers
520
+ // ────────────────────────────────────────────────────────────────────
521
+ async sendNotification(params) {
522
+ await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
523
+ }
524
+ async sendError(id, code, message, data) {
525
+ const error = { code, message };
526
+ if (data !== void 0) error.data = data;
527
+ await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
528
+ }
529
+ allocId() {
530
+ return this.nextId++;
349
531
  }
350
532
  };
533
+ function errorToJsonRpc(err) {
534
+ if (err && typeof err === "object") {
535
+ const e = err;
536
+ if (typeof e.code === "number" && typeof e.message === "string") {
537
+ const result = {
538
+ code: e.code,
539
+ message: e.message
540
+ };
541
+ if (e.data !== void 0) result.data = e.data;
542
+ return result;
543
+ }
544
+ }
545
+ const message = err instanceof Error ? err.message : String(err);
546
+ return { code: -32603, message };
547
+ }
351
548
  var WrongStackACPServer = class {
352
549
  transport;
353
- registry;
354
550
  handler;
355
551
  running = false;
356
- constructor(opts) {
552
+ constructor(opts = {}) {
357
553
  this.transport = new StdioTransport();
358
- this.registry = new ACPToolsRegistry(opts.owner);
359
- this.registry.register(opts.tools);
360
- this.handler = new ACPProtocolHandler(
361
- this.transport,
362
- this.registry,
363
- // Future: WrongStack session/memory context for tool execution.
364
- // When wired, this would carry session state, memory entries, and
365
- // project metadata so ACP tools can self-contextualise.
366
- // Tracked in docs/notes/refactor-2026-06-05.md §5.1.
367
- {}
368
- );
554
+ const runTurn = opts.runTurn ?? defaultEchoRunTurn;
555
+ this.handler = new ACPProtocolHandler({
556
+ transport: this.transport,
557
+ defaultCwd: opts.defaultCwd ?? process.cwd(),
558
+ runTurn,
559
+ agentName: opts.agentName
560
+ });
369
561
  }
370
562
  /**
371
563
  * Start the server. Blocks until the client disconnects.
372
564
  *
373
- * 1. Send the startup marker `[wstack-acp]` so the client
374
- * knows which stdout line is the protocol boundary.
375
- * 2. Loop: read messages, dispatch to handler, until EOF or error.
376
- *
377
- * Single dispatch path: every inbound message is read exactly once
378
- * from the transport and passed to the protocol handler exactly once.
379
- * An earlier version combined a `transport.onMessage` callback with
380
- * this read loop, which caused every message to be processed twice
381
- * (once by the callback, once by the loop) — duplicate tool calls
382
- * and duplicate responses to the client. See the ACP double-dispatch
383
- * fix in the security audit (P1-001).
565
+ * 1. Print the legacy `[wstack-acp]\n` marker so the client knows the
566
+ * process is the ACP server (the old `StdioTransport` handshake).
567
+ * 2. Loop: read messages, dispatch to the handler, until EOF / error.
384
568
  */
385
569
  async start() {
386
570
  this.transport.sendStartupMarker();
@@ -399,8 +583,11 @@ var WrongStackACPServer = class {
399
583
  this.transport.close();
400
584
  }
401
585
  };
586
+ var defaultEchoRunTurn = async (_input, _emit) => {
587
+ return { stopReason: "end_turn" };
588
+ };
402
589
  async function main() {
403
- const server = new WrongStackACPServer({ tools: [] });
590
+ const server = new WrongStackACPServer();
404
591
  await server.start();
405
592
  }
406
593
  var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
@@ -412,6 +599,88 @@ if (isEntrypoint) {
412
599
  });
413
600
  }
414
601
 
415
- export { ACPProtocolHandler, ACPToolsRegistry, StdioTransport, WrongStackACPServer };
602
+ // src/agent/server-agent-turn.ts
603
+ function makeACPServerAgentTurn(opts) {
604
+ const agents = /* @__PURE__ */ new Map();
605
+ const timeouts = /* @__PURE__ */ new Map();
606
+ const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
607
+ return async (input, emit) => {
608
+ let agent = agents.get(input.sessionId);
609
+ if (!agent) {
610
+ agent = await opts.agentFor(input.sessionId, process.cwd());
611
+ agents.set(input.sessionId, agent);
612
+ }
613
+ const timer = setTimeout(() => {
614
+ timeouts.delete(input.sessionId);
615
+ }, timeoutMs);
616
+ timeouts.set(input.sessionId, timer);
617
+ try {
618
+ const userMessage = promptToText(input.prompt);
619
+ const result = await agent.run(userMessage, { signal: input.signal });
620
+ const text = extractText(result);
621
+ if (text) {
622
+ emit({
623
+ sessionUpdate: "agent_message_chunk",
624
+ content: { type: "text", text }
625
+ });
626
+ }
627
+ const result_out = {
628
+ stopReason: pickStopReason(result, input.signal)
629
+ };
630
+ if (text) result_out.text = text;
631
+ return result_out;
632
+ } finally {
633
+ clearTimeout(timer);
634
+ timeouts.delete(input.sessionId);
635
+ }
636
+ };
637
+ }
638
+ function promptToText(blocks) {
639
+ const parts = [];
640
+ for (const b of blocks) {
641
+ if (b.type === "text") {
642
+ parts.push(b.text);
643
+ } else if (b.type === "image") {
644
+ parts.push(`[image: ${b.mimeType}]`);
645
+ } else if (b.type === "audio") {
646
+ parts.push(`[audio: ${b.mimeType}]`);
647
+ } else if (b.type === "resource") {
648
+ parts.push(`[embedded resource: ${b.resource.uri}]`);
649
+ } else if (b.type === "resource_link") {
650
+ parts.push(`[resource link: ${b.uri}]`);
651
+ }
652
+ }
653
+ return parts.join("\n").trim();
654
+ }
655
+ function extractText(result) {
656
+ if (typeof result !== "object" || result === null) return "";
657
+ const r = result;
658
+ if (typeof r.text === "string") return r.text;
659
+ if (Array.isArray(r.content)) {
660
+ const parts = [];
661
+ for (const c of r.content) {
662
+ if (typeof c === "object" && c !== null) {
663
+ const cb = c;
664
+ if (cb.type === "text" && typeof cb.text === "string") parts.push(cb.text);
665
+ }
666
+ }
667
+ return parts.join("");
668
+ }
669
+ return "";
670
+ }
671
+ function pickStopReason(result, signal) {
672
+ if (signal.aborted) return "cancelled";
673
+ if (typeof result !== "object" || result === null) return "end_turn";
674
+ const r = result;
675
+ if (r.error) {
676
+ return "end_turn";
677
+ }
678
+ if (typeof r.stopReason === "string" && r.stopReason) {
679
+ return r.stopReason;
680
+ }
681
+ return "end_turn";
682
+ }
683
+
684
+ export { ACPProtocolHandler, ACPToolsRegistry, StdioTransport, WrongStackACPServer, makeACPServerAgentTurn };
416
685
  //# sourceMappingURL=agent.js.map
417
686
  //# sourceMappingURL=agent.js.map