skimpyclaw 0.3.14 → 0.4.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 (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -2,17 +2,10 @@
2
2
  import { readFileSync, existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
- import { stripProvider, truncateToolResult } from './utils.js';
6
- import { toErrorMessage } from '../utils.js';
7
- import { compactCodexMessages } from './context-manager.js';
8
- import { toCodexContent, toCodexToolDefinitions } from './content.js';
9
- import { toNumericUsageDetails, toCostDetails } from './observability.js';
10
- import { executeTool } from '../tools.js';
11
- import { ToolCallGuard } from './tool-guard.js';
12
- import { startTrace, addEvent } from '../audit.js';
13
- import { startObservation } from '@langfuse/tracing';
5
+ import { toCostDetails } from './observability.js';
14
6
  import { buildUsageRecord, recordUsage } from '../usage.js';
15
- const DEFAULT_CODEX_AUTH_PATH = join(homedir(), '.codex', 'auth.json');
7
+ const DEFAULT_CODEX_HOME = join(homedir(), '.codex');
8
+ const DEFAULT_CODEX_AUTH_PATH = join(DEFAULT_CODEX_HOME, 'auth.json');
16
9
  const DEFAULT_CODEX_BASE_URL = 'https://chatgpt.com/backend-api';
17
10
  const DEFAULT_CODEX_FETCH_TIMEOUT_MS = 120_000;
18
11
  let codexAuthPath = DEFAULT_CODEX_AUTH_PATH;
@@ -33,12 +26,34 @@ export function resetCodexProviderState() {
33
26
  codexAuth = null;
34
27
  }
35
28
  export function setCodexAuthPath(path) {
36
- codexAuthPath = path;
29
+ codexAuthPath = resolveCodexAuthPath(path);
37
30
  }
38
31
  export function setCodexBaseUrl(url) {
39
32
  codexBaseUrl = url;
40
33
  }
34
+ export function resolveCodexAuthPath(authPath) {
35
+ if (!authPath)
36
+ return DEFAULT_CODEX_AUTH_PATH;
37
+ if (authPath === '~')
38
+ return homedir();
39
+ if (authPath.startsWith('~/'))
40
+ return join(homedir(), authPath.slice(2));
41
+ return authPath;
42
+ }
43
+ function decodeJwtPayload(token) {
44
+ const payloadPart = token.split('.')[1];
45
+ if (!payloadPart)
46
+ return null;
47
+ try {
48
+ const normalized = payloadPart.replace(/-/g, '+').replace(/_/g, '/');
49
+ return JSON.parse(Buffer.from(normalized, 'base64').toString());
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
41
55
  export function loadCodexAuth(authPath = codexAuthPath) {
56
+ authPath = resolveCodexAuthPath(authPath);
42
57
  if (!existsSync(authPath)) {
43
58
  console.log(`[codex] No auth file at ${authPath}`);
44
59
  return null;
@@ -46,28 +61,32 @@ export function loadCodexAuth(authPath = codexAuthPath) {
46
61
  try {
47
62
  const raw = JSON.parse(readFileSync(authPath, 'utf-8'));
48
63
  const token = raw?.tokens?.access_token;
49
- if (!token) {
64
+ if (typeof token !== 'string' || token.length === 0) {
50
65
  console.log('[codex] No access_token in auth file');
51
66
  return null;
52
67
  }
53
68
  // Decode JWT to check expiry and extract account ID
54
- const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
55
- const exp = payload.exp * 1000;
56
- const now = Date.now();
57
- if (now > exp) {
58
- const expiredAgo = Math.round((now - exp) / 60000);
59
- console.warn(`[codex] Token expired ${expiredAgo} min ago. Run 'codex' to re-auth.`);
60
- return null;
69
+ const payload = decodeJwtPayload(token);
70
+ if (payload?.exp) {
71
+ const exp = payload.exp * 1000;
72
+ const now = Date.now();
73
+ if (now > exp) {
74
+ const expiredAgo = Math.round((now - exp) / 60000);
75
+ console.warn(`[codex] Token expired ${expiredAgo} min ago. Run 'codex' to re-auth.`);
76
+ return null;
77
+ }
61
78
  }
62
- // Extract account ID from JWT claims
63
- const authClaims = payload['https://api.openai.com/auth'];
64
- const accountId = authClaims?.chatgpt_account_id;
65
- if (!accountId) {
66
- console.error('[codex] No account ID in token');
79
+ // Current Codex CLI auth files include tokens.account_id; older files only
80
+ // expose it in the JWT claims.
81
+ const authClaims = payload?.['https://api.openai.com/auth'];
82
+ const accountId = raw?.tokens?.account_id || authClaims?.chatgpt_account_id;
83
+ if (typeof accountId !== 'string' || accountId.length === 0) {
84
+ console.error('[codex] No account ID in auth file');
67
85
  return null;
68
86
  }
69
- const expiresIn = Math.round((exp - now) / 60000);
70
- console.log(`[codex] Token valid (expires in ${expiresIn} min, account: ${accountId.slice(0, 8)}...)`);
87
+ const expiresIn = payload?.exp ? Math.round((payload.exp * 1000 - Date.now()) / 60000) : null;
88
+ const expiryDetail = expiresIn === null ? 'expiry unknown' : `expires in ${expiresIn} min`;
89
+ console.log(`[codex] Token valid (${expiryDetail}, account: ${accountId.slice(0, 8)}...)`);
71
90
  return { accessToken: token, accountId };
72
91
  }
73
92
  catch (error) {
@@ -77,7 +96,7 @@ export function loadCodexAuth(authPath = codexAuthPath) {
77
96
  }
78
97
  export function initCodexAuth(path, baseUrl) {
79
98
  if (path)
80
- codexAuthPath = path;
99
+ codexAuthPath = resolveCodexAuthPath(path);
81
100
  if (baseUrl)
82
101
  codexBaseUrl = baseUrl;
83
102
  codexAuth = loadCodexAuth();
@@ -90,7 +109,7 @@ export function isCodexAvailable() {
90
109
  return codexAuth !== null;
91
110
  }
92
111
  const LANGFUSE_APP_NAME = 'skimpyclaw';
93
- function recordCodexUsage(params) {
112
+ export function recordCodexUsage(params) {
94
113
  const usage = params.usage;
95
114
  const inputTokens = typeof usage?.input_tokens === 'number' ? usage.input_tokens : 0;
96
115
  const outputTokens = typeof usage?.output_tokens === 'number' ? usage.output_tokens : 0;
@@ -115,12 +134,13 @@ async function startGenerationObservation(name, attributes) {
115
134
  if (!isLangfuseEnabled())
116
135
  return null;
117
136
  attributes.metadata = { app: LANGFUSE_APP_NAME, ...attributes.metadata };
137
+ const { startObservation } = await import('@langfuse/tracing');
118
138
  return startObservation(name, attributes, { asType: 'generation' });
119
139
  }
120
140
  /**
121
141
  * Make a single Codex API call. Returns raw SSE text.
122
142
  */
123
- async function codexFetch(body, timeoutMs = DEFAULT_CODEX_FETCH_TIMEOUT_MS) {
143
+ export async function codexFetch(body, timeoutMs = DEFAULT_CODEX_FETCH_TIMEOUT_MS) {
124
144
  if (!codexAuth) {
125
145
  throw new Error('Codex auth not initialized. Run "codex" CLI to authenticate.');
126
146
  }
@@ -160,9 +180,14 @@ async function codexFetch(body, timeoutMs = DEFAULT_CODEX_FETCH_TIMEOUT_MS) {
160
180
  * Parse an SSE response from the Codex backend.
161
181
  * Extracts function calls from the completed response object.
162
182
  */
163
- function parseCodexSSE(text) {
183
+ export function parseCodexSSE(text) {
164
184
  let outputText = '';
165
185
  let completedResponse = null;
186
+ // Track streaming function calls and output items
187
+ const streamingFunctionCalls = new Map();
188
+ const streamingOutputTexts = new Map();
189
+ let currentOutputIndex = -1;
190
+ let currentFcIndex = -1;
166
191
  for (const line of text.split('\n')) {
167
192
  if (!line.startsWith('data: '))
168
193
  continue;
@@ -172,7 +197,38 @@ function parseCodexSSE(text) {
172
197
  try {
173
198
  const event = JSON.parse(data);
174
199
  if (event.type === 'response.output_text.delta') {
175
- outputText += event.delta || '';
200
+ // Text delta accumulate by output_index or globally
201
+ const idx = event.output_index ?? currentOutputIndex;
202
+ if (idx >= 0) {
203
+ streamingOutputTexts.set(idx, (streamingOutputTexts.get(idx) || '') + (event.delta || ''));
204
+ }
205
+ else {
206
+ outputText += event.delta || '';
207
+ }
208
+ }
209
+ else if (event.type === 'response.output_item.added') {
210
+ // New output item — track its type
211
+ const item = event.item;
212
+ const idx = event.output_index ?? -1;
213
+ if (item?.type === 'function_call') {
214
+ currentFcIndex = idx;
215
+ streamingFunctionCalls.set(idx, {
216
+ callId: item.call_id || item.id || `fc-${idx}`,
217
+ name: item.name || '',
218
+ arguments: '',
219
+ });
220
+ }
221
+ else if (item?.type === 'output_text' || item?.type === 'message') {
222
+ currentOutputIndex = idx;
223
+ }
224
+ }
225
+ else if (event.type === 'response.function_call_arguments.delta') {
226
+ // Function call argument delta
227
+ const idx = event.output_index ?? currentFcIndex;
228
+ const fc = streamingFunctionCalls.get(idx);
229
+ if (fc) {
230
+ fc.arguments += event.delta || '';
231
+ }
176
232
  }
177
233
  else if (event.type === 'response.completed' && event.response) {
178
234
  completedResponse = event.response;
@@ -182,9 +238,9 @@ function parseCodexSSE(text) {
182
238
  }
183
239
  catch { /* skip non-JSON lines */ }
184
240
  }
185
- // Extract function calls and output_text items from completed response
241
+ // Build function calls prefer completed response, fall back to streaming
186
242
  const functionCalls = [];
187
- if (completedResponse?.output) {
243
+ if (completedResponse?.output?.length) {
188
244
  for (const item of completedResponse.output) {
189
245
  if (item.type === 'function_call') {
190
246
  functionCalls.push({
@@ -194,10 +250,43 @@ function parseCodexSSE(text) {
194
250
  });
195
251
  }
196
252
  else if (item.type === 'output_text' && item.text) {
197
- // Capture output_text items that may not appear at top-level
198
253
  if (!outputText)
199
254
  outputText = item.text;
200
255
  }
256
+ else if (item.type === 'message' && item.content) {
257
+ for (const c of item.content) {
258
+ if (c.type === 'output_text' && c.text) {
259
+ outputText += c.text;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ else {
266
+ // Completed response had empty output — use streaming data
267
+ // Also patch the rawResponse so appendAssistantResponse includes these items
268
+ const patchedOutput = [];
269
+ for (const [, fc] of streamingFunctionCalls) {
270
+ if (fc.name) {
271
+ functionCalls.push(fc);
272
+ patchedOutput.push({
273
+ type: 'function_call',
274
+ call_id: fc.callId,
275
+ name: fc.name,
276
+ arguments: fc.arguments,
277
+ });
278
+ }
279
+ }
280
+ for (const [, txt] of streamingOutputTexts) {
281
+ if (txt) {
282
+ if (!outputText)
283
+ outputText = txt;
284
+ patchedOutput.push({ type: 'output_text', text: txt });
285
+ }
286
+ }
287
+ if (completedResponse && patchedOutput.length > 0) {
288
+ completedResponse.output = patchedOutput;
289
+ console.log(`[codex] Patched empty response.output with ${patchedOutput.length} streaming items`);
201
290
  }
202
291
  }
203
292
  if (completedResponse?.usage) {
@@ -206,305 +295,35 @@ function parseCodexSSE(text) {
206
295
  else {
207
296
  console.log('[codex] No usage data in response');
208
297
  }
298
+ // Fallback: Codex may put text in response.text instead of output_text or output[]
299
+ if (!outputText && completedResponse?.text) {
300
+ const textContent = typeof completedResponse.text === 'string'
301
+ ? completedResponse.text
302
+ : typeof completedResponse.text?.content === 'string'
303
+ ? completedResponse.text.content
304
+ : '';
305
+ if (textContent)
306
+ outputText = textContent;
307
+ }
308
+ // Debug: log when output is empty despite having tokens
309
+ if (!outputText && completedResponse) {
310
+ const outputTypes = (completedResponse.output || []).map((item) => `${item.type}${item.content ? `[${(item.content || []).map((c) => c.type).join(',')}]` : ''}`);
311
+ console.warn(`[codex] Empty outputText! status: ${completedResponse.status}, output: ${JSON.stringify(completedResponse.output)?.slice(0, 1000)}, text: ${JSON.stringify(completedResponse.text)?.slice(0, 200)}, reasoning: ${JSON.stringify(completedResponse.reasoning)?.slice(0, 200)}`);
312
+ }
209
313
  return { outputText, functionCalls, response: completedResponse };
210
314
  }
315
+ /** @deprecated Use adapter.chat() via the provider registry instead. */
211
316
  export async function chatCodex(params) {
212
- const { messages, options } = params;
213
- const modelId = stripProvider(options.model);
214
- // Build input — system messages go to `instructions`, rest to `input`
215
- let instructions = 'You are a helpful assistant.';
216
- const input = [];
217
- for (const m of messages) {
218
- if (m.role === 'system') {
219
- // Convert content to text
220
- const { contentToText } = await import('./utils.js');
221
- instructions = contentToText(m.content);
222
- }
223
- else {
224
- const contentType = m.role === 'assistant' ? 'output_text' : 'input_text';
225
- const content = toCodexContent(m.content, contentType);
226
- input.push({
227
- type: 'message',
228
- role: m.role,
229
- content,
230
- });
231
- }
232
- }
233
- const body = {
234
- model: modelId,
235
- instructions,
236
- input,
237
- store: false,
238
- stream: true,
239
- reasoning: { effort: 'medium', summary: 'auto' },
240
- include: ['reasoning.encrypted_content'],
241
- };
242
- const genObs = await startGenerationObservation(`codex:${modelId}`, {
243
- input: { instructions, input },
244
- model: modelId,
245
- modelParameters: { stream: true, reasoning: body.reasoning },
246
- metadata: { provider: 'codex' },
247
- });
248
- try {
249
- const sseText = await codexFetch(body);
250
- const parsed = parseCodexSSE(sseText);
251
- recordCodexUsage({ model: modelId, usage: parsed.response?.usage, trigger: 'api' });
252
- genObs?.update({
253
- output: { text: parsed.outputText },
254
- usageDetails: toNumericUsageDetails(parsed.response?.usage),
255
- costDetails: toCostDetails(modelId, parsed.response?.usage),
256
- });
257
- genObs?.end();
258
- return parsed.outputText || '[No response from Codex]';
259
- }
260
- catch (err) {
261
- const errorMessage = toErrorMessage(err);
262
- genObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
263
- genObs?.end();
264
- throw err;
265
- }
317
+ const { CodexAdapter } = await import('./adapters/codex-adapter.js');
318
+ const adapter = new CodexAdapter();
319
+ return adapter.chat(params.messages, params.options, params.config);
266
320
  }
267
321
  export async function chatWithToolsCodex(params) {
268
- const { messages, options, config, toolConfig, toolContext } = params;
269
- const modelId = stripProvider(options.model);
270
- const maxIterations = toolConfig.maxIterations || 100;
271
- // Build input — system messages go to `instructions`, rest to `input`
272
- let instructions = 'You are a helpful assistant.';
273
- const input = [];
274
- for (const m of messages) {
275
- if (m.role === 'system') {
276
- const { contentToText } = await import('./utils.js');
277
- instructions = contentToText(m.content);
278
- }
279
- else {
280
- const contentType = m.role === 'assistant' ? 'output_text' : 'input_text';
281
- const content = toCodexContent(m.content, contentType);
282
- input.push({
283
- type: 'message',
284
- role: m.role,
285
- content,
286
- });
287
- }
288
- }
289
- // Get tool definitions
290
- const { getToolDefinitions } = await import('../tools.js');
291
- const includeSpawn = !!(toolContext?.fullConfig && (toolContext?.chatId || toolContext?.isCronJob));
292
- const toolDefs = await getToolDefinitions(toolConfig, {
293
- includeAgentTools: includeSpawn,
294
- includeMcp: false,
295
- projects: toolContext?.fullConfig?.projects
296
- });
297
- const tools = toolDefs ? toCodexToolDefinitions(toolDefs) : undefined;
298
- const toolLog = [];
299
- const auditTraceId = toolContext?.auditTraceId || startTrace((toolContext?.trigger || 'api'));
300
- // Guard: spin detection, no-progress detection, token budget
301
- const guard = new ToolCallGuard(toolConfig.maxTurnTokens);
302
- for (let i = 0; i < maxIterations; i++) {
303
- // Check abort signal before each iteration
304
- if (toolContext?.abortSignal?.aborted) {
305
- return {
306
- response: `[Cancelled after ${toolLog.length} tool calls]`,
307
- toolCalls: toolLog,
308
- };
309
- }
310
- // Compact old tool results if context is growing large
311
- const compactionResult = await compactCodexMessages(input, toolConfig.contextManagement, i + 1, config);
312
- const inputForApi = compactionResult.messages;
313
- if (compactionResult.compacted) {
314
- const method = compactionResult.method === 'llm' ? 'LLM summary' : 'truncation';
315
- const detail = `~${Math.round((compactionResult.tokensBefore || 0) / 1000)}k → ~${Math.round((compactionResult.tokensAfter || 0) / 1000)}k tokens`;
316
- toolLog.push(`[context compacted via ${method}: ${detail}]`);
317
- }
318
- const body = {
319
- model: modelId,
320
- instructions,
321
- input: inputForApi,
322
- store: false,
323
- stream: true,
324
- reasoning: { effort: 'medium', summary: 'auto' },
325
- include: ['reasoning.encrypted_content'],
326
- };
327
- if (tools)
328
- body.tools = tools;
329
- console.log(`[codex] Iteration ${i + 1}/${maxIterations} (model: ${modelId})`);
330
- const genObs = await startGenerationObservation(`codex:${modelId}`, {
331
- input: { instructions, input },
332
- model: modelId,
333
- modelParameters: { stream: true, reasoning: body.reasoning },
334
- metadata: { provider: 'codex', iteration: i + 1 },
335
- });
336
- let parsed;
337
- try {
338
- const sseText = await codexFetch(body);
339
- parsed = parseCodexSSE(sseText);
340
- recordCodexUsage({
341
- model: modelId,
342
- usage: parsed.response?.usage,
343
- trigger: toolContext?.trigger || 'api',
344
- agentId: toolContext?.agentId,
345
- });
346
- genObs?.update({
347
- output: { text: parsed.outputText },
348
- usageDetails: toNumericUsageDetails(parsed.response?.usage),
349
- costDetails: toCostDetails(modelId, parsed.response?.usage),
350
- });
351
- genObs?.end();
352
- // Guard: track token usage (stats only, no enforcement)
353
- guard.recordTokens(parsed.response?.usage?.input_tokens ?? 0, parsed.response?.usage?.output_tokens ?? 0);
354
- }
355
- catch (err) {
356
- const errorMessage = toErrorMessage(err);
357
- genObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
358
- genObs?.end();
359
- throw err;
360
- }
361
- // No function calls — we're done
362
- if (parsed.functionCalls.length === 0) {
363
- // Debug: log what Codex actually returned when outputText is empty
364
- if (!parsed.outputText) {
365
- console.log(`[codex] Empty outputText. Response output items:`, JSON.stringify(parsed.response?.output?.map((i) => ({ type: i.type, text: i.text?.slice(0, 200) })), null, 2));
366
- }
367
- let finalText = parsed.outputText;
368
- if (!finalText && toolLog.length > 0) {
369
- try {
370
- // Some Codex runs finish tool execution but omit final text. Ask once more
371
- // (without tools) for a user-facing answer from the gathered context.
372
- const finalizeInput = [...input];
373
- if (parsed.response?.output) {
374
- for (const item of parsed.response.output) {
375
- finalizeInput.push(item);
376
- }
377
- }
378
- finalizeInput.push({
379
- type: 'message',
380
- role: 'user',
381
- content: [
382
- {
383
- type: 'input_text',
384
- text: 'Provide the final answer to the user using the tool results above. Do not call tools. Be concise.',
385
- },
386
- ],
387
- });
388
- const finalizeBody = {
389
- model: modelId,
390
- instructions,
391
- input: finalizeInput,
392
- store: false,
393
- stream: true,
394
- reasoning: { effort: 'medium', summary: 'auto' },
395
- include: ['reasoning.encrypted_content'],
396
- };
397
- console.log('[codex] Finalizing tool run with a text-only follow-up');
398
- const finalizeSse = await codexFetch(finalizeBody);
399
- const finalized = parseCodexSSE(finalizeSse);
400
- finalText = finalized.outputText?.trim() || '';
401
- }
402
- catch (err) {
403
- const msg = toErrorMessage(err);
404
- console.warn(`[codex] Finalization pass failed: ${msg}`);
405
- }
406
- }
407
- if (!finalText && toolLog.length > 0) {
408
- finalText = `[Completed ${toolLog.length} tool calls, but no final text response was generated.]`;
409
- }
410
- const usage = parsed.response?.usage;
411
- return {
412
- response: finalText || '[No response from Codex]',
413
- toolCalls: toolLog,
414
- usage: {
415
- prompt_tokens: usage?.input_tokens ?? 0,
416
- completion_tokens: usage?.output_tokens ?? 0,
417
- total_tokens: (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0),
418
- },
419
- cost: toCostDetails(modelId, usage),
420
- };
421
- }
422
- // Add the assistant's output items to input for next turn
423
- if (parsed.response?.output) {
424
- for (const item of parsed.response.output) {
425
- input.push(item);
426
- }
427
- }
428
- // Execute each function call and add results to input
429
- for (const fc of parsed.functionCalls) {
430
- const argsStr = fc.arguments || '{}';
431
- let args;
432
- try {
433
- args = JSON.parse(argsStr);
434
- }
435
- catch {
436
- args = {};
437
- }
438
- const inputStr = argsStr.slice(0, 200);
439
- console.log(`[codex:tools] -> ${fc.name}(${inputStr})`);
440
- // Guard: spin detection
441
- const guardResult = guard.recordCall(fc.name, args);
442
- if (guardResult.warning)
443
- console.warn(`[codex:tools:guard] ${guardResult.warning}`);
444
- if (guardResult.blocked) {
445
- input.push({
446
- type: 'function_call_output',
447
- call_id: fc.callId,
448
- output: guardResult.warning || 'Blocked: repeated identical call',
449
- });
450
- toolLog.push(`${fc.name} [BLOCKED: spin detected]`);
451
- continue;
452
- }
453
- const { isLangfuseEnabled } = await import('../langfuse.js');
454
- const toolObs = isLangfuseEnabled()
455
- ? startObservation(`tool:${fc.name}`, { input: args, metadata: { app: LANGFUSE_APP_NAME, tool: fc.name } }, { asType: 'tool' })
456
- : null;
457
- const toolStart = Date.now();
458
- try {
459
- const result = await executeTool(fc.name, args, toolConfig, toolContext) || '';
460
- const truncatedResult = truncateToolResult(result);
461
- const resultPreview = result.slice(0, 200) + (result.length > 200 ? '...' : '');
462
- console.log(`[codex:tools] <- ${resultPreview}`);
463
- toolLog.push(`${fc.name}(${inputStr}) → ${resultPreview}`);
464
- toolObs?.update({ output: result });
465
- toolObs?.end();
466
- if (toolContext?.auditTraceId) {
467
- addEvent(toolContext.auditTraceId, {
468
- type: 'tool_use',
469
- summary: `${fc.name}(${inputStr})`,
470
- durationMs: Date.now() - toolStart,
471
- });
472
- }
473
- input.push({
474
- type: 'function_call_output',
475
- call_id: fc.callId,
476
- output: truncatedResult,
477
- });
478
- // Guard: no-progress detection
479
- const progressResult = guard.recordResult(result);
480
- if (progressResult.nudge) {
481
- console.warn(`[codex:tools:guard] ${progressResult.nudge}`);
482
- input[input.length - 1].output += `\n\n[System: ${progressResult.nudge}]`;
483
- }
484
- }
485
- catch (err) {
486
- const errorMessage = toErrorMessage(err);
487
- toolObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
488
- toolObs?.end();
489
- if (toolContext?.auditTraceId) {
490
- addEvent(toolContext.auditTraceId, {
491
- type: 'tool_error',
492
- summary: `${fc.name} error: ${errorMessage.slice(0, 150)}`,
493
- durationMs: Date.now() - toolStart,
494
- });
495
- }
496
- throw err;
497
- }
498
- }
499
- }
500
- console.warn(`[codex:tools] Max iterations (${maxIterations}) reached`);
501
- return { response: '[Tool use loop reached maximum iterations]', toolCalls: toolLog };
502
- }
503
- /** Build UsageDetails from Codex usage response */
504
- function buildCodexUsageDetails(usage) {
505
- return {
506
- prompt_tokens: usage?.input_tokens ?? 0,
507
- completion_tokens: usage?.output_tokens ?? 0,
508
- total_tokens: (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0),
509
- };
322
+ const { runToolLoop } = await import('./tool-loop.js');
323
+ const { CodexAdapter } = await import('./adapters/codex-adapter.js');
324
+ const adapter = new CodexAdapter();
325
+ const unifiedToolConfig = params.toolConfig.maxIterations
326
+ ? params.toolConfig
327
+ : { ...params.toolConfig, maxIterations: 100 };
328
+ return runToolLoop(adapter, params.messages, params.options, params.config, unifiedToolConfig, params.toolContext);
510
329
  }
@@ -1,7 +1,7 @@
1
1
  import type { ContentBlock } from '../types.js';
2
2
  /**
3
3
  * Convert content array to OpenAI vision-compatible format.
4
- * Preserves images as data URIs for multimodal models (Kimi, MiniMax, etc.).
4
+ * Preserves images as data URIs for multimodal model formats.
5
5
  */
6
6
  export declare function toOpenAIContent(content: string | ContentBlock[]): string | Array<{
7
7
  type: string;
@@ -1,7 +1,7 @@
1
1
  // Content Format Conversions for Different Providers
2
2
  /**
3
3
  * Convert content array to OpenAI vision-compatible format.
4
- * Preserves images as data URIs for multimodal models (Kimi, MiniMax, etc.).
4
+ * Preserves images as data URIs for multimodal model formats.
5
5
  */
6
6
  export function toOpenAIContent(content) {
7
7
  if (typeof content === 'string')
@@ -50,6 +50,6 @@ export function toCodexToolDefinitions(tools) {
50
50
  type: 'function',
51
51
  name: t.name,
52
52
  description: t.description,
53
- parameters: t.input_schema,
53
+ parameters: t.input_schema && t.input_schema.type ? t.input_schema : { type: 'object', properties: {} },
54
54
  }));
55
55
  }
@@ -1,5 +1,6 @@
1
1
  import type { ContextManagementConfig } from './types.js';
2
2
  import type { Config } from '../types.js';
3
+ import type { MessageFormatHelper } from './adapter.js';
3
4
  export type { ContextManagementConfig };
4
5
  /** Result of a compaction attempt, including metadata about what happened. */
5
6
  export interface CompactionResult<T> {
@@ -17,6 +18,20 @@ export interface CompactionResult<T> {
17
18
  }
18
19
  /** Rough token estimate: 1 token ≈ 4 chars of JSON. */
19
20
  export declare function estimateTokens(data: any[]): number;
21
+ /**
22
+ * Generic compaction function for any message format.
23
+ * Delegates format-specific concerns (truncation, serialization, summary building)
24
+ * to the provided MessageFormatHelper.
25
+ *
26
+ * Does NOT mutate the input array — returns a new array.
27
+ */
28
+ export declare function compactMessages<T>(items: T[], helper: MessageFormatHelper<T>, config?: ContextManagementConfig, iteration?: number, fullConfig?: Config): Promise<CompactionResult<T>>;
29
+ /** Anthropic message format helper. */
30
+ export declare const anthropicFormatHelper: MessageFormatHelper<any>;
31
+ /** OpenAI message format helper. */
32
+ export declare const openaiFormatHelper: MessageFormatHelper<any>;
33
+ /** Codex message format helper. */
34
+ export declare const codexFormatHelper: MessageFormatHelper<any>;
20
35
  /**
21
36
  * Serialize Anthropic-format messages into a human-readable conversation transcript
22
37
  * suitable for LLM summarization.
@@ -32,20 +47,17 @@ declare function serializeOpenAIMessages(messages: any[]): string;
32
47
  declare function serializeCodexMessages(items: any[]): string;
33
48
  /**
34
49
  * Compact Anthropic-format apiMessages when over threshold.
35
- * Uses LLM summarization for old messages; falls back to truncation on failure.
36
- * Does NOT mutate the input array — returns a new array.
50
+ * @deprecated Use compactMessages() with anthropicFormatHelper instead.
37
51
  */
38
52
  export declare function compactAnthropicMessages(messages: any[], config?: ContextManagementConfig, iteration?: number, fullConfig?: Config): Promise<CompactionResult<any>>;
39
53
  /**
40
54
  * Compact OpenAI-format apiMessages when over threshold.
41
- * Uses LLM summarization for old messages; falls back to truncation on failure.
42
- * Does NOT mutate the input array — returns a new array.
55
+ * @deprecated Use compactMessages() with openaiFormatHelper instead.
43
56
  */
44
57
  export declare function compactOpenAIMessages(messages: any[], config?: ContextManagementConfig, iteration?: number, fullConfig?: Config): Promise<CompactionResult<any>>;
45
58
  /**
46
59
  * Compact Codex-format input items when over threshold.
47
- * Uses LLM summarization for old items; falls back to truncation on failure.
48
- * Does NOT mutate the input array — returns a new array.
60
+ * @deprecated Use compactMessages() with codexFormatHelper instead.
49
61
  */
50
62
  export declare function compactCodexMessages(input: any[], config?: ContextManagementConfig, iteration?: number, fullConfig?: Config): Promise<CompactionResult<any>>;
51
63
  export { serializeAnthropicMessages, serializeOpenAIMessages, serializeCodexMessages };