@wingman-ai/gateway 0.2.5 → 0.3.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 (90) hide show
  1. package/.wingman/agents/coding/agent.md +5 -0
  2. package/.wingman/agents/coding-v2/agent.md +58 -0
  3. package/.wingman/agents/game-dev/agent.md +94 -0
  4. package/.wingman/agents/game-dev/art-generation.md +37 -0
  5. package/.wingman/agents/game-dev/asset-refinement.md +17 -0
  6. package/.wingman/agents/game-dev/planning-idea.md +17 -0
  7. package/.wingman/agents/game-dev/ui-specialist.md +17 -0
  8. package/.wingman/agents/main/agent.md +2 -0
  9. package/README.md +1 -0
  10. package/dist/agent/config/agentConfig.d.ts +4 -0
  11. package/dist/agent/config/mcpClientManager.cjs +44 -10
  12. package/dist/agent/config/mcpClientManager.d.ts +6 -2
  13. package/dist/agent/config/mcpClientManager.js +44 -10
  14. package/dist/agent/config/toolRegistry.cjs +3 -1
  15. package/dist/agent/config/toolRegistry.js +3 -1
  16. package/dist/agent/tests/mcpClientManager.test.cjs +124 -0
  17. package/dist/agent/tests/mcpClientManager.test.d.ts +1 -0
  18. package/dist/agent/tests/mcpClientManager.test.js +118 -0
  19. package/dist/agent/tools/command_execute.cjs +1 -1
  20. package/dist/agent/tools/command_execute.js +1 -1
  21. package/dist/cli/config/schema.d.ts +2 -0
  22. package/dist/cli/core/agentInvoker.cjs +55 -66
  23. package/dist/cli/core/agentInvoker.d.ts +10 -13
  24. package/dist/cli/core/agentInvoker.js +42 -62
  25. package/dist/cli/core/imagePersistence.cjs +125 -0
  26. package/dist/cli/core/imagePersistence.d.ts +24 -0
  27. package/dist/cli/core/imagePersistence.js +85 -0
  28. package/dist/cli/core/sessionManager.cjs +297 -40
  29. package/dist/cli/core/sessionManager.d.ts +9 -0
  30. package/dist/cli/core/sessionManager.js +297 -40
  31. package/dist/debug/terminalProbe.cjs +57 -0
  32. package/dist/debug/terminalProbe.d.ts +10 -0
  33. package/dist/debug/terminalProbe.js +20 -0
  34. package/dist/debug/terminalProbeAuth.cjs +140 -0
  35. package/dist/debug/terminalProbeAuth.d.ts +20 -0
  36. package/dist/debug/terminalProbeAuth.js +97 -0
  37. package/dist/gateway/http/fs.cjs +19 -0
  38. package/dist/gateway/http/fs.js +19 -0
  39. package/dist/gateway/http/sessions.cjs +25 -5
  40. package/dist/gateway/http/sessions.js +25 -5
  41. package/dist/gateway/server.cjs +112 -11
  42. package/dist/gateway/server.d.ts +2 -0
  43. package/dist/gateway/server.js +112 -11
  44. package/dist/tests/agentInvokerSummarization.test.cjs +56 -37
  45. package/dist/tests/agentInvokerSummarization.test.js +58 -39
  46. package/dist/tests/agentInvokerWorkdir.test.cjs +50 -0
  47. package/dist/tests/agentInvokerWorkdir.test.js +52 -2
  48. package/dist/tests/cli-init.test.cjs +36 -0
  49. package/dist/tests/cli-init.test.js +36 -0
  50. package/dist/tests/falRuntime.test.cjs +78 -0
  51. package/dist/tests/falRuntime.test.d.ts +1 -0
  52. package/dist/tests/falRuntime.test.js +72 -0
  53. package/dist/tests/falSummary.test.cjs +51 -0
  54. package/dist/tests/falSummary.test.d.ts +1 -0
  55. package/dist/tests/falSummary.test.js +45 -0
  56. package/dist/tests/gateway.test.cjs +109 -1
  57. package/dist/tests/gateway.test.js +109 -1
  58. package/dist/tests/imagePersistence.test.cjs +143 -0
  59. package/dist/tests/imagePersistence.test.d.ts +1 -0
  60. package/dist/tests/imagePersistence.test.js +137 -0
  61. package/dist/tests/sessionMessageAttachments.test.cjs +30 -0
  62. package/dist/tests/sessionMessageAttachments.test.js +30 -0
  63. package/dist/tests/sessionStateMessages.test.cjs +126 -0
  64. package/dist/tests/sessionStateMessages.test.js +126 -0
  65. package/dist/tests/sessions-api.test.cjs +117 -3
  66. package/dist/tests/sessions-api.test.js +118 -4
  67. package/dist/tests/terminalProbe.test.cjs +45 -0
  68. package/dist/tests/terminalProbe.test.d.ts +1 -0
  69. package/dist/tests/terminalProbe.test.js +39 -0
  70. package/dist/tests/terminalProbeAuth.test.cjs +85 -0
  71. package/dist/tests/terminalProbeAuth.test.d.ts +1 -0
  72. package/dist/tests/terminalProbeAuth.test.js +79 -0
  73. package/dist/tools/fal/runtime.cjs +103 -0
  74. package/dist/tools/fal/runtime.d.ts +10 -0
  75. package/dist/tools/fal/runtime.js +60 -0
  76. package/dist/tools/fal/summary.cjs +78 -0
  77. package/dist/tools/fal/summary.d.ts +22 -0
  78. package/dist/tools/fal/summary.js +41 -0
  79. package/dist/tools/mcp-fal-ai.cjs +1041 -0
  80. package/dist/tools/mcp-fal-ai.d.ts +1 -0
  81. package/dist/tools/mcp-fal-ai.js +1025 -0
  82. package/dist/types/mcp.cjs +2 -0
  83. package/dist/types/mcp.d.ts +8 -0
  84. package/dist/types/mcp.js +3 -1
  85. package/dist/webui/assets/index-0nUBsUUq.js +278 -0
  86. package/dist/webui/assets/index-kk7OrD-G.css +11 -0
  87. package/dist/webui/index.html +2 -2
  88. package/package.json +11 -8
  89. package/dist/webui/assets/index-C7EuTbnE.js +0 -270
  90. package/dist/webui/assets/index-DVWQluit.css +0 -11
@@ -336,12 +336,29 @@ class GatewayServer {
336
336
  const sessionManager = await this.getSessionManager(agentId);
337
337
  const existingSession = sessionManager.getSession(sessionKey);
338
338
  const session = existingSession || sessionManager.getOrCreateSession(sessionKey, agentId);
339
+ const requestId = msg.id || `req-${Date.now()}`;
339
340
  const workdir = session.metadata?.workdir ?? null;
340
341
  const defaultOutputDir = this.resolveDefaultOutputDir(agentId);
341
342
  const preview = hasContent ? content.trim() : buildAttachmentPreview(attachments);
342
343
  sessionManager.updateSession(session.id, {
344
+ messageCount: (session.messageCount ?? 0) + 1,
343
345
  lastMessagePreview: preview.substring(0, 200)
344
346
  });
347
+ try {
348
+ sessionManager.persistPendingMessage({
349
+ sessionId: sessionKey,
350
+ requestId,
351
+ message: {
352
+ id: `user-${requestId}`,
353
+ role: "user",
354
+ content,
355
+ attachments: attachments.length > 0 ? mapAttachmentsForPendingMessage(attachments) : void 0,
356
+ createdAt: Date.now()
357
+ }
358
+ });
359
+ } catch (error) {
360
+ this.logger.warn("Failed to persist pending user message", error);
361
+ }
345
362
  if (!existingSession) this.internalHooks?.emit({
346
363
  type: "session",
347
364
  action: "start",
@@ -444,9 +461,15 @@ class GatewayServer {
444
461
  this.activeSessionRequests.set(sessionQueueKey, msg.id);
445
462
  const outputManager = new OutputManager("interactive");
446
463
  let emittedAgentError = false;
464
+ let streamedCompletionResult;
447
465
  const outputHandler = (event)=>{
448
466
  const payloadWithSession = this.attachSessionContext(event, sessionKey, agentId);
449
- if (payloadWithSession && "object" == typeof payloadWithSession && !Array.isArray(payloadWithSession) && "agent-error" === payloadWithSession.type) emittedAgentError = true;
467
+ const payloadType = payloadWithSession && "object" == typeof payloadWithSession && !Array.isArray(payloadWithSession) && "string" == typeof payloadWithSession.type ? payloadWithSession.type : "";
468
+ if ("agent-complete" === payloadType) {
469
+ if (payloadWithSession && "object" == typeof payloadWithSession && !Array.isArray(payloadWithSession)) streamedCompletionResult = payloadWithSession.result;
470
+ return;
471
+ }
472
+ if ("agent-error" === payloadType) emittedAgentError = true;
450
473
  const baseMessage = {
451
474
  type: "event:agent",
452
475
  id: msg.id,
@@ -477,17 +500,43 @@ class GatewayServer {
477
500
  abortController
478
501
  });
479
502
  try {
480
- await invoker.invokeAgent(agentId, content, sessionKey, attachments, {
503
+ const invocationResult = await invoker.invokeAgent(agentId, content, sessionKey, attachments, {
481
504
  signal: abortController.signal
482
505
  });
483
- const updated = sessionManager.getSession(sessionKey);
484
- if (updated) sessionManager.updateSession(sessionKey, {
485
- messageCount: updated.messageCount + 1
506
+ if (msg.id) sessionManager.clearPendingMessagesForRequest(sessionKey, msg.id);
507
+ if (emittedAgentError) return;
508
+ const invocationCancelled = abortController.signal.aborted || "object" == typeof invocationResult && null !== invocationResult && !Array.isArray(invocationResult) && true === invocationResult.cancelled;
509
+ if (invocationCancelled) return void this.sendAgentError(ws, msg.id, "Request cancelled", {
510
+ sessionId: sessionKey,
511
+ agentId,
512
+ broadcastToSession: true,
513
+ exclude: ws
514
+ });
515
+ const completionResult = void 0 === streamedCompletionResult ? invocationResult : streamedCompletionResult;
516
+ this.sendAgentComplete(ws, msg.id, completionResult, {
517
+ sessionId: sessionKey,
518
+ agentId,
519
+ broadcastToSession: true,
520
+ exclude: ws
486
521
  });
487
522
  } catch (error) {
488
523
  this.logger.error("Agent invocation failed", error);
524
+ const message = error instanceof Error ? error.message : String(error);
525
+ if (msg.id) try {
526
+ sessionManager.persistPendingMessage({
527
+ sessionId: sessionKey,
528
+ requestId: msg.id,
529
+ message: {
530
+ id: msg.id,
531
+ role: "assistant",
532
+ content: message,
533
+ createdAt: Date.now()
534
+ }
535
+ });
536
+ } catch (persistError) {
537
+ this.logger.warn("Failed to persist pending assistant error message", persistError);
538
+ }
489
539
  if (!emittedAgentError) {
490
- const message = error instanceof Error ? error.message : String(error);
491
540
  const stack = error instanceof Error ? error.stack : void 0;
492
541
  this.sendAgentError(ws, msg.id, message, {
493
542
  sessionId: sessionKey,
@@ -563,6 +612,12 @@ class GatewayServer {
563
612
  },
564
613
  timestamp: Date.now()
565
614
  });
615
+ this.sendAgentError(ws, requestId, "Request cancelled", {
616
+ sessionId: queued.sessionKey,
617
+ agentId: queued.agentId,
618
+ broadcastToSession: true,
619
+ exclude: ws
620
+ });
566
621
  return;
567
622
  }
568
623
  this.sendMessage(ws, {
@@ -737,11 +792,25 @@ class GatewayServer {
737
792
  }
738
793
  sendMessage(ws, message) {
739
794
  try {
740
- ws.send(JSON.stringify(message));
795
+ const result = ws.send(JSON.stringify(message));
796
+ if ("number" == typeof result && result <= 0) return false;
797
+ return true;
741
798
  } catch (error) {
742
799
  this.log("error", "Failed to send message", error);
800
+ return false;
743
801
  }
744
802
  }
803
+ sendMessageWithRetry(ws, message, attempt = 0) {
804
+ if (this.sendMessage(ws, message)) return;
805
+ if (attempt >= 2) return void this.log("warn", "Dropping websocket message after retry attempts", {
806
+ type: message.type,
807
+ id: message.id
808
+ });
809
+ const delayMs = 25 * (attempt + 1);
810
+ setTimeout(()=>{
811
+ this.sendMessageWithRetry(ws, message, attempt + 1);
812
+ }, delayMs);
813
+ }
745
814
  sendError(ws, code, message) {
746
815
  const errorPayload = {
747
816
  code,
@@ -753,6 +822,25 @@ class GatewayServer {
753
822
  timestamp: Date.now()
754
823
  });
755
824
  }
825
+ sendAgentComplete(ws, requestId, result, options) {
826
+ let payload = {
827
+ type: "agent-complete",
828
+ result: result ?? null,
829
+ timestamp: new Date().toISOString()
830
+ };
831
+ if (options?.sessionId && options?.agentId) payload = this.attachSessionContext(payload, options.sessionId, options.agentId);
832
+ const baseMessage = {
833
+ type: "event:agent",
834
+ id: requestId,
835
+ payload,
836
+ timestamp: Date.now()
837
+ };
838
+ this.sendMessageWithRetry(ws, {
839
+ ...baseMessage,
840
+ clientId: ws.data.clientId
841
+ });
842
+ if (options?.broadcastToSession && options.sessionId) this.broadcastSessionEvent(options.sessionId, baseMessage, options.exclude, true);
843
+ }
756
844
  sendAgentError(ws, requestId, message, options) {
757
845
  let payload = {
758
846
  type: "agent-error",
@@ -767,11 +855,11 @@ class GatewayServer {
767
855
  payload,
768
856
  timestamp: Date.now()
769
857
  };
770
- this.sendMessage(ws, {
858
+ this.sendMessageWithRetry(ws, {
771
859
  ...baseMessage,
772
860
  clientId: ws.data.clientId
773
861
  });
774
- if (options?.broadcastToSession && options.sessionId) this.broadcastSessionEvent(options.sessionId, baseMessage, options.exclude);
862
+ if (options?.broadcastToSession && options.sessionId) this.broadcastSessionEvent(options.sessionId, baseMessage, options.exclude, true);
775
863
  }
776
864
  cancelSocketAgentRequests(ws) {
777
865
  for (const [requestId, active] of this.activeAgentRequests)if (active.socket === ws) {
@@ -800,12 +888,13 @@ class GatewayServer {
800
888
  agentId
801
889
  };
802
890
  }
803
- broadcastSessionEvent(sessionId, message, exclude) {
891
+ broadcastSessionEvent(sessionId, message, exclude, reliable = false) {
804
892
  const subscribers = this.sessionSubscriptions.get(sessionId);
805
893
  if (!subscribers || 0 === subscribers.size) return 0;
806
894
  let sent = 0;
807
895
  for (const ws of subscribers)if (!exclude || ws !== exclude) {
808
- this.sendMessage(ws, message);
896
+ if (reliable) this.sendMessageWithRetry(ws, message);
897
+ else this.sendMessage(ws, message);
809
898
  sent++;
810
899
  }
811
900
  return sent;
@@ -1379,6 +1468,18 @@ function buildAttachmentPreview(attachments) {
1379
1468
  if (hasAudio) return count > 1 ? "Audio attachments" : "Audio attachment";
1380
1469
  return count > 1 ? "Image attachments" : "Image attachment";
1381
1470
  }
1471
+ function mapAttachmentsForPendingMessage(attachments) {
1472
+ return attachments.map((attachment)=>{
1473
+ const kind = isFileAttachment(attachment) ? "file" : isAudioAttachment(attachment) ? "audio" : "image";
1474
+ return {
1475
+ kind,
1476
+ dataUrl: attachment.dataUrl,
1477
+ name: attachment.name,
1478
+ mimeType: attachment.mimeType,
1479
+ size: attachment.size
1480
+ };
1481
+ });
1482
+ }
1382
1483
  function isAudioAttachment(attachment) {
1383
1484
  if ("audio" === attachment.kind) return true;
1384
1485
  if (attachment.mimeType?.startsWith("audio/")) return true;
@@ -348,42 +348,6 @@ const parseConfig = (input)=>{
348
348
  })).toBeUndefined();
349
349
  });
350
350
  });
351
- (0, external_vitest_namespaceObject.describe)("evaluateStreamingCompletion", ()=>{
352
- (0, external_vitest_namespaceObject.it)("blocks with stream_error when no assistant text and stream error exists", ()=>{
353
- const result = (0, agentInvoker_cjs_namespaceObject.evaluateStreamingCompletion)({
354
- sawAssistantText: false,
355
- fallbackText: void 0,
356
- streamErrorMessage: "provider timeout"
357
- });
358
- (0, external_vitest_namespaceObject.expect)(result).toEqual({
359
- status: "blocked",
360
- reason: "stream_error",
361
- message: "Model call failed: provider timeout"
362
- });
363
- });
364
- (0, external_vitest_namespaceObject.it)("blocks with empty_stream_response when no text or fallback is present", ()=>{
365
- const result = (0, agentInvoker_cjs_namespaceObject.evaluateStreamingCompletion)({
366
- sawAssistantText: false,
367
- fallbackText: void 0,
368
- streamErrorMessage: void 0
369
- });
370
- (0, external_vitest_namespaceObject.expect)(result).toEqual({
371
- status: "blocked",
372
- reason: "empty_stream_response",
373
- message: "Model completed without a response. Check provider logs for request errors."
374
- });
375
- });
376
- (0, external_vitest_namespaceObject.it)("returns ok when assistant text exists", ()=>{
377
- const result = (0, agentInvoker_cjs_namespaceObject.evaluateStreamingCompletion)({
378
- sawAssistantText: true,
379
- fallbackText: void 0,
380
- streamErrorMessage: void 0
381
- });
382
- (0, external_vitest_namespaceObject.expect)(result).toEqual({
383
- status: "ok"
384
- });
385
- });
386
- });
387
351
  (0, external_vitest_namespaceObject.describe)("LangGraph lifecycle termination", ()=>{
388
352
  (0, external_vitest_namespaceObject.it)("tracks root LangGraph run id from parentless on_chain_start", ()=>{
389
353
  const rootRunId = (0, agentInvoker_cjs_namespaceObject.trackRootLangGraphRunId)(void 0, {
@@ -441,12 +405,67 @@ const parseConfig = (input)=>{
441
405
  run_id: "root-run",
442
406
  parent_ids: []
443
407
  }, "root-run")).toBe(false);
408
+ });
409
+ (0, external_vitest_namespaceObject.it)("treats root LangGraph on_chain_end as terminal even without tracked run id", ()=>{
444
410
  (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.isRootLangGraphTerminalEvent)({
445
411
  event: "on_chain_end",
446
412
  name: "LangGraph",
447
413
  run_id: "root-run",
448
414
  parent_ids: []
449
- }, void 0)).toBe(false);
415
+ }, void 0)).toBe(true);
416
+ });
417
+ (0, external_vitest_namespaceObject.it)("treats root LangGraph on_chain_end as terminal when run id is missing", ()=>{
418
+ (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.isRootLangGraphTerminalEvent)({
419
+ event: "on_chain_end",
420
+ name: "LangGraph",
421
+ parent_ids: []
422
+ }, "root-run")).toBe(true);
423
+ });
424
+ });
425
+ (0, external_vitest_namespaceObject.describe)("emitCompletionAndContinuePostProcessing", ()=>{
426
+ (0, external_vitest_namespaceObject.it)("emits completion before post-processing resolves", ()=>{
427
+ const callOrder = [];
428
+ let resolvePostProcess;
429
+ const postProcess = ()=>new Promise((resolve)=>{
430
+ callOrder.push("post-process-start");
431
+ resolvePostProcess = resolve;
432
+ });
433
+ (0, agentInvoker_cjs_namespaceObject.emitCompletionAndContinuePostProcessing)({
434
+ outputManager: {
435
+ emitAgentComplete: ()=>{
436
+ callOrder.push("emit-complete");
437
+ }
438
+ },
439
+ result: {
440
+ ok: true
441
+ },
442
+ postProcess
443
+ });
444
+ (0, external_vitest_namespaceObject.expect)(callOrder).toEqual([
445
+ "emit-complete",
446
+ "post-process-start"
447
+ ]);
448
+ resolvePostProcess?.();
449
+ });
450
+ (0, external_vitest_namespaceObject.it)("logs and swallows post-processing failures", async ()=>{
451
+ const debug = external_vitest_namespaceObject.vi.fn();
452
+ (0, agentInvoker_cjs_namespaceObject.emitCompletionAndContinuePostProcessing)({
453
+ outputManager: {
454
+ emitAgentComplete: external_vitest_namespaceObject.vi.fn()
455
+ },
456
+ result: {
457
+ ok: true
458
+ },
459
+ postProcess: async ()=>{
460
+ throw new Error("materialization failed");
461
+ },
462
+ logger: {
463
+ debug
464
+ }
465
+ });
466
+ await Promise.resolve();
467
+ await Promise.resolve();
468
+ (0, external_vitest_namespaceObject.expect)(debug).toHaveBeenCalledWith("Failed post-completion processing for streamed agent response", external_vitest_namespaceObject.expect.any(Error));
450
469
  });
451
470
  });
452
471
  for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
@@ -1,6 +1,6 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import { validateConfig } from "../cli/config/schema.js";
3
- import { chunkHasAssistantText, configureDeepAgentSummarizationMiddleware, detectStreamErrorMessage, detectToolEventContext, evaluateStreamingCompletion, isRootLangGraphTerminalEvent, resolveHumanInTheLoopSettings, resolveModelRetryMiddlewareSettings, resolveSummarizationMiddlewareSettings, resolveToolRetryMiddlewareSettings, selectStreamingFallbackText, trackRootLangGraphRunId } from "../cli/core/agentInvoker.js";
3
+ import { chunkHasAssistantText, configureDeepAgentSummarizationMiddleware, detectStreamErrorMessage, detectToolEventContext, emitCompletionAndContinuePostProcessing, isRootLangGraphTerminalEvent, resolveHumanInTheLoopSettings, resolveModelRetryMiddlewareSettings, resolveSummarizationMiddlewareSettings, resolveToolRetryMiddlewareSettings, selectStreamingFallbackText, trackRootLangGraphRunId } from "../cli/core/agentInvoker.js";
4
4
  const parseConfig = (input)=>{
5
5
  const result = validateConfig(input);
6
6
  if (!result.success || !result.data) throw new Error(result.error || "Expected config validation to succeed");
@@ -346,42 +346,6 @@ describe("detectStreamErrorMessage", ()=>{
346
346
  })).toBeUndefined();
347
347
  });
348
348
  });
349
- describe("evaluateStreamingCompletion", ()=>{
350
- it("blocks with stream_error when no assistant text and stream error exists", ()=>{
351
- const result = evaluateStreamingCompletion({
352
- sawAssistantText: false,
353
- fallbackText: void 0,
354
- streamErrorMessage: "provider timeout"
355
- });
356
- expect(result).toEqual({
357
- status: "blocked",
358
- reason: "stream_error",
359
- message: "Model call failed: provider timeout"
360
- });
361
- });
362
- it("blocks with empty_stream_response when no text or fallback is present", ()=>{
363
- const result = evaluateStreamingCompletion({
364
- sawAssistantText: false,
365
- fallbackText: void 0,
366
- streamErrorMessage: void 0
367
- });
368
- expect(result).toEqual({
369
- status: "blocked",
370
- reason: "empty_stream_response",
371
- message: "Model completed without a response. Check provider logs for request errors."
372
- });
373
- });
374
- it("returns ok when assistant text exists", ()=>{
375
- const result = evaluateStreamingCompletion({
376
- sawAssistantText: true,
377
- fallbackText: void 0,
378
- streamErrorMessage: void 0
379
- });
380
- expect(result).toEqual({
381
- status: "ok"
382
- });
383
- });
384
- });
385
349
  describe("LangGraph lifecycle termination", ()=>{
386
350
  it("tracks root LangGraph run id from parentless on_chain_start", ()=>{
387
351
  const rootRunId = trackRootLangGraphRunId(void 0, {
@@ -439,11 +403,66 @@ describe("LangGraph lifecycle termination", ()=>{
439
403
  run_id: "root-run",
440
404
  parent_ids: []
441
405
  }, "root-run")).toBe(false);
406
+ });
407
+ it("treats root LangGraph on_chain_end as terminal even without tracked run id", ()=>{
442
408
  expect(isRootLangGraphTerminalEvent({
443
409
  event: "on_chain_end",
444
410
  name: "LangGraph",
445
411
  run_id: "root-run",
446
412
  parent_ids: []
447
- }, void 0)).toBe(false);
413
+ }, void 0)).toBe(true);
414
+ });
415
+ it("treats root LangGraph on_chain_end as terminal when run id is missing", ()=>{
416
+ expect(isRootLangGraphTerminalEvent({
417
+ event: "on_chain_end",
418
+ name: "LangGraph",
419
+ parent_ids: []
420
+ }, "root-run")).toBe(true);
421
+ });
422
+ });
423
+ describe("emitCompletionAndContinuePostProcessing", ()=>{
424
+ it("emits completion before post-processing resolves", ()=>{
425
+ const callOrder = [];
426
+ let resolvePostProcess;
427
+ const postProcess = ()=>new Promise((resolve)=>{
428
+ callOrder.push("post-process-start");
429
+ resolvePostProcess = resolve;
430
+ });
431
+ emitCompletionAndContinuePostProcessing({
432
+ outputManager: {
433
+ emitAgentComplete: ()=>{
434
+ callOrder.push("emit-complete");
435
+ }
436
+ },
437
+ result: {
438
+ ok: true
439
+ },
440
+ postProcess
441
+ });
442
+ expect(callOrder).toEqual([
443
+ "emit-complete",
444
+ "post-process-start"
445
+ ]);
446
+ resolvePostProcess?.();
447
+ });
448
+ it("logs and swallows post-processing failures", async ()=>{
449
+ const debug = vi.fn();
450
+ emitCompletionAndContinuePostProcessing({
451
+ outputManager: {
452
+ emitAgentComplete: vi.fn()
453
+ },
454
+ result: {
455
+ ok: true
456
+ },
457
+ postProcess: async ()=>{
458
+ throw new Error("materialization failed");
459
+ },
460
+ logger: {
461
+ debug
462
+ }
463
+ });
464
+ await Promise.resolve();
465
+ await Promise.resolve();
466
+ expect(debug).toHaveBeenCalledWith("Failed post-completion processing for streamed agent response", expect.any(Error));
448
467
  });
449
468
  });
@@ -21,6 +21,8 @@ var __webpack_require__ = {};
21
21
  __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
22
22
  })();
23
23
  var __webpack_exports__ = {};
24
+ const external_node_fs_namespaceObject = require("node:fs");
25
+ const external_node_os_namespaceObject = require("node:os");
24
26
  const external_node_path_namespaceObject = require("node:path");
25
27
  var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
26
28
  const external_vitest_namespaceObject = require("vitest");
@@ -83,6 +85,21 @@ const agentInvoker_cjs_namespaceObject = require("../cli/core/agentInvoker.cjs")
83
85
  (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.resolveExecutionWorkspace)(workspace, absolute)).toBe(external_node_path_default().normalize(absolute));
84
86
  });
85
87
  });
88
+ (0, external_vitest_namespaceObject.describe)("resolveAgentExecutionWorkspace", ()=>{
89
+ const workspace = external_node_path_default().resolve("workspace");
90
+ (0, external_vitest_namespaceObject.it)("prefers explicit workdir over default output dir", ()=>{
91
+ const workdir = external_node_path_default().resolve("outside", "session-output");
92
+ const defaultOutputDir = external_node_path_default().resolve("outside", "default-output");
93
+ (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.resolveAgentExecutionWorkspace)(workspace, workdir, defaultOutputDir)).toBe(external_node_path_default().normalize(workdir));
94
+ });
95
+ (0, external_vitest_namespaceObject.it)("uses default output dir when session workdir is unset", ()=>{
96
+ const defaultOutputDir = external_node_path_default().resolve("outside", "default-output");
97
+ (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.resolveAgentExecutionWorkspace)(workspace, null, defaultOutputDir)).toBe(external_node_path_default().normalize(defaultOutputDir));
98
+ });
99
+ (0, external_vitest_namespaceObject.it)("falls back to workspace when neither workdir nor default output dir exist", ()=>{
100
+ (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.resolveAgentExecutionWorkspace)(workspace, null, null)).toBe(external_node_path_default().normalize(workspace));
101
+ });
102
+ });
86
103
  (0, external_vitest_namespaceObject.describe)("toWorkspaceAliasVirtualPath", ()=>{
87
104
  (0, external_vitest_namespaceObject.it)("builds an alias path for absolute workspaces", ()=>{
88
105
  const absolute = external_node_path_default().resolve("outside", "session-output");
@@ -94,6 +111,39 @@ const agentInvoker_cjs_namespaceObject = require("../cli/core/agentInvoker.cjs")
94
111
  (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.toWorkspaceAliasVirtualPath)("relative/workspace")).toBeNull();
95
112
  });
96
113
  });
114
+ (0, external_vitest_namespaceObject.describe)("resolveAgentMemorySources", ()=>{
115
+ const tempDirs = [];
116
+ (0, external_vitest_namespaceObject.afterEach)(()=>{
117
+ for (const dir of tempDirs)(0, external_node_fs_namespaceObject.rmSync)(dir, {
118
+ recursive: true,
119
+ force: true
120
+ });
121
+ tempDirs.length = 0;
122
+ });
123
+ (0, external_vitest_namespaceObject.it)("returns /AGENTS.md when present in execution workspace", ()=>{
124
+ const workspace = (0, external_node_fs_namespaceObject.mkdtempSync)(external_node_path_default().join((0, external_node_os_namespaceObject.tmpdir)(), "wingman-memory-"));
125
+ tempDirs.push(workspace);
126
+ (0, external_node_fs_namespaceObject.writeFileSync)(external_node_path_default().join(workspace, "AGENTS.md"), "# Agent Memory");
127
+ (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.resolveAgentMemorySources)(workspace)).toEqual([
128
+ "/AGENTS.md"
129
+ ]);
130
+ });
131
+ (0, external_vitest_namespaceObject.it)("returns no sources when AGENTS.md is absent", ()=>{
132
+ const workspace = (0, external_node_fs_namespaceObject.mkdtempSync)(external_node_path_default().join((0, external_node_os_namespaceObject.tmpdir)(), "wingman-memory-"));
133
+ tempDirs.push(workspace);
134
+ (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.resolveAgentMemorySources)(workspace)).toEqual([]);
135
+ });
136
+ (0, external_vitest_namespaceObject.it)("ignores .deepagents/AGENTS.md when top-level AGENTS.md is missing", ()=>{
137
+ const workspace = (0, external_node_fs_namespaceObject.mkdtempSync)(external_node_path_default().join((0, external_node_os_namespaceObject.tmpdir)(), "wingman-memory-"));
138
+ tempDirs.push(workspace);
139
+ const deepagentsDir = external_node_path_default().join(workspace, ".deepagents");
140
+ (0, external_node_fs_namespaceObject.mkdirSync)(deepagentsDir, {
141
+ recursive: true
142
+ });
143
+ (0, external_node_fs_namespaceObject.writeFileSync)(external_node_path_default().join(deepagentsDir, "AGENTS.md"), "# Nested Memory");
144
+ (0, external_vitest_namespaceObject.expect)((0, agentInvoker_cjs_namespaceObject.resolveAgentMemorySources)(workspace)).toEqual([]);
145
+ });
146
+ });
97
147
  for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
98
148
  Object.defineProperty(exports, '__esModule', {
99
149
  value: true
@@ -1,6 +1,8 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
1
3
  import node_path from "node:path";
2
- import { describe, expect, it } from "vitest";
3
- import { OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, resolveExecutionWorkspace, resolveExternalOutputMount, toWorkspaceAliasVirtualPath } from "../cli/core/agentInvoker.js";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, resolveAgentExecutionWorkspace, resolveAgentMemorySources, resolveExecutionWorkspace, resolveExternalOutputMount, toWorkspaceAliasVirtualPath } from "../cli/core/agentInvoker.js";
4
6
  describe("resolveExternalOutputMount", ()=>{
5
7
  const workspace = node_path.resolve("workspace");
6
8
  it("mounts external workdir paths", ()=>{
@@ -59,6 +61,21 @@ describe("resolveExecutionWorkspace", ()=>{
59
61
  expect(resolveExecutionWorkspace(workspace, absolute)).toBe(node_path.normalize(absolute));
60
62
  });
61
63
  });
64
+ describe("resolveAgentExecutionWorkspace", ()=>{
65
+ const workspace = node_path.resolve("workspace");
66
+ it("prefers explicit workdir over default output dir", ()=>{
67
+ const workdir = node_path.resolve("outside", "session-output");
68
+ const defaultOutputDir = node_path.resolve("outside", "default-output");
69
+ expect(resolveAgentExecutionWorkspace(workspace, workdir, defaultOutputDir)).toBe(node_path.normalize(workdir));
70
+ });
71
+ it("uses default output dir when session workdir is unset", ()=>{
72
+ const defaultOutputDir = node_path.resolve("outside", "default-output");
73
+ expect(resolveAgentExecutionWorkspace(workspace, null, defaultOutputDir)).toBe(node_path.normalize(defaultOutputDir));
74
+ });
75
+ it("falls back to workspace when neither workdir nor default output dir exist", ()=>{
76
+ expect(resolveAgentExecutionWorkspace(workspace, null, null)).toBe(node_path.normalize(workspace));
77
+ });
78
+ });
62
79
  describe("toWorkspaceAliasVirtualPath", ()=>{
63
80
  it("builds an alias path for absolute workspaces", ()=>{
64
81
  const absolute = node_path.resolve("outside", "session-output");
@@ -70,3 +87,36 @@ describe("toWorkspaceAliasVirtualPath", ()=>{
70
87
  expect(toWorkspaceAliasVirtualPath("relative/workspace")).toBeNull();
71
88
  });
72
89
  });
90
+ describe("resolveAgentMemorySources", ()=>{
91
+ const tempDirs = [];
92
+ afterEach(()=>{
93
+ for (const dir of tempDirs)rmSync(dir, {
94
+ recursive: true,
95
+ force: true
96
+ });
97
+ tempDirs.length = 0;
98
+ });
99
+ it("returns /AGENTS.md when present in execution workspace", ()=>{
100
+ const workspace = mkdtempSync(node_path.join(tmpdir(), "wingman-memory-"));
101
+ tempDirs.push(workspace);
102
+ writeFileSync(node_path.join(workspace, "AGENTS.md"), "# Agent Memory");
103
+ expect(resolveAgentMemorySources(workspace)).toEqual([
104
+ "/AGENTS.md"
105
+ ]);
106
+ });
107
+ it("returns no sources when AGENTS.md is absent", ()=>{
108
+ const workspace = mkdtempSync(node_path.join(tmpdir(), "wingman-memory-"));
109
+ tempDirs.push(workspace);
110
+ expect(resolveAgentMemorySources(workspace)).toEqual([]);
111
+ });
112
+ it("ignores .deepagents/AGENTS.md when top-level AGENTS.md is missing", ()=>{
113
+ const workspace = mkdtempSync(node_path.join(tmpdir(), "wingman-memory-"));
114
+ tempDirs.push(workspace);
115
+ const deepagentsDir = node_path.join(workspace, ".deepagents");
116
+ mkdirSync(deepagentsDir, {
117
+ recursive: true
118
+ });
119
+ writeFileSync(node_path.join(deepagentsDir, "AGENTS.md"), "# Nested Memory");
120
+ expect(resolveAgentMemorySources(workspace)).toEqual([]);
121
+ });
122
+ });
@@ -43,6 +43,7 @@ const init_cjs_namespaceObject = require("../cli/commands/init.cjs");
43
43
  (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(codingAgentPath)).toBe(true);
44
44
  const codingPrompt = (0, external_node_fs_namespaceObject.readFileSync)(codingAgentPath, "utf-8");
45
45
  (0, external_vitest_namespaceObject.expect)(codingPrompt).toContain("write_todos");
46
+ (0, external_vitest_namespaceObject.expect)(codingPrompt).toContain("read_todos");
46
47
  (0, external_vitest_namespaceObject.expect)(codingPrompt).not.toContain("update_plan");
47
48
  (0, external_vitest_namespaceObject.expect)(codingPrompt).not.toContain("subAgents:");
48
49
  (0, external_vitest_namespaceObject.expect)(codingPrompt).toContain("Do not delegate coding work to subagents");
@@ -55,6 +56,7 @@ const init_cjs_namespaceObject = require("../cli/commands/init.cjs");
55
56
  (0, external_vitest_namespaceObject.expect)(codingV2Prompt).toContain("promptFile: ./implementor.md");
56
57
  (0, external_vitest_namespaceObject.expect)(codingV2Prompt).toContain("`task` tool");
57
58
  (0, external_vitest_namespaceObject.expect)(codingV2Prompt).toContain("write_todos");
59
+ (0, external_vitest_namespaceObject.expect)(codingV2Prompt).toContain("read_todos");
58
60
  const codingV2ImplementorPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "coding-v2", "implementor.md");
59
61
  const codingV2PlannerPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "coding-v2", "planner.md");
60
62
  const codingV2ReviewerPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "coding-v2", "reviewer.md");
@@ -63,6 +65,40 @@ const init_cjs_namespaceObject = require("../cli/commands/init.cjs");
63
65
  (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(codingV2PlannerPath)).toBe(false);
64
66
  (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(codingV2ReviewerPath)).toBe(false);
65
67
  (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(codingV2ResearcherPath)).toBe(false);
68
+ const gameDevAgentPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "game-dev", "agent.md");
69
+ (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(gameDevAgentPath)).toBe(true);
70
+ const gameDevPrompt = (0, external_node_fs_namespaceObject.readFileSync)(gameDevAgentPath, "utf-8");
71
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("name: game-dev");
72
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("subAgents:");
73
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("name: art-generation");
74
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("promptFile: ./art-generation.md");
75
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("name: asset-refinement");
76
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("promptFile: ./asset-refinement.md");
77
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("name: planning-idea");
78
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("promptFile: ./planning-idea.md");
79
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("name: ui-specialist");
80
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("promptFile: ./ui-specialist.md");
81
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("write_todos");
82
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("read_todos");
83
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("UV-aware texture planning");
84
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("MeshStandardMaterial");
85
+ (0, external_vitest_namespaceObject.expect)(gameDevPrompt).toContain("uv`/`uv2");
86
+ const gameDevArtGenerationPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "game-dev", "art-generation.md");
87
+ const gameDevAssetRefinementPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "game-dev", "asset-refinement.md");
88
+ const gameDevPlanningIdeaPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "game-dev", "planning-idea.md");
89
+ const gameDevUiSpecialistPath = (0, external_node_path_namespaceObject.join)(workspace, ".wingman", "agents", "game-dev", "ui-specialist.md");
90
+ (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(gameDevArtGenerationPath)).toBe(true);
91
+ (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(gameDevAssetRefinementPath)).toBe(true);
92
+ (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(gameDevPlanningIdeaPath)).toBe(true);
93
+ (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(gameDevUiSpecialistPath)).toBe(true);
94
+ const gameDevArtGenerationPrompt = (0, external_node_fs_namespaceObject.readFileSync)(gameDevArtGenerationPath, "utf-8");
95
+ (0, external_vitest_namespaceObject.expect)(gameDevArtGenerationPrompt).toContain("UV set(s) or UDIM tiles");
96
+ (0, external_vitest_namespaceObject.expect)(gameDevArtGenerationPrompt).toContain("texel density targets");
97
+ (0, external_vitest_namespaceObject.expect)(gameDevArtGenerationPrompt).toContain("Texture-to-geometry mapping notes");
98
+ (0, external_vitest_namespaceObject.expect)(gameDevArtGenerationPrompt).toContain("material slot");
99
+ (0, external_vitest_namespaceObject.expect)(gameDevArtGenerationPrompt).toContain("need `uv`, and `aoMap`");
100
+ (0, external_vitest_namespaceObject.expect)(gameDevArtGenerationPrompt).toContain("flipY = false");
101
+ (0, external_vitest_namespaceObject.expect)(gameDevArtGenerationPrompt).toContain("RepeatWrapping");
66
102
  });
67
103
  (0, external_vitest_namespaceObject.it)("merges existing config when --merge is set", async ()=>{
68
104
  const configDir = (0, external_node_path_namespaceObject.join)(workspace, ".wingman");