@shawnstack/quickforge 1.3.18 → 1.3.20

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 (133) hide show
  1. package/README.md +10 -10
  2. package/bin/quickforge.mjs +258 -49
  3. package/dist/assets/anthropic-Bj3HAZgj.js +39 -0
  4. package/dist/assets/azure-openai-responses-IdZZrSrI.js +1 -0
  5. package/dist/assets/github-copilot-headers-CMb2BbzT.js +1 -0
  6. package/dist/assets/google-Brt_lS1J.js +1 -0
  7. package/dist/assets/{google-shared-XhYUKiGZ.js → google-shared-CLc4ziON.js} +3 -3
  8. package/dist/assets/google-vertex-B6HsoZ34.js +1 -0
  9. package/dist/assets/{index-Dm7aEWvT.js → index-D0CVLdX_.js} +525 -489
  10. package/dist/assets/index-D0W9hAl_.css +3 -0
  11. package/dist/assets/{mistral-DxhS4Wkn.js → mistral-CenXqwPz.js} +3 -3
  12. package/dist/assets/openai-codex-responses-D9ffGwbj.js +7 -0
  13. package/dist/assets/openai-completions-eWdeSGBG.js +5 -0
  14. package/dist/assets/openai-responses-Cavpmjeu.js +1 -0
  15. package/dist/assets/{openai-responses-shared-f_P3e1nz.js → openai-responses-shared-DF3ZGaUx.js} +5 -3
  16. package/dist/assets/transform-messages-CmnxG9RB.js +1 -0
  17. package/dist/index.html +2 -2
  18. package/node_modules/@anthropic-ai/sdk/CHANGELOG.md +34 -0
  19. package/node_modules/@anthropic-ai/sdk/bin/migration-config.json +185 -0
  20. package/node_modules/@anthropic-ai/sdk/package.json +1 -1
  21. package/node_modules/@anthropic-ai/sdk/resources/beta/beta.js +4 -0
  22. package/node_modules/@anthropic-ai/sdk/resources/beta/beta.mjs +4 -0
  23. package/node_modules/@anthropic-ai/sdk/resources/beta/files.js +5 -5
  24. package/node_modules/@anthropic-ai/sdk/resources/beta/files.mjs +5 -5
  25. package/node_modules/@anthropic-ai/sdk/resources/beta/index.js +11 -9
  26. package/node_modules/@anthropic-ai/sdk/resources/beta/index.mjs +1 -0
  27. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/index.js +11 -0
  28. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/index.mjs +5 -0
  29. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memories.js +130 -0
  30. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memories.mjs +126 -0
  31. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-stores.js +145 -0
  32. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-stores.mjs +140 -0
  33. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-versions.js +81 -0
  34. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-versions.mjs +77 -0
  35. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores.js +6 -0
  36. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores.mjs +3 -0
  37. package/node_modules/@anthropic-ai/sdk/tools/memory/node.js +12 -5
  38. package/node_modules/@anthropic-ai/sdk/tools/memory/node.mjs +12 -5
  39. package/node_modules/@anthropic-ai/sdk/version.js +1 -1
  40. package/node_modules/@anthropic-ai/sdk/version.mjs +1 -1
  41. package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +5 -5
  42. package/node_modules/@aws-sdk/core/package.json +2 -2
  43. package/node_modules/@aws-sdk/credential-provider-env/package.json +2 -2
  44. package/node_modules/@aws-sdk/credential-provider-http/dist-cjs/fromHttp/fromHttp.js +12 -6
  45. package/node_modules/@aws-sdk/credential-provider-http/dist-es/fromHttp/fromHttp.js +12 -6
  46. package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -2
  47. package/node_modules/@aws-sdk/credential-provider-ini/package.json +9 -9
  48. package/node_modules/@aws-sdk/credential-provider-login/package.json +3 -3
  49. package/node_modules/@aws-sdk/credential-provider-node/package.json +7 -7
  50. package/node_modules/@aws-sdk/credential-provider-process/package.json +2 -2
  51. package/node_modules/@aws-sdk/credential-provider-sso/package.json +4 -4
  52. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +3 -3
  53. package/node_modules/@aws-sdk/middleware-websocket/package.json +2 -2
  54. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
  55. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
  56. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
  57. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
  58. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
  59. package/node_modules/@aws-sdk/nested-clients/package.json +3 -3
  60. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +1 -2
  61. package/node_modules/@aws-sdk/token-providers/package.json +3 -3
  62. package/node_modules/@aws-sdk/xml-builder/package.json +2 -2
  63. package/node_modules/@mariozechner/pi-agent-core/README.md +14 -0
  64. package/node_modules/@mariozechner/pi-agent-core/dist/agent-loop.js +9 -0
  65. package/node_modules/@mariozechner/pi-agent-core/dist/agent.js +1 -1
  66. package/node_modules/@mariozechner/pi-agent-core/package.json +2 -2
  67. package/node_modules/@mariozechner/pi-ai/README.md +20 -31
  68. package/node_modules/@mariozechner/pi-ai/dist/env-api-keys.js +7 -0
  69. package/node_modules/@mariozechner/pi-ai/dist/index.js +2 -0
  70. package/node_modules/@mariozechner/pi-ai/dist/models.generated.js +2420 -1213
  71. package/node_modules/@mariozechner/pi-ai/dist/models.js +28 -20
  72. package/node_modules/@mariozechner/pi-ai/dist/providers/amazon-bedrock.js +11 -11
  73. package/node_modules/@mariozechner/pi-ai/dist/providers/anthropic.js +43 -26
  74. package/node_modules/@mariozechner/pi-ai/dist/providers/azure-openai-responses.js +12 -6
  75. package/node_modules/@mariozechner/pi-ai/dist/providers/cloudflare.js +10 -3
  76. package/node_modules/@mariozechner/pi-ai/dist/providers/google-shared.js +4 -13
  77. package/node_modules/@mariozechner/pi-ai/dist/providers/google-vertex.js +4 -3
  78. package/node_modules/@mariozechner/pi-ai/dist/providers/google.js +4 -3
  79. package/node_modules/@mariozechner/pi-ai/dist/providers/mistral.js +8 -7
  80. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-codex-responses.js +296 -41
  81. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-completions.js +169 -153
  82. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-responses-shared.js +14 -1
  83. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-responses.js +22 -8
  84. package/node_modules/@mariozechner/pi-ai/dist/providers/register-builtins.js +0 -18
  85. package/node_modules/@mariozechner/pi-ai/dist/providers/simple-options.js +1 -0
  86. package/node_modules/@mariozechner/pi-ai/dist/session-resources.js +22 -0
  87. package/node_modules/@mariozechner/pi-ai/dist/utils/diagnostics.js +25 -0
  88. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/index.js +0 -10
  89. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/openai-codex.js +25 -14
  90. package/node_modules/@mariozechner/pi-ai/dist/utils/overflow.js +14 -0
  91. package/node_modules/@mariozechner/pi-ai/package.json +2 -6
  92. package/package.json +3 -3
  93. package/server/agent-manager.mjs +279 -12
  94. package/server/auto-compaction.mjs +1 -2
  95. package/server/conversation-compaction.mjs +0 -5
  96. package/server/index.mjs +1 -0
  97. package/server/routes/static.mjs +1 -0
  98. package/server/routes/tools.mjs +3 -1
  99. package/server/session-utils.mjs +6 -1
  100. package/server/share-store.mjs +27 -4
  101. package/server/subagents.mjs +101 -0
  102. package/server/system-prompt.mjs +30 -1
  103. package/server/tools/definitions.mjs +18 -0
  104. package/server/tools/index.mjs +1013 -911
  105. package/dist/assets/anthropic-Ck2DxOfr.js +0 -39
  106. package/dist/assets/azure-openai-responses-DIoz5q4Z.js +0 -1
  107. package/dist/assets/github-copilot-headers-CrI0CIJ7.js +0 -1
  108. package/dist/assets/google-Dau-4ve_.js +0 -1
  109. package/dist/assets/google-gemini-cli-DttMmbGb.js +0 -2
  110. package/dist/assets/google-vertex-BeukMl44.js +0 -1
  111. package/dist/assets/index-DgJVElbv.css +0 -3
  112. package/dist/assets/openai-codex-responses-X3sTzNAa.js +0 -7
  113. package/dist/assets/openai-completions-CRB9Vm0w.js +0 -5
  114. package/dist/assets/openai-responses-DXluu3oi.js +0 -1
  115. package/dist/assets/transform-messages-CV4kCtBB.js +0 -1
  116. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/LICENSE +0 -201
  117. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/README.md +0 -62
  118. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-cjs/index.js +0 -156
  119. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/constants.js +0 -2
  120. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromEnvSigningName.js +0 -16
  121. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromSso.js +0 -80
  122. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromStatic.js +0 -8
  123. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getNewSsoOidcToken.js +0 -11
  124. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getSsoOidcClient.js +0 -10
  125. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/index.js +0 -4
  126. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/nodeProvider.js +0 -5
  127. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenExpiry.js +0 -7
  128. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenKey.js +0 -7
  129. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/writeSSOTokenToFile.js +0 -8
  130. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +0 -69
  131. package/node_modules/@mariozechner/pi-ai/dist/providers/google-gemini-cli.js +0 -779
  132. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/google-antigravity.js +0 -377
  133. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/google-gemini-cli.js +0 -482
@@ -0,0 +1,25 @@
1
+ export function formatThrownValue(value) {
2
+ if (value instanceof Error)
3
+ return value.message || value.name;
4
+ if (typeof value === "string")
5
+ return value;
6
+ return String(value);
7
+ }
8
+ export function extractDiagnosticError(error) {
9
+ if (!(error instanceof Error))
10
+ return { name: "ThrownValue", message: formatThrownValue(error) };
11
+ const code = error.code;
12
+ return {
13
+ name: error.name || undefined,
14
+ message: error.message || error.name,
15
+ stack: error.stack,
16
+ code: typeof code === "string" || typeof code === "number" ? code : undefined,
17
+ };
18
+ }
19
+ export function createAssistantMessageDiagnostic(type, error, details) {
20
+ return { type, timestamp: Date.now(), error: extractDiagnosticError(error), details };
21
+ }
22
+ export function appendAssistantMessageDiagnostic(message, diagnostic) {
23
+ message.diagnostics = [...(message.diagnostics ?? []), diagnostic];
24
+ }
25
+ //# sourceMappingURL=diagnostics.js.map
@@ -5,17 +5,11 @@
5
5
  * for OAuth-based providers:
6
6
  * - Anthropic (Claude Pro/Max)
7
7
  * - GitHub Copilot
8
- * - Google Cloud Code Assist (Gemini CLI)
9
- * - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud)
10
8
  */
11
9
  // Anthropic
12
10
  export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
13
11
  // GitHub Copilot
14
12
  export { getGitHubCopilotBaseUrl, githubCopilotOAuthProvider, loginGitHubCopilot, normalizeDomain, refreshGitHubCopilotToken, } from "./github-copilot.js";
15
- // Google Antigravity
16
- export { antigravityOAuthProvider, loginAntigravity, refreshAntigravityToken } from "./google-antigravity.js";
17
- // Google Gemini CLI
18
- export { geminiCliOAuthProvider, loginGeminiCli, refreshGoogleCloudToken } from "./google-gemini-cli.js";
19
13
  // OpenAI Codex (ChatGPT OAuth)
20
14
  export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.js";
21
15
  export * from "./types.js";
@@ -24,14 +18,10 @@ export * from "./types.js";
24
18
  // ============================================================================
25
19
  import { anthropicOAuthProvider } from "./anthropic.js";
26
20
  import { githubCopilotOAuthProvider } from "./github-copilot.js";
27
- import { antigravityOAuthProvider } from "./google-antigravity.js";
28
- import { geminiCliOAuthProvider } from "./google-gemini-cli.js";
29
21
  import { openaiCodexOAuthProvider } from "./openai-codex.js";
30
22
  const BUILT_IN_OAUTH_PROVIDERS = [
31
23
  anthropicOAuthProvider,
32
24
  githubCopilotOAuthProvider,
33
- geminiCliOAuthProvider,
34
- antigravityOAuthProvider,
35
25
  openaiCodexOAuthProvider,
36
26
  ];
37
27
  const oauthProviderRegistry = new Map(BUILT_IN_OAUTH_PROVIDERS.map((provider) => [provider.id, provider]));
@@ -84,13 +84,18 @@ async function exchangeAuthorizationCode(code, verifier, redirectUri = REDIRECT_
84
84
  });
85
85
  if (!response.ok) {
86
86
  const text = await response.text().catch(() => "");
87
- console.error("[openai-codex] code->token failed:", response.status, text);
88
- return { type: "failed" };
87
+ return {
88
+ type: "failed",
89
+ status: response.status,
90
+ message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,
91
+ };
89
92
  }
90
93
  const json = (await response.json());
91
94
  if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
92
- console.error("[openai-codex] token response missing fields:", json);
93
- return { type: "failed" };
95
+ return {
96
+ type: "failed",
97
+ message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,
98
+ };
94
99
  }
95
100
  return {
96
101
  type: "success",
@@ -112,13 +117,18 @@ async function refreshAccessToken(refreshToken) {
112
117
  });
113
118
  if (!response.ok) {
114
119
  const text = await response.text().catch(() => "");
115
- console.error("[openai-codex] Token refresh failed:", response.status, text);
116
- return { type: "failed" };
120
+ return {
121
+ type: "failed",
122
+ status: response.status,
123
+ message: `OpenAI Codex token refresh failed (${response.status}): ${text || response.statusText}`,
124
+ };
117
125
  }
118
126
  const json = (await response.json());
119
127
  if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
120
- console.error("[openai-codex] Token refresh response missing fields:", json);
121
- return { type: "failed" };
128
+ return {
129
+ type: "failed",
130
+ message: `OpenAI Codex token refresh response missing fields: ${JSON.stringify(json)}`,
131
+ };
122
132
  }
123
133
  return {
124
134
  type: "success",
@@ -128,8 +138,10 @@ async function refreshAccessToken(refreshToken) {
128
138
  };
129
139
  }
130
140
  catch (error) {
131
- console.error("[openai-codex] Token refresh error:", error);
132
- return { type: "failed" };
141
+ return {
142
+ type: "failed",
143
+ message: `OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`,
144
+ };
133
145
  }
134
146
  }
135
147
  async function createAuthorizationFlow(originator = "pi") {
@@ -206,8 +218,7 @@ function startLocalOAuthServer(state) {
206
218
  waitForCode: () => waitForCodePromise,
207
219
  });
208
220
  })
209
- .on("error", (err) => {
210
- console.error(`[openai-codex] Failed to bind http://${CALLBACK_HOST}:1455 (`, err.code, ") Falling back to manual paste.");
221
+ .on("error", (_err) => {
211
222
  settleWait?.(null);
212
223
  resolve({
213
224
  close: () => {
@@ -316,7 +327,7 @@ export async function loginOpenAICodex(options) {
316
327
  }
317
328
  const tokenResult = await exchangeAuthorizationCode(code, verifier);
318
329
  if (tokenResult.type !== "success") {
319
- throw new Error("Token exchange failed");
330
+ throw new Error(tokenResult.message);
320
331
  }
321
332
  const accountId = getAccountId(tokenResult.access);
322
333
  if (!accountId) {
@@ -339,7 +350,7 @@ export async function loginOpenAICodex(options) {
339
350
  export async function refreshOpenAICodexToken(refreshToken) {
340
351
  const result = await refreshAccessToken(refreshToken);
341
352
  if (result.type !== "success") {
342
- throw new Error("Failed to refresh OpenAI Codex token");
353
+ throw new Error(result.message);
343
354
  }
344
355
  const accountId = getAccountId(result.access);
345
356
  if (!accountId) {
@@ -21,6 +21,9 @@
21
21
  * - Cerebras: "400/413 status code (no body)"
22
22
  * - Mistral: "Prompt contains X tokens ... too large for model with Y maximum context length"
23
23
  * - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow
24
+ * - Xiaomi MiMo: Truncates input to fill contextWindow exactly, then returns finish_reason "length"
25
+ * with output=0 (no room left to generate). Detected via stopReason "length" + zero output +
26
+ * input filling the context window.
24
27
  * - Ollama: Some deployments truncate silently, others return errors like "prompt too long; exceeded max context length by X tokens"
25
28
  */
26
29
  const OVERFLOW_PATTERNS = [
@@ -86,6 +89,8 @@ const NON_OVERFLOW_PATTERNS = [
86
89
  * **Unreliable detection:**
87
90
  * - z.ai: Sometimes accepts overflow silently (detectable via usage.input > contextWindow),
88
91
  * sometimes returns rate limit errors. Pass contextWindow param to detect silent overflow.
92
+ * - Xiaomi MiMo: Truncates input to fit contextWindow then returns stopReason "length" with
93
+ * output=0. Pass contextWindow param to detect via the "filled context + zero output" signal.
89
94
  * - Ollama: May truncate input silently for some setups, but may also return explicit
90
95
  * overflow errors that match the patterns above. Silent truncation still cannot be
91
96
  * detected here because we do not know the expected token count.
@@ -121,6 +126,15 @@ export function isContextOverflow(message, contextWindow) {
121
126
  return true;
122
127
  }
123
128
  }
129
+ // Case 3: Length-stop overflow (Xiaomi MiMo style) - server truncates oversized input
130
+ // to fit the context window, leaving no room for output. Returns stopReason "length"
131
+ // with output=0 and input+cacheRead filling the context window.
132
+ if (contextWindow && message.stopReason === "length" && message.usage.output === 0) {
133
+ const inputTokens = message.usage.input + message.usage.cacheRead;
134
+ if (inputTokens >= contextWindow * 0.99) {
135
+ return true;
136
+ }
137
+ }
124
138
  return false;
125
139
  }
126
140
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-ai",
3
- "version": "0.70.6",
3
+ "version": "0.73.1",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,10 +22,6 @@
22
22
  "types": "./dist/providers/google.d.ts",
23
23
  "import": "./dist/providers/google.js"
24
24
  },
25
- "./google-gemini-cli": {
26
- "types": "./dist/providers/google-gemini-cli.d.ts",
27
- "import": "./dist/providers/google-gemini-cli.js"
28
- },
29
25
  "./google-vertex": {
30
26
  "types": "./dist/providers/google-vertex.d.ts",
31
27
  "import": "./dist/providers/google-vertex.js"
@@ -72,7 +68,7 @@
72
68
  "prepublishOnly": "npm run clean && npm run build"
73
69
  },
74
70
  "dependencies": {
75
- "@anthropic-ai/sdk": "^0.90.0",
71
+ "@anthropic-ai/sdk": "^0.91.1",
76
72
  "@aws-sdk/client-bedrock-runtime": "^3.1030.0",
77
73
  "@google/genai": "^1.40.0",
78
74
  "@mistralai/mistralai": "^2.2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shawnstack/quickforge",
3
- "version": "1.3.18",
3
+ "version": "1.3.20",
4
4
  "description": "AI chat application with YOLO-mode local workspace tools. React + Vite + Tailwind CSS frontend, local Node.js storage server.",
5
5
  "keywords": [
6
6
  "ai",
@@ -42,8 +42,8 @@
42
42
  "package.json"
43
43
  ],
44
44
  "dependencies": {
45
- "@mariozechner/pi-agent-core": "^0.70.5",
46
- "@mariozechner/pi-ai": "^0.70.5",
45
+ "@mariozechner/pi-agent-core": "^0.73.1",
46
+ "@mariozechner/pi-ai": "^0.73.1",
47
47
  "@modelcontextprotocol/sdk": "^1.29.0",
48
48
  "ws": "^8.20.1"
49
49
  },
@@ -5,6 +5,11 @@ import { streamSimpleWithAiHttpLogging } from './ai-http-logger.mjs'
5
5
  import { toolHandlers, loadSkillToolContext, abortRunningCommand } from './tools/index.mjs'
6
6
  import { createSkillTools, workspaceTools } from './tools/definitions.mjs'
7
7
  import { callMcpTool, createMcpToolDefinitions, isMcpToolName } from './mcp/registry.mjs'
8
+ import {
9
+ composeSubagentSystemPrompt,
10
+ formatSubagentTask,
11
+ getSubagentDefinition,
12
+ } from './subagents.mjs'
8
13
  import { projectContextFromId, readProjectConfig } from './project-config.mjs'
9
14
  import { readStore, atomicUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
10
15
  import { logger } from './utils/logger.mjs'
@@ -88,7 +93,36 @@ function wrapMcpToolDefinition(definition, toolPermissions) {
88
93
  }
89
94
  }
90
95
 
91
- async function createServerTools(projectId, projectContext, skillsContext, includeWorkspaceTools, toolPermissions) {
96
+ function wrapSubagentToolDefinition(definition, parentSessionId) {
97
+ return {
98
+ ...definition,
99
+ execute: async (_toolCallId, params, signal, onUpdate) => {
100
+ const parentSession = agentSessions.get(parentSessionId)
101
+ if (!parentSession) throw new Error('Parent session is no longer active.')
102
+ const result = await runSubagent(parentSession, params || {}, signal, onUpdate)
103
+ return {
104
+ content: [{ type: 'text', text: result.content }],
105
+ details: result.details,
106
+ }
107
+ },
108
+ }
109
+ }
110
+
111
+ function wrapWorkspaceToolDefinition(definition, context, toolPermissions, options = {}) {
112
+ if (definition.name === 'run_subagent') return wrapSubagentToolDefinition(definition, options.parentSessionId)
113
+ return wrapToolDefinition(definition, context, toolPermissions)
114
+ }
115
+
116
+ async function createServerTools(projectId, projectContext, skillsContext, includeWorkspaceTools, toolPermissions, options = {}) {
117
+ const {
118
+ allowedToolNames = null,
119
+ includeSubagentTool = true,
120
+ includeMcpTools = true,
121
+ parentSessionId = null,
122
+ } = options
123
+ const allowedTools = allowedToolNames ? new Set(allowedToolNames) : null
124
+ const isAllowed = (definition) => !allowedTools || allowedTools.has(definition.name)
125
+
92
126
  const skillTools = await createSkillTools({
93
127
  globalSkillNames: skillsContext.globalSkillNames,
94
128
  projectSkillNames: skillsContext.projectSkillNames,
@@ -100,14 +134,21 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
100
134
  workspaceRoot: projectContext?.workspaceRoot,
101
135
  })
102
136
  const toolContext = { ...projectContext, ...skillToolContext }
103
- const tools = skillTools.map((definition) => wrapToolDefinition(definition, toolContext, toolPermissions))
137
+ const tools = skillTools
138
+ .filter(isAllowed)
139
+ .map((definition) => wrapToolDefinition(definition, toolContext, toolPermissions))
104
140
 
105
141
  if (includeWorkspaceTools && projectId && projectContext) {
106
- tools.push(...workspaceTools.map((definition) => wrapToolDefinition(definition, toolContext, toolPermissions)))
142
+ const definitions = workspaceTools.filter((definition) => includeSubagentTool || definition.name !== 'run_subagent')
143
+ tools.push(...definitions
144
+ .filter(isAllowed)
145
+ .map((definition) => wrapWorkspaceToolDefinition(definition, toolContext, toolPermissions, { parentSessionId })))
107
146
  }
108
147
 
109
- const mcpTools = await createMcpToolDefinitions()
110
- tools.push(...mcpTools.map((definition) => wrapMcpToolDefinition(definition, toolPermissions)))
148
+ if (includeMcpTools) {
149
+ const mcpTools = await createMcpToolDefinitions()
150
+ tools.push(...mcpTools.filter(isAllowed).map((definition) => wrapMcpToolDefinition(definition, toolPermissions)))
151
+ }
111
152
 
112
153
  return tools
113
154
  }
@@ -126,6 +167,7 @@ async function rebuildSessionTools(session) {
126
167
  sessionSkillsContext(session),
127
168
  !!(session.projectId && session.projectContext),
128
169
  createCommandToolPermissions(session),
170
+ { parentSessionId: session.sessionId },
129
171
  )
130
172
  }
131
173
 
@@ -139,9 +181,10 @@ const agentSessions = new Map()
139
181
 
140
182
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
141
183
  const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes for tool approval
184
+ const SUBAGENT_DEFAULT_TIMEOUT_MS = 30 * 60 * 1000
142
185
  const commandRestrictedTools = new Set(['write_file', 'edit_file', 'run_command'])
143
186
  const safeReadTools = new Set(['read_file', 'grep_files'])
144
- const pendingApprovals = new Map() // toolCallId → { resolve, reject, sessionId, toolName, args, timeout }
187
+ const pendingApprovals = new Map() // toolCallId → { resolve, reject, sessionId, toolName, args, source, timeout }
145
188
  const pendingAutoCompactApprovals = new Map() // approvalId → { resolve, reject, sessionId, timeout }
146
189
 
147
190
  function createCommandToolPermissions(session) {
@@ -163,7 +206,7 @@ function createCommandToolPermissions(session) {
163
206
  * The agent loop's `await config.beforeToolCall(...)` pauses on this promise,
164
207
  * effectively freezing the agent until the user decides.
165
208
  */
166
- function createApprovalPromise(session, toolCallId, toolName, args) {
209
+ function createApprovalPromise(session, toolCallId, toolName, args, source) {
167
210
  if (!session) return { block: true, reason: 'No active session for tool approval.' }
168
211
  return new Promise((resolve, reject) => {
169
212
  let settled = false
@@ -209,6 +252,7 @@ function createApprovalPromise(session, toolCallId, toolName, args) {
209
252
  sessionId: session.sessionId,
210
253
  toolName,
211
254
  args,
255
+ source,
212
256
  })
213
257
 
214
258
  // Notify the frontend via both the session-level and global event buses.
@@ -220,6 +264,7 @@ function createApprovalPromise(session, toolCallId, toolName, args) {
220
264
  toolCallId,
221
265
  toolName,
222
266
  args,
267
+ source,
223
268
  }
224
269
  session.eventBus.emit('agent_event', approvalEvent)
225
270
  agentEvents.emit('agent_event', approvalEvent)
@@ -574,6 +619,13 @@ async function resolveCommandState(session, userMessage) {
574
619
  }
575
620
  }
576
621
 
622
+ function omitDetailsForLlm(message) {
623
+ if (!message || typeof message !== 'object' || message.details === undefined) return message
624
+ const copy = { ...message }
625
+ delete copy.details
626
+ return copy
627
+ }
628
+
577
629
  /**
578
630
  * Convert AgentMessage[] to LLM-compatible Message[].
579
631
  * Handles "user-with-attachments" → "user" with multi-modal content blocks.
@@ -597,14 +649,197 @@ function serverConvertToLlm(messages) {
597
649
  }
598
650
  }
599
651
  }
600
- return { ...m, role: 'user', content: textContent }
652
+ return omitDetailsForLlm({ ...m, role: 'user', content: textContent })
601
653
  }
602
- if (m.role === 'user' || m.role === 'assistant' || m.role === 'toolResult') return m
654
+ if (m.role === 'user' || m.role === 'assistant' || m.role === 'toolResult') return omitDetailsForLlm(m)
603
655
  return null
604
656
  })
605
657
  .filter(Boolean)
606
658
  }
607
659
 
660
+ function messageText(message) {
661
+ const content = message?.content
662
+ if (typeof content === 'string') return content
663
+ if (Array.isArray(content)) {
664
+ return content
665
+ .filter((block) => block?.type === 'text')
666
+ .map((block) => block.text ?? '')
667
+ .join('\n')
668
+ .trim()
669
+ }
670
+ return ''
671
+ }
672
+
673
+ function lastAssistantText(messages) {
674
+ for (let index = messages.length - 1; index >= 0; index--) {
675
+ const message = messages[index]
676
+ if (message?.role !== 'assistant') continue
677
+ const text = messageText(message)
678
+ if (text) return text
679
+ }
680
+ return ''
681
+ }
682
+
683
+ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
684
+ const definition = getSubagentDefinition(params?.subagent)
685
+ if (!definition) {
686
+ const error = new Error(`Unknown subagent: ${params?.subagent || ''}`)
687
+ error.statusCode = 400
688
+ throw error
689
+ }
690
+
691
+ const task = String(params?.task || '').trim()
692
+ if (!task) {
693
+ const error = new Error('task is required')
694
+ error.statusCode = 400
695
+ throw error
696
+ }
697
+ if (!parentSession.projectId || !parentSession.projectContext) {
698
+ throw new Error('Subagents require an active project workspace.')
699
+ }
700
+ if (!parentSession.model) {
701
+ throw new Error('No active model is configured for the parent session.')
702
+ }
703
+
704
+ const timeoutMs = Math.max(1000, Math.min(Number(definition.maxRuntimeMs || SUBAGENT_DEFAULT_TIMEOUT_MS), 30 * 60 * 1000))
705
+ const subagentSessionId = `${parentSession.sessionId}:subagent:${definition.name}:${randomUUID()}`
706
+ const startedAt = Date.now()
707
+ let toolCalls = 0
708
+ let latestMessages = []
709
+ let latestPendingToolCalls = []
710
+ let toolsForClient = []
711
+
712
+ const tools = await createServerTools(
713
+ parentSession.projectId,
714
+ parentSession.projectContext,
715
+ sessionSkillsContext(parentSession),
716
+ true,
717
+ (toolName) => {
718
+ if (!definition.allowedTools.includes(toolName)) return `Subagent ${definition.name} is not allowed to use ${toolName}.`
719
+ return null
720
+ },
721
+ {
722
+ allowedToolNames: definition.allowedTools,
723
+ includeSubagentTool: false,
724
+ includeMcpTools: false,
725
+ },
726
+ )
727
+ toolsForClient = tools.map(({ execute, prepareArguments, ...tool }) => tool)
728
+
729
+ const emitSubagentTrace = () => {
730
+ onUpdate?.({
731
+ content: [],
732
+ details: {
733
+ subagent: definition.name,
734
+ label: definition.label,
735
+ sessionId: subagentSessionId,
736
+ parentSessionId: parentSession.sessionId,
737
+ toolCalls,
738
+ allowedTools: definition.allowedTools,
739
+ timeoutMs,
740
+ durationMs: Date.now() - startedAt,
741
+ messages: latestMessages,
742
+ tools: toolsForClient,
743
+ pendingToolCalls: latestPendingToolCalls,
744
+ },
745
+ })
746
+ }
747
+
748
+ const systemPrompt = composeSubagentSystemPrompt({
749
+ definition,
750
+ parentSystemPrompt: parentSession.agent.state.systemPrompt,
751
+ projectContext: parentSession.projectContext,
752
+ })
753
+ const userMessage = {
754
+ role: 'user',
755
+ content: [{ type: 'text', text: formatSubagentTask(params) }],
756
+ timestamp: Date.now(),
757
+ }
758
+ const subagent = new Agent({
759
+ initialState: {
760
+ systemPrompt,
761
+ model: parentSession.model,
762
+ thinkingLevel: parentSession.thinkingLevel,
763
+ messages: [],
764
+ tools,
765
+ },
766
+ streamFn: streamSimpleWithAiHttpLogging,
767
+ getApiKey: parentSession.getApiKey,
768
+ sessionId: subagentSessionId,
769
+ convertToLlm: serverConvertToLlm,
770
+ onPayload: (payload) => {
771
+ restoreReasoningContentInPayload(payload, subagent.state.messages, subagent.state.model)
772
+ },
773
+ beforeToolCall: async (context) => {
774
+ const toolName = context.toolCall?.name
775
+ toolCalls += 1
776
+ emitSubagentTrace()
777
+ if (toolCalls > Number(definition.maxToolCalls || 12)) {
778
+ return { block: true, reason: `Subagent ${definition.name} exceeded its tool-call budget.` }
779
+ }
780
+ if (!definition.allowedTools.includes(toolName)) {
781
+ return { block: true, reason: `Subagent ${definition.name} is not allowed to use ${toolName}.` }
782
+ }
783
+ if (!parentSession.yoloMode) {
784
+ if (safeReadTools.has(toolName)) return undefined
785
+ return createApprovalPromise(parentSession, context.toolCall?.id, toolName, context.args, {
786
+ type: 'subagent',
787
+ subagent: definition.name,
788
+ label: definition.label,
789
+ sessionId: subagentSessionId,
790
+ })
791
+ }
792
+ return undefined
793
+ },
794
+ })
795
+
796
+ subagent.subscribe((event) => {
797
+ latestMessages = subagent.state.messages.slice()
798
+ latestPendingToolCalls = Array.from(subagent.state.pendingToolCalls || [])
799
+ if (event.type === 'message_start' || event.type === 'message_update') {
800
+ if (event.message?.role === 'assistant') {
801
+ latestMessages = [...latestMessages, event.message]
802
+ }
803
+ }
804
+ emitSubagentTrace()
805
+ })
806
+
807
+ let timedOut = false
808
+ const timeout = setTimeout(() => {
809
+ timedOut = true
810
+ subagent.abort()
811
+ }, timeoutMs)
812
+ const onParentAbort = () => subagent.abort()
813
+ parentSignal?.addEventListener?.('abort', onParentAbort, { once: true })
814
+
815
+ try {
816
+ await subagent.prompt(userMessage)
817
+ if (timedOut) throw new Error(`Subagent ${definition.name} timed out after ${timeoutMs}ms.`)
818
+ if (parentSignal?.aborted) throw new Error(`Subagent ${definition.name} aborted with parent run.`)
819
+ } finally {
820
+ clearTimeout(timeout)
821
+ parentSignal?.removeEventListener?.('abort', onParentAbort)
822
+ }
823
+
824
+ const content = lastAssistantText(subagent.state.messages) || `Subagent ${definition.name} completed without a text response.`
825
+ return {
826
+ content,
827
+ details: {
828
+ subagent: definition.name,
829
+ label: definition.label,
830
+ sessionId: subagentSessionId,
831
+ parentSessionId: parentSession.sessionId,
832
+ toolCalls,
833
+ allowedTools: definition.allowedTools,
834
+ timeoutMs,
835
+ durationMs: Date.now() - startedAt,
836
+ messages: latestMessages,
837
+ tools: toolsForClient,
838
+ pendingToolCalls: latestPendingToolCalls,
839
+ },
840
+ }
841
+ }
842
+
608
843
  function applyActiveCommandPrompt(messages, commandPrompt) {
609
844
  if (!commandPrompt) return messages
610
845
 
@@ -717,6 +952,7 @@ export async function createAgent(sessionId, config = {}) {
717
952
  systemPrompt = null,
718
953
  title = 'New chat',
719
954
  createdAt = new Date().toISOString(),
955
+ lastModified = null,
720
956
  contextCompaction = null,
721
957
  } = config
722
958
 
@@ -764,6 +1000,7 @@ export async function createAgent(sessionId, config = {}) {
764
1000
  const session = agentSessions.get(sessionId)
765
1001
  return session ? createCommandToolPermissions(session)(toolName) : null
766
1002
  },
1003
+ { parentSessionId: sessionId },
767
1004
  )
768
1005
 
769
1006
  // Resolve API key
@@ -798,6 +1035,7 @@ export async function createAgent(sessionId, config = {}) {
798
1035
  const isSkillTool = toolName === 'activate_skill' || toolName === 'read_skill_resource'
799
1036
  if (isSkillTool) return undefined
800
1037
  const currentSession = agentSessions.get(sessionId)
1038
+ if (toolName === 'run_subagent') return undefined
801
1039
  if (isMcpToolName(toolName)) {
802
1040
  if (!currentSession?.yoloMode) return createApprovalPromise(currentSession, toolCallId, toolName, context.args)
803
1041
  return undefined
@@ -828,6 +1066,7 @@ export async function createAgent(sessionId, config = {}) {
828
1066
  scope,
829
1067
  title,
830
1068
  createdAt,
1069
+ lastModified,
831
1070
  globalSkillNames: skillsContext.globalSkillNames,
832
1071
  projectSkillNames: skillsContext.projectSkillNames,
833
1072
  status: 'idle',
@@ -902,11 +1141,35 @@ export async function createAgent(sessionId, config = {}) {
902
1141
  return session
903
1142
  }
904
1143
 
1144
+ function messageTimestampMs(message) {
1145
+ const timestamp = message?.timestamp
1146
+ if (typeof timestamp === 'number' && Number.isFinite(timestamp)) return timestamp
1147
+ if (typeof timestamp === 'string') {
1148
+ const trimmed = timestamp.trim()
1149
+ if (!trimmed) return undefined
1150
+ const numeric = Number(trimmed)
1151
+ if (Number.isFinite(numeric)) return numeric
1152
+ const parsed = Date.parse(trimmed)
1153
+ return Number.isNaN(parsed) ? undefined : parsed
1154
+ }
1155
+ return undefined
1156
+ }
1157
+
1158
+ function sessionLastModifiedFromMessages(messages, fallback) {
1159
+ for (let index = messages.length - 1; index >= 0; index--) {
1160
+ const timestamp = messageTimestampMs(messages[index])
1161
+ if (timestamp !== undefined) return new Date(timestamp).toISOString()
1162
+ }
1163
+
1164
+ const fallbackMs = Date.parse(fallback)
1165
+ return Number.isNaN(fallbackMs) ? new Date().toISOString() : new Date(fallbackMs).toISOString()
1166
+ }
1167
+
905
1168
  /**
906
1169
  * Persist session data to storage.
907
1170
  */
908
1171
  async function persistSession(session) {
909
- const { sessionId, agent, scope, projectId, title, createdAt, status, startedAt, finishedAt, model, thinkingLevel, yoloMode, contextCompaction } = session
1172
+ const { sessionId, agent, scope, projectId, title, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, yoloMode, contextCompaction } = session
910
1173
  const messages = agent.state.messages
911
1174
 
912
1175
  if (messages.length === 0) {
@@ -923,6 +1186,7 @@ async function persistSession(session) {
923
1186
  }
924
1187
 
925
1188
  const now = new Date().toISOString()
1189
+ const lastModified = sessionLastModifiedFromMessages(messages, storedLastModified || createdAt || now)
926
1190
  const sessionData = {
927
1191
  id: sessionId,
928
1192
  title,
@@ -931,7 +1195,7 @@ async function persistSession(session) {
931
1195
  yoloMode,
932
1196
  messages,
933
1197
  createdAt: createdAt || now,
934
- lastModified: now,
1198
+ lastModified,
935
1199
  scope,
936
1200
  projectId: scope === 'project' ? projectId : undefined,
937
1201
  taskStatus: status,
@@ -939,6 +1203,7 @@ async function persistSession(session) {
939
1203
  taskFinishedAt: finishedAt,
940
1204
  contextCompaction: contextCompaction || undefined,
941
1205
  }
1206
+ session.lastModified = lastModified
942
1207
 
943
1208
  // Calculate usage
944
1209
  let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 }
@@ -974,7 +1239,7 @@ async function persistSession(session) {
974
1239
  id: sessionId,
975
1240
  title,
976
1241
  createdAt: createdAt || now,
977
- lastModified: now,
1242
+ lastModified,
978
1243
  messageCount: messages.length,
979
1244
  usage,
980
1245
  thinkingLevel,
@@ -1261,6 +1526,7 @@ export function getSessionState(sessionId) {
1261
1526
  thinkingLevel: session.thinkingLevel,
1262
1527
  title: session.title,
1263
1528
  createdAt: session.createdAt,
1529
+ lastModified: session.lastModified,
1264
1530
  status: session.status,
1265
1531
  startedAt: session.startedAt,
1266
1532
  finishedAt: session.finishedAt,
@@ -1362,6 +1628,7 @@ export async function restoreAgent(sessionId) {
1362
1628
  messages: sessionData.messages || [],
1363
1629
  title: sessionData.title || 'New chat',
1364
1630
  createdAt: sessionData.createdAt,
1631
+ lastModified: sessionData.lastModified,
1365
1632
  contextCompaction: sessionData.contextCompaction || null,
1366
1633
  })
1367
1634
  } catch (err) {