@wingman-ai/gateway 0.2.4 → 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 (95) 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/providers/codex.cjs +230 -37
  45. package/dist/providers/codex.d.ts +2 -0
  46. package/dist/providers/codex.js +231 -38
  47. package/dist/tests/agentInvokerSummarization.test.cjs +56 -37
  48. package/dist/tests/agentInvokerSummarization.test.js +58 -39
  49. package/dist/tests/agentInvokerWorkdir.test.cjs +50 -0
  50. package/dist/tests/agentInvokerWorkdir.test.js +52 -2
  51. package/dist/tests/cli-init.test.cjs +36 -0
  52. package/dist/tests/cli-init.test.js +36 -0
  53. package/dist/tests/codex-provider.test.cjs +173 -0
  54. package/dist/tests/codex-provider.test.js +174 -1
  55. package/dist/tests/falRuntime.test.cjs +78 -0
  56. package/dist/tests/falRuntime.test.d.ts +1 -0
  57. package/dist/tests/falRuntime.test.js +72 -0
  58. package/dist/tests/falSummary.test.cjs +51 -0
  59. package/dist/tests/falSummary.test.d.ts +1 -0
  60. package/dist/tests/falSummary.test.js +45 -0
  61. package/dist/tests/gateway.test.cjs +109 -1
  62. package/dist/tests/gateway.test.js +109 -1
  63. package/dist/tests/imagePersistence.test.cjs +143 -0
  64. package/dist/tests/imagePersistence.test.d.ts +1 -0
  65. package/dist/tests/imagePersistence.test.js +137 -0
  66. package/dist/tests/sessionMessageAttachments.test.cjs +30 -0
  67. package/dist/tests/sessionMessageAttachments.test.js +30 -0
  68. package/dist/tests/sessionStateMessages.test.cjs +126 -0
  69. package/dist/tests/sessionStateMessages.test.js +126 -0
  70. package/dist/tests/sessions-api.test.cjs +117 -3
  71. package/dist/tests/sessions-api.test.js +118 -4
  72. package/dist/tests/terminalProbe.test.cjs +45 -0
  73. package/dist/tests/terminalProbe.test.d.ts +1 -0
  74. package/dist/tests/terminalProbe.test.js +39 -0
  75. package/dist/tests/terminalProbeAuth.test.cjs +85 -0
  76. package/dist/tests/terminalProbeAuth.test.d.ts +1 -0
  77. package/dist/tests/terminalProbeAuth.test.js +79 -0
  78. package/dist/tools/fal/runtime.cjs +103 -0
  79. package/dist/tools/fal/runtime.d.ts +10 -0
  80. package/dist/tools/fal/runtime.js +60 -0
  81. package/dist/tools/fal/summary.cjs +78 -0
  82. package/dist/tools/fal/summary.d.ts +22 -0
  83. package/dist/tools/fal/summary.js +41 -0
  84. package/dist/tools/mcp-fal-ai.cjs +1041 -0
  85. package/dist/tools/mcp-fal-ai.d.ts +1 -0
  86. package/dist/tools/mcp-fal-ai.js +1025 -0
  87. package/dist/types/mcp.cjs +2 -0
  88. package/dist/types/mcp.d.ts +8 -0
  89. package/dist/types/mcp.js +3 -1
  90. package/dist/webui/assets/index-0nUBsUUq.js +278 -0
  91. package/dist/webui/assets/index-kk7OrD-G.css +11 -0
  92. package/dist/webui/index.html +2 -2
  93. package/package.json +16 -13
  94. package/dist/webui/assets/index-DVWQluit.css +0 -11
  95. package/dist/webui/assets/index-Dlyzwalc.js +0 -270
@@ -1,9 +1,12 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { createLogger } from "../logger.js";
5
5
  const CODEX_HOME_ENV = "CODEX_HOME";
6
6
  const CODEX_AUTH_FILE = "auth.json";
7
+ const CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV = "CODEX_REFRESH_TOKEN_URL_OVERRIDE";
8
+ const DEFAULT_CODEX_REFRESH_TOKEN_URL = "https://auth.openai.com/oauth/token";
9
+ const TOKEN_REFRESH_BUFFER_MS = 300000;
7
10
  const DEFAULT_CODEX_INSTRUCTIONS = "You are Wingman, a coding assistant. Follow the user's request exactly and keep tool usage focused.";
8
11
  const logger = createLogger();
9
12
  function getCodexAuthPath() {
@@ -13,53 +16,77 @@ function getCodexAuthPath() {
13
16
  }
14
17
  function resolveCodexAuthFromFile() {
15
18
  const authPath = getCodexAuthPath();
16
- if (!existsSync(authPath)) return {
19
+ const root = readCodexAuthRoot(authPath);
20
+ if (!root) return {
21
+ authPath
22
+ };
23
+ const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
24
+ const accessToken = firstNonEmptyString([
25
+ tokens?.access_token,
26
+ root.access_token
27
+ ]);
28
+ const refreshToken = firstNonEmptyString([
29
+ tokens?.refresh_token,
30
+ root.refresh_token
31
+ ]);
32
+ const idToken = firstNonEmptyString([
33
+ tokens?.id_token,
34
+ root.id_token
35
+ ]);
36
+ const accountId = firstNonEmptyString([
37
+ tokens?.account_id,
38
+ root.account_id,
39
+ extractAccountIdFromIdToken(idToken)
40
+ ]);
41
+ return {
42
+ accessToken,
43
+ refreshToken,
44
+ idToken,
45
+ accountId,
17
46
  authPath
18
47
  };
19
- try {
20
- const parsed = JSON.parse(readFileSync(authPath, "utf-8"));
21
- if (!parsed || "object" != typeof parsed) return {
22
- authPath
23
- };
24
- const root = parsed;
25
- const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
26
- const accessToken = firstNonEmptyString([
27
- tokens?.access_token,
28
- root.access_token
29
- ]);
30
- const accountId = firstNonEmptyString([
31
- tokens?.account_id,
32
- root.account_id
33
- ]);
34
- return {
35
- accessToken,
36
- accountId,
37
- authPath
38
- };
39
- } catch {
40
- return {
41
- authPath
42
- };
43
- }
44
48
  }
45
49
  function createCodexFetch(options = {}) {
46
50
  const baseFetch = options.baseFetch || globalThis.fetch.bind(globalThis);
47
51
  return async (input, init)=>{
48
- const codexAuth = resolveCodexAuthFromFile();
49
- const accessToken = codexAuth.accessToken || options.fallbackToken;
50
- const accountId = codexAuth.accountId || options.fallbackAccountId;
52
+ let codexAuth = await maybeRefreshCodexAuth({
53
+ authState: resolveCodexAuthFromFile(),
54
+ baseFetch
55
+ });
56
+ let accessToken = codexAuth.accessToken || options.fallbackToken;
57
+ let accountId = codexAuth.accountId || options.fallbackAccountId;
51
58
  if (!accessToken) throw new Error("Codex credentials missing. Run `codex login` or set CODEX_ACCESS_TOKEN.");
52
- const headers = new Headers(init?.headers || {});
53
- headers.delete("authorization");
54
- headers.delete("x-api-key");
55
- headers.set("Authorization", `Bearer ${accessToken}`);
56
- if (accountId) headers.set("ChatGPT-Account-ID", accountId);
57
59
  const body = withCodexRequestDefaults(init?.body);
58
- const response = await baseFetch(input, {
59
- ...init,
60
- headers,
60
+ let response = await dispatchCodexRequest({
61
+ input,
62
+ init,
63
+ baseFetch,
64
+ accessToken,
65
+ accountId,
61
66
  body
62
67
  });
68
+ if ((401 === response.status || 403 === response.status) && canRetryCodexRequest(body) && codexAuth.refreshToken) {
69
+ const refreshed = await maybeRefreshCodexAuth({
70
+ authState: codexAuth,
71
+ baseFetch,
72
+ force: true
73
+ });
74
+ const refreshedAccessToken = refreshed.accessToken || options.fallbackToken;
75
+ const refreshedAccountId = refreshed.accountId || options.fallbackAccountId;
76
+ if (refreshedAccessToken && refreshedAccessToken !== accessToken) {
77
+ codexAuth = refreshed;
78
+ accessToken = refreshedAccessToken;
79
+ accountId = refreshedAccountId;
80
+ response = await dispatchCodexRequest({
81
+ input,
82
+ init,
83
+ baseFetch,
84
+ accessToken,
85
+ accountId,
86
+ body
87
+ });
88
+ }
89
+ }
63
90
  if (!response.ok) {
64
91
  let responseBody = "";
65
92
  try {
@@ -75,6 +102,172 @@ function createCodexFetch(options = {}) {
75
102
  return response;
76
103
  };
77
104
  }
105
+ async function dispatchCodexRequest(input) {
106
+ const headers = new Headers(input.init?.headers || {});
107
+ headers.delete("authorization");
108
+ headers.delete("x-api-key");
109
+ headers.set("Authorization", `Bearer ${input.accessToken}`);
110
+ if (input.accountId) headers.set("ChatGPT-Account-ID", input.accountId);
111
+ return input.baseFetch(input.input, {
112
+ ...input.init,
113
+ headers,
114
+ body: input.body
115
+ });
116
+ }
117
+ function canRetryCodexRequest(body) {
118
+ return null == body || "string" == typeof body || body instanceof URLSearchParams;
119
+ }
120
+ async function maybeRefreshCodexAuth(input) {
121
+ const { authState, baseFetch, force = false } = input;
122
+ if (!authState.refreshToken) return authState;
123
+ const shouldRefresh = force || !authState.accessToken || isTokenExpiredOrExpiring(authState.accessToken);
124
+ if (!shouldRefresh) return authState;
125
+ try {
126
+ const refreshed = await refreshCodexAuthToken(authState, baseFetch);
127
+ if (refreshed) return refreshed;
128
+ } catch (error) {
129
+ logger.warn("Failed to refresh Codex token", {
130
+ error: error instanceof Error ? error.message : String(error)
131
+ });
132
+ }
133
+ return authState;
134
+ }
135
+ async function refreshCodexAuthToken(authState, baseFetch) {
136
+ const refreshToken = authState.refreshToken;
137
+ if (!refreshToken) return;
138
+ const clientId = extractClientIdForRefresh(authState);
139
+ const tokenUrl = resolveCodexRefreshTokenUrl();
140
+ const form = new URLSearchParams({
141
+ grant_type: "refresh_token",
142
+ refresh_token: refreshToken
143
+ });
144
+ if (clientId) form.set("client_id", clientId);
145
+ const response = await baseFetch(tokenUrl, {
146
+ method: "POST",
147
+ headers: {
148
+ Accept: "application/json",
149
+ "Content-Type": "application/x-www-form-urlencoded"
150
+ },
151
+ body: form.toString()
152
+ });
153
+ if (!response.ok) {
154
+ const preview = await readResponsePreview(response);
155
+ logger.warn("Codex token refresh failed", {
156
+ status: response.status,
157
+ statusText: response.statusText || null,
158
+ bodyPreview: preview || null
159
+ });
160
+ return;
161
+ }
162
+ const payload = await response.json();
163
+ const accessToken = firstNonEmptyString([
164
+ payload.access_token
165
+ ]);
166
+ if (!accessToken) return void logger.warn("Codex token refresh failed: missing access_token");
167
+ const idToken = firstNonEmptyString([
168
+ payload.id_token,
169
+ authState.idToken
170
+ ]);
171
+ const refreshed = {
172
+ accessToken,
173
+ refreshToken: firstNonEmptyString([
174
+ payload.refresh_token,
175
+ authState.refreshToken
176
+ ]),
177
+ idToken,
178
+ accountId: firstNonEmptyString([
179
+ extractAccountIdFromIdToken(idToken),
180
+ authState.accountId
181
+ ])
182
+ };
183
+ persistCodexAuthUpdate(authState.authPath, refreshed);
184
+ return resolveCodexAuthFromFile();
185
+ }
186
+ async function readResponsePreview(response) {
187
+ try {
188
+ const text = await response.text();
189
+ return text.trim().slice(0, 1200);
190
+ } catch {
191
+ return "";
192
+ }
193
+ }
194
+ function persistCodexAuthUpdate(authPath, updated) {
195
+ const root = readCodexAuthRoot(authPath) || {};
196
+ const existingTokens = root.tokens && "object" == typeof root.tokens && !Array.isArray(root.tokens) ? root.tokens : {};
197
+ const tokens = {
198
+ ...existingTokens,
199
+ access_token: updated.accessToken
200
+ };
201
+ if (updated.refreshToken) tokens.refresh_token = updated.refreshToken;
202
+ if (updated.idToken) tokens.id_token = updated.idToken;
203
+ if (updated.accountId) tokens.account_id = updated.accountId;
204
+ root.tokens = tokens;
205
+ root.last_refresh = new Date().toISOString();
206
+ writeFileSync(authPath, `${JSON.stringify(root, null, 2)}\n`, "utf-8");
207
+ }
208
+ function readCodexAuthRoot(authPath) {
209
+ if (!existsSync(authPath)) return;
210
+ try {
211
+ const parsed = JSON.parse(readFileSync(authPath, "utf-8"));
212
+ if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
213
+ return parsed;
214
+ } catch {
215
+ return;
216
+ }
217
+ }
218
+ function resolveCodexRefreshTokenUrl() {
219
+ const override = process.env[CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV];
220
+ if (override?.trim()) return override.trim();
221
+ return DEFAULT_CODEX_REFRESH_TOKEN_URL;
222
+ }
223
+ function extractClientIdForRefresh(authState) {
224
+ const accessTokenClaims = parseJwtPayload(authState.accessToken);
225
+ const accessTokenClientId = accessTokenClaims && "string" == typeof accessTokenClaims.client_id ? accessTokenClaims.client_id : void 0;
226
+ if (accessTokenClientId?.trim()) return accessTokenClientId.trim();
227
+ const idTokenClaims = parseJwtPayload(authState.idToken);
228
+ if (!idTokenClaims) return;
229
+ const aud = idTokenClaims.aud;
230
+ if ("string" == typeof aud && aud.trim()) return aud.trim();
231
+ if (Array.isArray(aud)) {
232
+ for (const value of aud)if ("string" == typeof value && value.trim()) return value.trim();
233
+ }
234
+ }
235
+ function isTokenExpiredOrExpiring(token) {
236
+ const expiryMs = extractTokenExpiryMs(token);
237
+ if (!expiryMs) return false;
238
+ return expiryMs <= Date.now() + TOKEN_REFRESH_BUFFER_MS;
239
+ }
240
+ function extractTokenExpiryMs(token) {
241
+ const payload = parseJwtPayload(token);
242
+ if (!payload || "number" != typeof payload.exp) return;
243
+ return 1000 * payload.exp;
244
+ }
245
+ function extractAccountIdFromIdToken(idToken) {
246
+ const payload = parseJwtPayload(idToken);
247
+ if (!payload) return;
248
+ const nested = payload["https://api.openai.com/auth"];
249
+ if (nested && "object" == typeof nested && !Array.isArray(nested)) {
250
+ const accountId = nested.chatgpt_account_id;
251
+ if ("string" == typeof accountId && accountId.trim()) return accountId.trim();
252
+ }
253
+ const direct = payload.chatgpt_account_id;
254
+ if ("string" == typeof direct && direct.trim()) return direct.trim();
255
+ }
256
+ function parseJwtPayload(token) {
257
+ if (!token) return;
258
+ const parts = token.split(".");
259
+ if (3 !== parts.length) return;
260
+ try {
261
+ const payload = parts[1];
262
+ const normalized = payload + "=".repeat((4 - payload.length % 4) % 4);
263
+ const decoded = Buffer.from(normalized, "base64url").toString("utf-8");
264
+ const parsed = JSON.parse(decoded);
265
+ if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
266
+ return parsed;
267
+ } catch {
268
+ return;
269
+ }
270
+ }
78
271
  function withCodexRequestDefaults(body) {
79
272
  if ("string" != typeof body || !body.trim()) return body;
80
273
  try {
@@ -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
+ });