groove-dev 0.27.87 → 0.27.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -2
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +115 -7
- package/node_modules/@groove-dev/daemon/src/conversations.js +29 -3
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +28 -10
- package/node_modules/@groove-dev/daemon/src/registry.js +30 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +23 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-B_igwWvq.js +8642 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +254 -0
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +177 -0
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +148 -0
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +377 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +117 -40
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +10 -13
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -1
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +5 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +132 -1
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +22 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +115 -7
- package/packages/daemon/src/conversations.js +29 -3
- package/packages/daemon/src/providers/codex.js +28 -10
- package/packages/daemon/src/registry.js +30 -0
- package/packages/daemon/src/validate.js +23 -0
- package/packages/gui/dist/assets/index-BSqk8cbI.css +1 -0
- package/packages/gui/dist/assets/index-B_igwWvq.js +8642 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-file-tree.jsx +254 -0
- package/packages/gui/src/components/agents/code-review.jsx +177 -0
- package/packages/gui/src/components/agents/diff-viewer.jsx +148 -0
- package/packages/gui/src/components/agents/workspace-mode.jsx +377 -0
- package/packages/gui/src/components/chat/chat-input.jsx +117 -40
- package/packages/gui/src/components/chat/chat-messages.jsx +10 -13
- package/packages/gui/src/components/chat/chat-view.jsx +26 -1
- package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
- package/packages/gui/src/components/chat/model-picker.jsx +5 -0
- package/packages/gui/src/stores/groove.js +132 -1
- package/packages/gui/src/views/agents.jsx +22 -3
- package/test/doomsday-clock/index.html +55 -0
- package/test/doomsday-clock/script.js +66 -0
- package/test/doomsday-clock/style.css +315 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BCQY8ojz.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-C5e7KVGN.js +0 -8637
- package/packages/gui/dist/assets/index-BCQY8ojz.css +0 -1
- package/packages/gui/dist/assets/index-C5e7KVGN.js +0 -8637
package/CLAUDE.md
CHANGED
|
@@ -266,10 +266,11 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
|
|
|
266
266
|
|
|
267
267
|
<!-- GROOVE:START -->
|
|
268
268
|
## GROOVE Orchestration (auto-injected)
|
|
269
|
-
Active agents:
|
|
269
|
+
Active agents: 2
|
|
270
270
|
| Name | Role | Scope |
|
|
271
271
|
|------|------|-------|
|
|
272
|
-
|
|
|
272
|
+
| fullstack-23 | fullstack | - |
|
|
273
|
+
| fullstack-12 | fullstack | - |
|
|
273
274
|
See AGENTS_REGISTRY.md for full agent state.
|
|
274
275
|
**Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
|
|
275
276
|
<!-- GROOVE:END -->
|
|
@@ -16,7 +16,7 @@ import { OllamaProvider } from './providers/ollama.js';
|
|
|
16
16
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
17
17
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
18
18
|
import { ConsentManager } from '../../../moe-training/client/index.js';
|
|
19
|
-
import { validateAgentConfig } from './validate.js';
|
|
19
|
+
import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from './validate.js';
|
|
20
20
|
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
|
|
21
21
|
|
|
22
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -72,6 +72,9 @@ async function _executeApprovalRetry(daemon, approval) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
const FILE_READ_TOOLS = new Set(['Read', 'read_file']);
|
|
76
|
+
const FILE_WRITE_TOOLS = new Set(['Write', 'Edit', 'write_file', 'edit_file', 'create_file']);
|
|
77
|
+
|
|
75
78
|
export function createApi(app, daemon) {
|
|
76
79
|
_daemon = daemon;
|
|
77
80
|
|
|
@@ -325,6 +328,14 @@ export function createApi(app, daemon) {
|
|
|
325
328
|
}
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
// Track file operations for the files-touched API
|
|
332
|
+
if (targets.length > 0) {
|
|
333
|
+
const op = FILE_WRITE_TOOLS.has(toolName) ? 'write' : FILE_READ_TOOLS.has(toolName) ? 'read' : null;
|
|
334
|
+
if (op) {
|
|
335
|
+
for (const t of targets) daemon.registry.trackFileOp(agentId, t, op);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
328
339
|
daemon.audit.log('knock.allowed', { agentId, toolName, targets });
|
|
329
340
|
res.json({ allow: true });
|
|
330
341
|
});
|
|
@@ -1100,14 +1111,19 @@ export function createApi(app, daemon) {
|
|
|
1100
1111
|
|
|
1101
1112
|
app.post('/api/conversations', async (req, res) => {
|
|
1102
1113
|
try {
|
|
1103
|
-
const { provider, model, title, mode } = req.body;
|
|
1114
|
+
const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
|
|
1104
1115
|
if (!provider || typeof provider !== 'string') {
|
|
1105
1116
|
return res.status(400).json({ error: 'provider is required' });
|
|
1106
1117
|
}
|
|
1107
1118
|
if (mode && mode !== 'api' && mode !== 'agent') {
|
|
1108
1119
|
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
1109
1120
|
}
|
|
1110
|
-
const
|
|
1121
|
+
const validatedEffort = validateReasoningEffort(reasoning_effort);
|
|
1122
|
+
const validatedVerbosity = validateVerbosity(verbosity);
|
|
1123
|
+
const conversation = await daemon.conversations.create(provider, model, title, mode || 'api', {
|
|
1124
|
+
reasoningEffort: validatedEffort,
|
|
1125
|
+
verbosity: validatedVerbosity,
|
|
1126
|
+
});
|
|
1111
1127
|
daemon.audit.log('conversation.create', { id: conversation.id, provider, model, mode: conversation.mode });
|
|
1112
1128
|
res.status(201).json(conversation);
|
|
1113
1129
|
} catch (err) {
|
|
@@ -1139,6 +1155,11 @@ export function createApi(app, daemon) {
|
|
|
1139
1155
|
}
|
|
1140
1156
|
await daemon.conversations.setMode(req.params.id, req.body.mode);
|
|
1141
1157
|
}
|
|
1158
|
+
if (req.body.reasoning_effort !== undefined || req.body.verbosity !== undefined) {
|
|
1159
|
+
const validatedEffort = req.body.reasoning_effort !== undefined ? validateReasoningEffort(req.body.reasoning_effort) : undefined;
|
|
1160
|
+
const validatedVerbosity = req.body.verbosity !== undefined ? validateVerbosity(req.body.verbosity) : undefined;
|
|
1161
|
+
daemon.conversations.updateReasoningSettings(req.params.id, validatedEffort, validatedVerbosity);
|
|
1162
|
+
}
|
|
1142
1163
|
daemon.audit.log('conversation.update', { id: req.params.id, provider: req.body.provider, model: req.body.model, mode: req.body.mode });
|
|
1143
1164
|
res.json(daemon.conversations.get(req.params.id));
|
|
1144
1165
|
} catch (err) {
|
|
@@ -1160,10 +1181,13 @@ export function createApi(app, daemon) {
|
|
|
1160
1181
|
|
|
1161
1182
|
app.post('/api/conversations/:id/message', async (req, res) => {
|
|
1162
1183
|
try {
|
|
1163
|
-
const { message, history } = req.body;
|
|
1184
|
+
const { message, history, reasoning_effort, verbosity } = req.body;
|
|
1164
1185
|
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
1165
1186
|
return res.status(400).json({ error: 'message is required' });
|
|
1166
1187
|
}
|
|
1188
|
+
const validatedEffort = validateReasoningEffort(reasoning_effort);
|
|
1189
|
+
const validatedVerbosity = validateVerbosity(verbosity);
|
|
1190
|
+
|
|
1167
1191
|
const conv = daemon.conversations.get(req.params.id);
|
|
1168
1192
|
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
1169
1193
|
|
|
@@ -1172,7 +1196,10 @@ export function createApi(app, daemon) {
|
|
|
1172
1196
|
|
|
1173
1197
|
// API mode — lightweight headless streaming, no agent spawned
|
|
1174
1198
|
if (conv.mode === 'api' || !conv.agentId) {
|
|
1175
|
-
await daemon.conversations.sendMessage(req.params.id, message.trim(), history || []
|
|
1199
|
+
await daemon.conversations.sendMessage(req.params.id, message.trim(), history || [], {
|
|
1200
|
+
reasoningEffort: validatedEffort,
|
|
1201
|
+
verbosity: validatedVerbosity,
|
|
1202
|
+
});
|
|
1176
1203
|
daemon.audit.log('conversation.message', { id: req.params.id, mode: 'api' });
|
|
1177
1204
|
return res.json({ status: 'streaming', mode: 'api' });
|
|
1178
1205
|
}
|
|
@@ -3064,6 +3091,87 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3064
3091
|
});
|
|
3065
3092
|
});
|
|
3066
3093
|
|
|
3094
|
+
// Files touched by an agent during its session
|
|
3095
|
+
app.get('/api/agents/:id/files-touched', (req, res) => {
|
|
3096
|
+
const agent = daemon.registry.get(req.params.id);
|
|
3097
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
3098
|
+
const files = daemon.registry.getFilesTouched(req.params.id);
|
|
3099
|
+
res.json({ files, total: files.length });
|
|
3100
|
+
});
|
|
3101
|
+
|
|
3102
|
+
// Git diff — structured diff for a file, an agent's touched files, or all uncommitted changes
|
|
3103
|
+
app.get('/api/files/git-diff', (req, res) => {
|
|
3104
|
+
const rootDir = getEditorRoot();
|
|
3105
|
+
if (!rootDir) return res.status(400).json({ error: 'Editor root not set' });
|
|
3106
|
+
|
|
3107
|
+
let paths = [];
|
|
3108
|
+
|
|
3109
|
+
if (req.query.path) {
|
|
3110
|
+
const result = validateFilePath(req.query.path, rootDir);
|
|
3111
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
3112
|
+
paths = [req.query.path];
|
|
3113
|
+
} else if (req.query.agentId) {
|
|
3114
|
+
const agent = daemon.registry.get(req.query.agentId);
|
|
3115
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
3116
|
+
paths = daemon.registry.getFilesTouched(req.query.agentId).map(f => f.path);
|
|
3117
|
+
if (paths.length === 0) return res.json({ diffs: [] });
|
|
3118
|
+
// Validate each path
|
|
3119
|
+
for (const p of paths) {
|
|
3120
|
+
if (p.startsWith('/') || p.includes('..') || p.includes('\0')) {
|
|
3121
|
+
return res.status(400).json({ error: 'Invalid path in agent files' });
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
const args = ['diff'];
|
|
3127
|
+
const cachedArgs = ['diff', '--cached'];
|
|
3128
|
+
if (paths.length > 0) {
|
|
3129
|
+
args.push('--', ...paths);
|
|
3130
|
+
cachedArgs.push('--', ...paths);
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
try {
|
|
3134
|
+
const unstaged = execFileSync('git', args, { cwd: rootDir, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }).toString();
|
|
3135
|
+
const staged = execFileSync('git', cachedArgs, { cwd: rootDir, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }).toString();
|
|
3136
|
+
const combined = (staged + '\n' + unstaged).trim();
|
|
3137
|
+
const diffs = parseDiffOutput(combined);
|
|
3138
|
+
res.json({ diffs });
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
if (err.status !== undefined) {
|
|
3141
|
+
return res.json({ diffs: [] });
|
|
3142
|
+
}
|
|
3143
|
+
res.status(500).json({ error: 'Failed to compute diff' });
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
function parseDiffOutput(raw) {
|
|
3148
|
+
if (!raw) return [];
|
|
3149
|
+
const fileDiffs = raw.split(/^diff --git /m).filter(Boolean);
|
|
3150
|
+
return fileDiffs.map(chunk => {
|
|
3151
|
+
const lines = chunk.split('\n');
|
|
3152
|
+
const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
|
|
3153
|
+
const filePath = headerMatch ? headerMatch[2] : 'unknown';
|
|
3154
|
+
let status = 'modified';
|
|
3155
|
+
if (lines.some(l => l.startsWith('new file'))) status = 'added';
|
|
3156
|
+
else if (lines.some(l => l.startsWith('deleted file'))) status = 'deleted';
|
|
3157
|
+
let additions = 0, deletions = 0;
|
|
3158
|
+
const hunks = [];
|
|
3159
|
+
let currentHunk = null;
|
|
3160
|
+
for (const line of lines) {
|
|
3161
|
+
if (line.startsWith('@@')) {
|
|
3162
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
3163
|
+
currentHunk = { header: line, lines: [] };
|
|
3164
|
+
} else if (currentHunk) {
|
|
3165
|
+
currentHunk.lines.push(line);
|
|
3166
|
+
if (line.startsWith('+') && !line.startsWith('+++')) additions++;
|
|
3167
|
+
else if (line.startsWith('-') && !line.startsWith('---')) deletions++;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
3171
|
+
return { path: filePath, status, hunks, additions, deletions, content: 'diff --git ' + chunk };
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3067
3175
|
// File search — fuzzy filename matching for quick-open (Ctrl+P)
|
|
3068
3176
|
app.get('/api/files/search', (req, res) => {
|
|
3069
3177
|
const query = req.query.q;
|
|
@@ -4172,10 +4280,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4172
4280
|
|
|
4173
4281
|
app.post('/api/tunnels', (req, res) => {
|
|
4174
4282
|
try {
|
|
4175
|
-
const { name, host, user, port, sshKeyPath, autoStart, autoConnect } = req.body;
|
|
4283
|
+
const { name, host, user, port, sshKeyPath, autoStart, autoConnect, projectDir } = req.body;
|
|
4176
4284
|
if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name is required (string)' });
|
|
4177
4285
|
if (!host || typeof host !== 'string') return res.status(400).json({ error: 'host is required (string)' });
|
|
4178
|
-
const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect });
|
|
4286
|
+
const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect, projectDir });
|
|
4179
4287
|
res.json(result);
|
|
4180
4288
|
} catch (err) {
|
|
4181
4289
|
res.status(400).json({ error: err.message });
|
|
@@ -55,7 +55,7 @@ export class ConversationManager {
|
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
async create(provider, model, title, mode = 'api') {
|
|
58
|
+
async create(provider, model, title, mode = 'api', options = {}) {
|
|
59
59
|
if (!provider && this.daemon.config?.defaultChatProvider) {
|
|
60
60
|
provider = this.daemon.config.defaultChatProvider;
|
|
61
61
|
}
|
|
@@ -90,6 +90,9 @@ export class ConversationManager {
|
|
|
90
90
|
provider,
|
|
91
91
|
model: model || null,
|
|
92
92
|
mode: mode === 'agent' ? 'agent' : 'api',
|
|
93
|
+
reasoningEffort: options.reasoningEffort || null,
|
|
94
|
+
verbosity: options.verbosity || null,
|
|
95
|
+
previousResponseId: null,
|
|
93
96
|
createdAt: now,
|
|
94
97
|
updatedAt: now,
|
|
95
98
|
pinned: false,
|
|
@@ -218,6 +221,17 @@ export class ConversationManager {
|
|
|
218
221
|
return conv;
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
updateReasoningSettings(id, reasoningEffort, verbosity) {
|
|
225
|
+
const conv = this.conversations.get(id);
|
|
226
|
+
if (!conv) throw new Error('Conversation not found');
|
|
227
|
+
if (reasoningEffort !== undefined) conv.reasoningEffort = reasoningEffort || null;
|
|
228
|
+
if (verbosity !== undefined) conv.verbosity = verbosity || null;
|
|
229
|
+
conv.updatedAt = new Date().toISOString();
|
|
230
|
+
this._save();
|
|
231
|
+
this.daemon.broadcast({ type: 'conversation:updated', data: conv });
|
|
232
|
+
return conv;
|
|
233
|
+
}
|
|
234
|
+
|
|
221
235
|
async setMode(id, mode) {
|
|
222
236
|
const conv = this.conversations.get(id);
|
|
223
237
|
if (!conv) throw new Error('Conversation not found');
|
|
@@ -302,7 +316,7 @@ export class ConversationManager {
|
|
|
302
316
|
} catch { return null; }
|
|
303
317
|
}
|
|
304
318
|
|
|
305
|
-
async sendMessage(id, message, history) {
|
|
319
|
+
async sendMessage(id, message, history, { reasoningEffort, verbosity } = {}) {
|
|
306
320
|
const conv = this.conversations.get(id);
|
|
307
321
|
if (!conv) throw new Error('Conversation not found');
|
|
308
322
|
if (conv.mode !== 'api') throw new Error('sendMessage only works in API mode');
|
|
@@ -331,6 +345,9 @@ export class ConversationManager {
|
|
|
331
345
|
|
|
332
346
|
const apiKey = this._getApiKey(providerName);
|
|
333
347
|
|
|
348
|
+
const effectiveReasoningEffort = reasoningEffort || conv.reasoningEffort || null;
|
|
349
|
+
const effectiveVerbosity = verbosity || conv.verbosity || null;
|
|
350
|
+
|
|
334
351
|
// Try direct API streaming first (sub-second latency)
|
|
335
352
|
const controller = provider.streamChat(
|
|
336
353
|
messages, modelId, apiKey,
|
|
@@ -340,7 +357,11 @@ export class ConversationManager {
|
|
|
340
357
|
data: { conversationId: id, text },
|
|
341
358
|
});
|
|
342
359
|
},
|
|
343
|
-
() => {
|
|
360
|
+
(result) => {
|
|
361
|
+
if (result?.responseId) {
|
|
362
|
+
conv.previousResponseId = result.responseId;
|
|
363
|
+
this._save();
|
|
364
|
+
}
|
|
344
365
|
this._getStreamingProcesses().delete(id);
|
|
345
366
|
this.daemon.broadcast({
|
|
346
367
|
type: 'conversation:complete',
|
|
@@ -354,6 +375,11 @@ export class ConversationManager {
|
|
|
354
375
|
data: { conversationId: id, error: err.message },
|
|
355
376
|
});
|
|
356
377
|
},
|
|
378
|
+
{
|
|
379
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
380
|
+
verbosity: effectiveVerbosity,
|
|
381
|
+
previousResponseId: conv.previousResponseId,
|
|
382
|
+
},
|
|
357
383
|
);
|
|
358
384
|
|
|
359
385
|
if (controller) {
|
|
@@ -153,22 +153,31 @@ export class CodexProvider extends Provider {
|
|
|
153
153
|
return (inputTokens / 1000) * model.pricing.input + (outputTokens / 1000) * model.pricing.output;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
streamChat(messages, model, apiKey, onChunk, onDone, onError) {
|
|
156
|
+
streamChat(messages, model, apiKey, onChunk, onDone, onError, { reasoningEffort, verbosity, previousResponseId } = {}) {
|
|
157
157
|
if (!apiKey) return null;
|
|
158
158
|
const controller = new AbortController();
|
|
159
159
|
let finished = false;
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
let responseId = null;
|
|
161
|
+
const finish = () => { if (!finished) { finished = true; onDone({ responseId }); } };
|
|
162
|
+
|
|
163
|
+
const effort = reasoningEffort || 'medium';
|
|
164
|
+
const verb = verbosity || 'medium';
|
|
165
|
+
const body = {
|
|
166
|
+
model: model || 'gpt-5.5',
|
|
167
|
+
input: previousResponseId ? [messages[messages.length - 1]] : messages,
|
|
168
|
+
stream: true,
|
|
169
|
+
reasoning: { effort },
|
|
170
|
+
text: { format: { type: 'text' }, verbosity: verb },
|
|
171
|
+
};
|
|
172
|
+
if (previousResponseId) body.previous_response_id = previousResponseId;
|
|
173
|
+
|
|
174
|
+
fetch('https://api.openai.com/v1/responses', {
|
|
162
175
|
method: 'POST',
|
|
163
176
|
headers: {
|
|
164
177
|
'Authorization': `Bearer ${apiKey}`,
|
|
165
178
|
'Content-Type': 'application/json',
|
|
166
179
|
},
|
|
167
|
-
body: JSON.stringify(
|
|
168
|
-
model: model || 'gpt-5.4-mini',
|
|
169
|
-
messages,
|
|
170
|
-
stream: true,
|
|
171
|
-
}),
|
|
180
|
+
body: JSON.stringify(body),
|
|
172
181
|
signal: controller.signal,
|
|
173
182
|
}).then((res) => {
|
|
174
183
|
if (!res.ok) {
|
|
@@ -176,8 +185,11 @@ export class CodexProvider extends Provider {
|
|
|
176
185
|
}
|
|
177
186
|
return parseSSEStream(res, (event) => {
|
|
178
187
|
if (event.done) { finish(); return; }
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
if (event.type === 'response.output_text.delta') {
|
|
189
|
+
if (event.delta) onChunk(event.delta);
|
|
190
|
+
} else if (event.type === 'response.completed') {
|
|
191
|
+
responseId = event.response?.id || null;
|
|
192
|
+
}
|
|
181
193
|
});
|
|
182
194
|
}).then(() => {
|
|
183
195
|
finish();
|
|
@@ -313,6 +325,10 @@ export class CodexProvider extends Provider {
|
|
|
313
325
|
};
|
|
314
326
|
}
|
|
315
327
|
|
|
328
|
+
if (result && item.phase !== undefined) {
|
|
329
|
+
result.phase = item.phase;
|
|
330
|
+
}
|
|
331
|
+
|
|
316
332
|
// Attach intermediate context estimate so all 7 layers see Codex progress
|
|
317
333
|
if (result && this._sessionInputTokens > 0) {
|
|
318
334
|
result.contextUsage = this._sessionInputTokens / this._getMaxContext();
|
|
@@ -328,6 +344,7 @@ export class CodexProvider extends Provider {
|
|
|
328
344
|
const inputTokens = usage.input_tokens || 0;
|
|
329
345
|
const outputTokens = usage.output_tokens || 0;
|
|
330
346
|
const cachedTokens = usage.cached_input_tokens || 0;
|
|
347
|
+
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0;
|
|
331
348
|
const totalTokens = inputTokens + outputTokens;
|
|
332
349
|
const cacheCreationTokens = cachedTokens > 0 ? Math.max(0, inputTokens - cachedTokens) : 0;
|
|
333
350
|
|
|
@@ -352,6 +369,7 @@ export class CodexProvider extends Provider {
|
|
|
352
369
|
tokensUsed: totalTokens,
|
|
353
370
|
inputTokens,
|
|
354
371
|
outputTokens,
|
|
372
|
+
reasoningTokens,
|
|
355
373
|
cacheReadTokens: cachedTokens,
|
|
356
374
|
cacheCreationTokens,
|
|
357
375
|
contextUsage: inputTokens / maxContext,
|
|
@@ -43,6 +43,7 @@ export class Registry extends EventEmitter {
|
|
|
43
43
|
lastActivity: null,
|
|
44
44
|
tokensUsed: 0,
|
|
45
45
|
contextUsage: 0,
|
|
46
|
+
filesTouched: {},
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
this.agents.set(agent.id, agent);
|
|
@@ -94,6 +95,35 @@ export class Registry extends EventEmitter {
|
|
|
94
95
|
return this.getAll().filter((a) => a.teamId === teamId);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
trackFileOp(id, filePath, op) {
|
|
99
|
+
const agent = this.agents.get(id);
|
|
100
|
+
if (!agent) return;
|
|
101
|
+
if (!agent.filesTouched) agent.filesTouched = {};
|
|
102
|
+
const entry = agent.filesTouched[filePath] || { reads: 0, writes: 0, lastOp: null };
|
|
103
|
+
if (op === 'read') entry.reads++;
|
|
104
|
+
else entry.writes++;
|
|
105
|
+
entry.lastOp = new Date().toISOString();
|
|
106
|
+
agent.filesTouched[filePath] = entry;
|
|
107
|
+
|
|
108
|
+
const keys = Object.keys(agent.filesTouched);
|
|
109
|
+
if (keys.length > 5000) {
|
|
110
|
+
const sorted = keys
|
|
111
|
+
.map((k) => ({ k, t: agent.filesTouched[k].lastOp || '' }))
|
|
112
|
+
.sort((a, b) => a.t.localeCompare(b.t));
|
|
113
|
+
for (let i = 0; i < keys.length - 5000; i++) {
|
|
114
|
+
delete agent.filesTouched[sorted[i].k];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getFilesTouched(id) {
|
|
120
|
+
const agent = this.agents.get(id);
|
|
121
|
+
if (!agent || !agent.filesTouched) return [];
|
|
122
|
+
return Object.entries(agent.filesTouched)
|
|
123
|
+
.map(([path, info]) => ({ path, reads: info.reads, writes: info.writes, lastOp: info.lastOp }))
|
|
124
|
+
.sort((a, b) => (b.lastOp || '').localeCompare(a.lastOp || ''));
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
restore(agents) {
|
|
98
128
|
for (const agent of agents) {
|
|
99
129
|
agent.status = 'stopped';
|
|
@@ -90,6 +90,9 @@ export function validateAgentConfig(config) {
|
|
|
90
90
|
|
|
91
91
|
const personality = (typeof config.personality === 'string' && config.personality.length > 0 && config.personality.length <= 64 && NAME_PATTERN.test(config.personality)) ? config.personality : undefined;
|
|
92
92
|
|
|
93
|
+
const reasoningEffort = config.reasoning_effort !== undefined && config.reasoning_effort !== null
|
|
94
|
+
? validateReasoningEffort(config.reasoning_effort) : undefined;
|
|
95
|
+
|
|
93
96
|
// Return sanitized config (only known fields)
|
|
94
97
|
return {
|
|
95
98
|
role: config.role,
|
|
@@ -106,6 +109,7 @@ export function validateAgentConfig(config) {
|
|
|
106
109
|
integrationApproval,
|
|
107
110
|
repos,
|
|
108
111
|
personality,
|
|
112
|
+
reasoningEffort,
|
|
109
113
|
};
|
|
110
114
|
}
|
|
111
115
|
|
|
@@ -195,6 +199,25 @@ export function validateGatewayConfig(config) {
|
|
|
195
199
|
};
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
const VALID_REASONING_EFFORTS = ['none', 'low', 'medium', 'high', 'xhigh'];
|
|
203
|
+
const VALID_VERBOSITIES = ['low', 'medium'];
|
|
204
|
+
|
|
205
|
+
export function validateReasoningEffort(value) {
|
|
206
|
+
if (value === null || value === undefined) return null;
|
|
207
|
+
if (typeof value !== 'string' || !VALID_REASONING_EFFORTS.includes(value)) {
|
|
208
|
+
throw new Error(`Invalid reasoning_effort: must be one of ${VALID_REASONING_EFFORTS.join(', ')}`);
|
|
209
|
+
}
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function validateVerbosity(value) {
|
|
214
|
+
if (value === null || value === undefined) return null;
|
|
215
|
+
if (typeof value !== 'string' || !VALID_VERBOSITIES.includes(value)) {
|
|
216
|
+
throw new Error(`Invalid verbosity: must be one of ${VALID_VERBOSITIES.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
|
|
198
221
|
export function escapeMd(text) {
|
|
199
222
|
if (!text) return '';
|
|
200
223
|
// Escape markdown special chars that could break table rendering or inject formatting.
|