@yemi33/minions 0.1.1621 → 0.1.1622
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/CHANGELOG.md +4 -1
- package/dashboard.js +48 -50
- package/engine/copilot-models.json +1 -1
- package/engine/llm.js +72 -216
- package/engine/routing.js +2 -1
- package/engine/runtimes/claude.js +135 -0
- package/engine/runtimes/copilot.js +110 -0
- package/engine.js +7 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1622 (2026-04-29)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
6
|
- harden action block parsing (#1863)
|
|
7
7
|
|
|
8
|
+
### Other
|
|
9
|
+
- refactor: move stream-event handling into runtime adapters
|
|
10
|
+
|
|
8
11
|
## 0.1.1620 (2026-04-29)
|
|
9
12
|
|
|
10
13
|
### Features
|
package/dashboard.js
CHANGED
|
@@ -584,7 +584,7 @@ function _ensureCcLiveStream(tabId) {
|
|
|
584
584
|
tabId,
|
|
585
585
|
text: '',
|
|
586
586
|
tools: [],
|
|
587
|
-
|
|
587
|
+
thinkingSent: false,
|
|
588
588
|
donePayload: null,
|
|
589
589
|
writer: null,
|
|
590
590
|
endResponse: null,
|
|
@@ -4635,6 +4635,40 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4635
4635
|
return out;
|
|
4636
4636
|
}
|
|
4637
4637
|
|
|
4638
|
+
/**
|
|
4639
|
+
* Build the callLLMStreaming invocation for the SSE Command Center path.
|
|
4640
|
+
* Both the initial call and the post-resume-fail retry share the same
|
|
4641
|
+
* onChunk/onToolUse/onThinking shape — only `sessionId` differs (set on
|
|
4642
|
+
* initial call, undefined on retry). Hoisted to keep the two call sites
|
|
4643
|
+
* in lock-step.
|
|
4644
|
+
*/
|
|
4645
|
+
function _invokeCcStream({ prompt, sessionId, liveState, toolUses, model, effort, maxTurns, engineConfig }) {
|
|
4646
|
+
const { callLLMStreaming } = require('./engine/llm');
|
|
4647
|
+
return callLLMStreaming(prompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
4648
|
+
timeout: 900000, label: 'command-center', model, maxTurns,
|
|
4649
|
+
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
4650
|
+
sessionId, effort, direct: true,
|
|
4651
|
+
engineConfig,
|
|
4652
|
+
onChunk: (text) => {
|
|
4653
|
+
const display = stripCCActionsForStream(text);
|
|
4654
|
+
liveState.text = display;
|
|
4655
|
+
// Once text is flowing, the SSE-replay branch (live.thinkingSent &&
|
|
4656
|
+
// !live.text) shouldn't show stale "Thinking…" on reconnect.
|
|
4657
|
+
if (liveState.thinkingSent) liveState.thinkingSent = false;
|
|
4658
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4659
|
+
},
|
|
4660
|
+
onToolUse: (name, input) => {
|
|
4661
|
+
toolUses.push({ name, input: input || {} });
|
|
4662
|
+
liveState.tools.push({ name, input: input || {} });
|
|
4663
|
+
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4664
|
+
},
|
|
4665
|
+
onThinking: () => {
|
|
4666
|
+
liveState.thinkingSent = true;
|
|
4667
|
+
if (liveState.writer) liveState.writer({ type: 'thinking', text: 'Thinking...' });
|
|
4668
|
+
},
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4638
4672
|
async function handleCommandCenterStream(req, res) {
|
|
4639
4673
|
// SSE Origin gate (belt-and-suspenders: the top-level dispatcher has
|
|
4640
4674
|
// already rejected disallowed origins on POST, but validate again here
|
|
@@ -4702,7 +4736,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4702
4736
|
for (const tool of live.tools || []) {
|
|
4703
4737
|
writeCcEvent({ type: 'tool', name: tool.name, input: _lightToolInput(tool.input) });
|
|
4704
4738
|
}
|
|
4705
|
-
if (live.
|
|
4739
|
+
if (live.thinkingSent && !live.text) writeCcEvent({ type: 'thinking', text: 'Thinking...' });
|
|
4706
4740
|
if (live.text) writeCcEvent({ type: 'chunk', text: live.text });
|
|
4707
4741
|
if (live.donePayload) {
|
|
4708
4742
|
writeCcEvent(live.donePayload);
|
|
@@ -4773,33 +4807,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4773
4807
|
const preamble = wasResume ? '' : buildCCStatePreamble();
|
|
4774
4808
|
const prompt = (preamble ? preamble + '\n\n---\n\n' : '') + body.message;
|
|
4775
4809
|
|
|
4776
|
-
const {
|
|
4810
|
+
const { trackEngineUsage: trackUsage } = require('./engine/llm');
|
|
4777
4811
|
const streamModel = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
4778
4812
|
const streamEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
4779
4813
|
const ccMaxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
4780
4814
|
let toolUses = [];
|
|
4781
|
-
const llmPromise =
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
sessionId, effort: streamEffort, direct: true,
|
|
4815
|
+
const llmPromise = _invokeCcStream({
|
|
4816
|
+
prompt, sessionId, liveState, toolUses,
|
|
4817
|
+
model: streamModel, effort: streamEffort, maxTurns: ccMaxTurns,
|
|
4785
4818
|
engineConfig: CONFIG.engine,
|
|
4786
|
-
onChunk: (text) => {
|
|
4787
|
-
const display = stripCCActionsForStream(text);
|
|
4788
|
-
liveState.text = display;
|
|
4789
|
-
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4790
|
-
// Once text is flowing, the SSE-replay branch (live.thinking &&
|
|
4791
|
-
// !live.text) shouldn't show stale "Thinking…" on reconnect.
|
|
4792
|
-
if (liveState.thinking) liveState.thinking = false;
|
|
4793
|
-
},
|
|
4794
|
-
onToolUse: (name, input) => {
|
|
4795
|
-
toolUses.push({ name, input: input || {} });
|
|
4796
|
-
liveState.tools.push({ name, input: input || {} });
|
|
4797
|
-
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4798
|
-
},
|
|
4799
|
-
onThinking: () => {
|
|
4800
|
-
liveState.thinking = true;
|
|
4801
|
-
if (liveState.writer) liveState.writer({ type: 'thinking', text: 'Thinking...' });
|
|
4802
|
-
}
|
|
4803
4819
|
});
|
|
4804
4820
|
_ccStreamAbort = llmPromise.abort;
|
|
4805
4821
|
liveState.abortFn = _ccStreamAbort;
|
|
@@ -4814,33 +4830,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4814
4830
|
const freshPreamble = buildCCStatePreamble();
|
|
4815
4831
|
const freshPrompt = (freshPreamble ? freshPreamble + '\n\n---\n\n' : '') + body.message;
|
|
4816
4832
|
toolUses = []; // discard stale metadata from the failed resume attempt
|
|
4817
|
-
const retryPromise =
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
effort: streamEffort, direct: true,
|
|
4833
|
+
const retryPromise = _invokeCcStream({
|
|
4834
|
+
prompt: freshPrompt, sessionId: undefined, liveState, toolUses,
|
|
4835
|
+
model: streamModel, effort: streamEffort, maxTurns: ccMaxTurns,
|
|
4821
4836
|
engineConfig: CONFIG.engine,
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
// leave a stale "Thinking…" frame visible on SSE reconnect.
|
|
4828
|
-
if (liveState.thinking) liveState.thinking = false;
|
|
4829
|
-
},
|
|
4830
|
-
onToolUse: (name, input) => {
|
|
4831
|
-
toolUses.push({ name, input: input || {} });
|
|
4832
|
-
liveState.tools.push({ name, input: input || {} });
|
|
4833
|
-
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4834
|
-
},
|
|
4835
|
-
onThinking: () => {
|
|
4836
|
-
liveState.thinking = true;
|
|
4837
|
-
if (liveState.writer) liveState.writer({ type: 'thinking', text: 'Thinking...' });
|
|
4838
|
-
}
|
|
4839
|
-
});
|
|
4840
|
-
_ccStreamAbort = retryPromise.abort;
|
|
4841
|
-
liveState.abortFn = _ccStreamAbort;
|
|
4842
|
-
ccInFlightAborts.set(tabId, _ccStreamAbort);
|
|
4843
|
-
const retryResult = await retryPromise;
|
|
4837
|
+
});
|
|
4838
|
+
_ccStreamAbort = retryPromise.abort;
|
|
4839
|
+
liveState.abortFn = _ccStreamAbort;
|
|
4840
|
+
ccInFlightAborts.set(tabId, _ccStreamAbort);
|
|
4841
|
+
const retryResult = await retryPromise;
|
|
4844
4842
|
trackUsage('command-center', retryResult.usage);
|
|
4845
4843
|
if (retryResult.text) {
|
|
4846
4844
|
// Fresh session succeeded — use retryResult from here
|
package/engine/llm.js
CHANGED
|
@@ -24,10 +24,6 @@ const MINIONS_DIR = shared.MINIONS_DIR;
|
|
|
24
24
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
25
25
|
const COPILOT_TASK_COMPLETE_GRACE_MS = 3000;
|
|
26
26
|
|
|
27
|
-
// Claude content blocks come in two thinking variants; hoisted to module scope
|
|
28
|
-
// so the streaming accumulator's hot path doesn't recreate the set per event.
|
|
29
|
-
const THINKING_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking']);
|
|
30
|
-
|
|
31
27
|
// ─── Engine-Usage Metrics ────────────────────────────────────────────────────
|
|
32
28
|
|
|
33
29
|
function trackEngineUsage(category, usage) {
|
|
@@ -246,12 +242,12 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
246
242
|
// ─── Streaming Accumulator ───────────────────────────────────────────────────
|
|
247
243
|
//
|
|
248
244
|
// Reads JSONL events as they stream in. JSON parsing is delegated to
|
|
249
|
-
// `runtime.parseStreamChunk()`
|
|
250
|
-
//
|
|
245
|
+
// `runtime.parseStreamChunk()` and event-shape interpretation is delegated to
|
|
246
|
+
// `runtime.createStreamConsumer(ctx)`. This file stays runtime-agnostic — it
|
|
247
|
+
// owns the global accumulator state (stdout/stderr/text dedup/toolUses) and
|
|
248
|
+
// exposes a `ctx` callback API the adapter calls when it sees Claude- or
|
|
249
|
+
// Copilot-shaped events.
|
|
251
250
|
//
|
|
252
|
-
// Text / tool extraction branches on event SHAPE rather than runtime identity.
|
|
253
|
-
// Both Claude and Copilot events flow through here; for any given object only
|
|
254
|
-
// one branch matches because the event type strings don't collide.
|
|
255
251
|
// Final reconciliation calls `runtime.parseOutput(stdout)` so per-runtime
|
|
256
252
|
// finalization quirks (Copilot's premiumRequests, Claude's session_id) stay
|
|
257
253
|
// inside the adapter.
|
|
@@ -267,6 +263,10 @@ function _createStreamAccumulator({
|
|
|
267
263
|
onTaskComplete = null,
|
|
268
264
|
onThinking = null,
|
|
269
265
|
}) {
|
|
266
|
+
if (!runtime?.capabilities?.streamConsumer || typeof runtime.createStreamConsumer !== 'function') {
|
|
267
|
+
throw new Error(`runtime ${runtime?.name || '<unknown>'} missing createStreamConsumer (capabilities.streamConsumer)`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
270
|
let stdout = '';
|
|
271
271
|
let stderr = '';
|
|
272
272
|
let lineBuf = '';
|
|
@@ -274,217 +274,76 @@ function _createStreamAccumulator({
|
|
|
274
274
|
let usage = null;
|
|
275
275
|
let sessionId = null;
|
|
276
276
|
let lastTextSent = '';
|
|
277
|
-
const toolUses = [];
|
|
278
|
-
|
|
279
|
-
// Copilot streams `assistant.message_delta` with `data.deltaContent` chunks
|
|
280
|
-
// before emitting `assistant.message`. Tool-request messages can include
|
|
281
|
-
// narration ("I'll inspect...") that is only progress text, so terminal text
|
|
282
|
-
// comes from non-tool assistant messages or trailing deltas.
|
|
283
|
-
let copilotMessageBuffer = '';
|
|
284
|
-
let copilotTaskCompleteSeen = false;
|
|
285
|
-
let copilotTaskCompleteSummary = '';
|
|
286
|
-
const claudeStreamBlocks = new Map();
|
|
287
|
-
// Maintained accumulator of Claude text — incrementally appended on each
|
|
288
|
-
// text_delta so the hot path doesn't rebuild from the Map every chunk
|
|
289
|
-
// (rebuild was O(n) per delta → O(n²) over the response).
|
|
290
|
-
let claudeJoinedText = '';
|
|
291
277
|
let thinkingSent = false;
|
|
278
|
+
let taskCompleteFired = false;
|
|
279
|
+
let lastTaskCompleteSummary = '';
|
|
280
|
+
const toolUses = [];
|
|
292
281
|
|
|
293
282
|
function _streamText(value) {
|
|
294
283
|
return (maxTextLength && value.length > maxTextLength) ? value.slice(-maxTextLength) : value;
|
|
295
284
|
}
|
|
296
285
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
// Rebuild the joined text from the Map. Only used as a safety net when
|
|
309
|
-
// content blocks arrive out of order (a non-trailing index lands after a
|
|
310
|
-
// later one — rare but possible if events get reordered upstream).
|
|
311
|
-
function _rebuildClaudeJoinedText() {
|
|
312
|
-
claudeJoinedText = Array.from(claudeStreamBlocks.keys()).sort((a, b) => a - b)
|
|
313
|
-
.map(index => claudeStreamBlocks.get(index))
|
|
314
|
-
.filter(block => block && block.type === 'text' && block.text)
|
|
315
|
-
.map(block => block.text)
|
|
316
|
-
.join('');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function _captureClaudeText(value) {
|
|
320
|
-
if (typeof value !== 'string' || !value) return;
|
|
321
|
-
const nextText = _streamText(value);
|
|
322
|
-
text = nextText;
|
|
323
|
-
if (onChunk && nextText !== lastTextSent) {
|
|
324
|
-
lastTextSent = nextText;
|
|
325
|
-
onChunk(nextText);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function _captureClaudeStreamEvent(obj) {
|
|
330
|
-
const event = obj?.event;
|
|
331
|
-
if (!event || typeof event !== 'object') return false;
|
|
332
|
-
if (event.type === 'message_start') {
|
|
333
|
-
claudeStreamBlocks.clear();
|
|
334
|
-
claudeJoinedText = '';
|
|
335
|
-
thinkingSent = false;
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
if (event.type === 'content_block_start') {
|
|
339
|
-
const index = Number.isInteger(event.index) ? event.index : Number(event.index) || 0;
|
|
340
|
-
const block = event.content_block || {};
|
|
341
|
-
claudeStreamBlocks.set(index, { type: block.type || '', text: block.text || '' });
|
|
342
|
-
if (THINKING_BLOCK_TYPES.has(block.type)) _notifyThinking();
|
|
343
|
-
// If a block lands at a non-trailing index (out-of-order delivery), the
|
|
344
|
-
// monotonic-append path can't reconstruct the joined text — rebuild as
|
|
345
|
-
// a safety net. The common case is in-order arrival; rebuild is rare.
|
|
346
|
-
const indices = Array.from(claudeStreamBlocks.keys());
|
|
347
|
-
const isTrailing = indices.every(i => i <= index);
|
|
348
|
-
if (!isTrailing) {
|
|
349
|
-
_rebuildClaudeJoinedText();
|
|
350
|
-
} else if (block.type === 'text' && block.text) {
|
|
351
|
-
claudeJoinedText += block.text;
|
|
352
|
-
}
|
|
353
|
-
if (claudeJoinedText) _captureClaudeText(claudeJoinedText);
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
if (event.type === 'content_block_delta') {
|
|
357
|
-
const index = Number.isInteger(event.index) ? event.index : Number(event.index) || 0;
|
|
358
|
-
const delta = event.delta || {};
|
|
359
|
-
if (delta.type === 'thinking_delta' || typeof delta.thinking === 'string') _notifyThinking();
|
|
360
|
-
if (delta.type === 'text_delta' && typeof delta.text === 'string' && delta.text) {
|
|
361
|
-
const block = claudeStreamBlocks.get(index) || { type: 'text', text: '' };
|
|
362
|
-
block.type = 'text';
|
|
363
|
-
block.text = (block.text || '') + delta.text;
|
|
364
|
-
claudeStreamBlocks.set(index, block);
|
|
365
|
-
// Common case: deltas arrive monotonically per index, so appending to
|
|
366
|
-
// the joined accumulator directly is correct.
|
|
367
|
-
claudeJoinedText += delta.text;
|
|
368
|
-
_captureClaudeText(claudeJoinedText);
|
|
369
|
-
}
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
return event.type === 'content_block_stop' || event.type === 'message_delta' || event.type === 'message_stop';
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function _captureCopilotTaskComplete(summary, success = true) {
|
|
376
|
-
if (typeof summary !== 'string' || !summary) return;
|
|
377
|
-
const finalSummary = _streamText(summary);
|
|
378
|
-
const alreadySeen = copilotTaskCompleteSeen && copilotTaskCompleteSummary === finalSummary;
|
|
379
|
-
copilotTaskCompleteSeen = true;
|
|
380
|
-
copilotTaskCompleteSummary = finalSummary;
|
|
381
|
-
const hadText = !!text;
|
|
382
|
-
if (!hadText) {
|
|
383
|
-
text = finalSummary;
|
|
384
|
-
if (onChunk && finalSummary !== lastTextSent) {
|
|
385
|
-
lastTextSent = finalSummary;
|
|
386
|
-
onChunk(finalSummary);
|
|
286
|
+
// ── ctx surface — the only API the runtime stream consumer sees ─────────
|
|
287
|
+
const ctx = {
|
|
288
|
+
maxTextLength,
|
|
289
|
+
pushText(value) {
|
|
290
|
+
if (typeof value !== 'string' || !value) return;
|
|
291
|
+
const next = _streamText(value);
|
|
292
|
+
text = next;
|
|
293
|
+
if (onChunk && next !== lastTextSent) {
|
|
294
|
+
lastTextSent = next;
|
|
295
|
+
onChunk(next);
|
|
387
296
|
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
assistantText += block.text;
|
|
425
|
-
} else if (THINKING_BLOCK_TYPES.has(block?.type)) {
|
|
426
|
-
_notifyThinking();
|
|
427
|
-
} else if (block?.type === 'tool_use' && block.name) {
|
|
428
|
-
const toolUse = { name: block.name, input: block.input || {} };
|
|
429
|
-
toolUses.push(toolUse);
|
|
430
|
-
if (onToolUse) onToolUse(toolUse.name, toolUse.input);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (assistantText) _captureClaudeText(assistantText);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ── Copilot shape ───────────────────────────────────────────────────────
|
|
437
|
-
if (obj.type === 'result' && typeof obj.sessionId === 'string') sessionId = obj.sessionId;
|
|
438
|
-
if (obj.type === 'session.task_complete') {
|
|
439
|
-
_captureCopilotTaskComplete(obj.data?.summary, obj.data?.success);
|
|
440
|
-
}
|
|
441
|
-
if (obj.type === 'assistant.reasoning' || obj.type === 'assistant.reasoning_delta') {
|
|
442
|
-
_notifyThinking();
|
|
443
|
-
}
|
|
444
|
-
if (obj.type === 'assistant.message_delta' && typeof obj.data?.deltaContent === 'string') {
|
|
445
|
-
if (copilotTaskCompleteSeen) return;
|
|
446
|
-
copilotMessageBuffer += obj.data.deltaContent;
|
|
447
|
-
if (onChunk && copilotMessageBuffer !== lastTextSent) {
|
|
448
|
-
lastTextSent = copilotMessageBuffer;
|
|
449
|
-
onChunk(copilotMessageBuffer);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (obj.type === 'assistant.message' && typeof obj.data?.content === 'string') {
|
|
453
|
-
// Tool-request narration ("I'll look into this...") is progress text, not
|
|
454
|
-
// the final answer. Keep streaming it live, but don't let it become the
|
|
455
|
-
// terminal result if the process exits before a final answer message.
|
|
456
|
-
const content = obj.data.content;
|
|
457
|
-
if (content && !_copilotAssistantMessageHasTools(obj)) text = _streamText(content);
|
|
458
|
-
copilotMessageBuffer = '';
|
|
459
|
-
if (Array.isArray(obj.data.toolRequests)) {
|
|
460
|
-
for (const tr of obj.data.toolRequests) {
|
|
461
|
-
if (tr && tr.name) {
|
|
462
|
-
if (tr.name === 'task_complete') {
|
|
463
|
-
_captureCopilotTaskComplete(tr.arguments?.summary || tr.intentionSummary);
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
const toolUse = { name: tr.name, input: tr.arguments || {} };
|
|
467
|
-
toolUses.push(toolUse);
|
|
468
|
-
if (onToolUse) onToolUse(toolUse.name, toolUse.input);
|
|
469
|
-
}
|
|
297
|
+
},
|
|
298
|
+
setText(value) {
|
|
299
|
+
// Hard-set text bypassing dedup — for terminal events that should
|
|
300
|
+
// override any streamed text (Claude's `result`, Copilot's final
|
|
301
|
+
// assistant.message). onChunk is NOT fired here; this is the
|
|
302
|
+
// authoritative final-text path, not a streaming chunk.
|
|
303
|
+
if (typeof value !== 'string') return;
|
|
304
|
+
text = _streamText(value);
|
|
305
|
+
},
|
|
306
|
+
pushToolUse(name, input) {
|
|
307
|
+
if (!name) return;
|
|
308
|
+
const toolUse = { name, input: input || {} };
|
|
309
|
+
toolUses.push(toolUse);
|
|
310
|
+
if (onToolUse) onToolUse(toolUse.name, toolUse.input);
|
|
311
|
+
},
|
|
312
|
+
toolUseAlreadySeen(name, input) {
|
|
313
|
+
if (!name) return false;
|
|
314
|
+
const stringified = JSON.stringify(input || {});
|
|
315
|
+
return toolUses.some(t => t.name === name && JSON.stringify(t.input) === stringified);
|
|
316
|
+
},
|
|
317
|
+
notifyThinking() {
|
|
318
|
+
if (!onThinking || thinkingSent) return;
|
|
319
|
+
thinkingSent = true;
|
|
320
|
+
onThinking();
|
|
321
|
+
},
|
|
322
|
+
notifyTaskComplete(summary, success = true) {
|
|
323
|
+
if (typeof summary !== 'string' || !summary) return;
|
|
324
|
+
const finalSummary = _streamText(summary);
|
|
325
|
+
const alreadySeen = taskCompleteFired && lastTaskCompleteSummary === finalSummary;
|
|
326
|
+
lastTaskCompleteSummary = finalSummary;
|
|
327
|
+
// Surface as terminal text only if nothing streamed yet.
|
|
328
|
+
if (!text) {
|
|
329
|
+
text = finalSummary;
|
|
330
|
+
if (onChunk && finalSummary !== lastTextSent) {
|
|
331
|
+
lastTextSent = finalSummary;
|
|
332
|
+
onChunk(finalSummary);
|
|
470
333
|
}
|
|
471
334
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
const toolUse = { name: obj.data.toolName, input: obj.data.arguments || {} };
|
|
479
|
-
// Dedup: assistant.message.toolRequests already adds this — only push if
|
|
480
|
-
// we haven't seen it yet (toolCallId would be the unique key, but we
|
|
481
|
-
// compare by name+input shape since not every consumer cares).
|
|
482
|
-
if (!toolUses.some(t => t.name === toolUse.name && JSON.stringify(t.input) === JSON.stringify(toolUse.input))) {
|
|
483
|
-
toolUses.push(toolUse);
|
|
484
|
-
if (onToolUse) onToolUse(toolUse.name, toolUse.input);
|
|
335
|
+
if (!alreadySeen && onTaskComplete) {
|
|
336
|
+
taskCompleteFired = true;
|
|
337
|
+
onTaskComplete({ summary: finalSummary, success: success !== false });
|
|
338
|
+
} else {
|
|
339
|
+
taskCompleteFired = true;
|
|
485
340
|
}
|
|
486
|
-
}
|
|
487
|
-
|
|
341
|
+
},
|
|
342
|
+
setUsage(u) { if (u) usage = u; },
|
|
343
|
+
setSessionId(id) { if (typeof id === 'string' && id) sessionId = id; },
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const consumer = runtime.createStreamConsumer(ctx);
|
|
488
347
|
|
|
489
348
|
function ingestStdout(chunk) {
|
|
490
349
|
const str = chunk == null ? '' : chunk.toString();
|
|
@@ -494,7 +353,7 @@ function _createStreamAccumulator({
|
|
|
494
353
|
lineBuf = lines.pop() || '';
|
|
495
354
|
for (const line of lines) {
|
|
496
355
|
const ev = runtime.parseStreamChunk(line);
|
|
497
|
-
if (ev)
|
|
356
|
+
if (ev) consumer.consume(ev);
|
|
498
357
|
}
|
|
499
358
|
}
|
|
500
359
|
|
|
@@ -506,12 +365,9 @@ function _createStreamAccumulator({
|
|
|
506
365
|
const trimmed = lineBuf.trim();
|
|
507
366
|
if (trimmed) {
|
|
508
367
|
const ev = runtime.parseStreamChunk(trimmed);
|
|
509
|
-
if (ev)
|
|
510
|
-
}
|
|
511
|
-
if (copilotMessageBuffer && !copilotTaskCompleteSeen) {
|
|
512
|
-
text = _streamText(copilotMessageBuffer);
|
|
368
|
+
if (ev) consumer.consume(ev);
|
|
513
369
|
}
|
|
514
|
-
if (!text &&
|
|
370
|
+
if (!text && lastTaskCompleteSummary) text = lastTaskCompleteSummary;
|
|
515
371
|
// Reconciliation: if any field is still missing, ask the runtime adapter
|
|
516
372
|
// to re-parse the whole stdout. parseOutput() may catch a result event
|
|
517
373
|
// that was malformed when streamed in chunks.
|
package/engine/routing.js
CHANGED
|
@@ -156,7 +156,8 @@ function normalizeAgentHints(agentHints, authorAgent = null, agents = null) {
|
|
|
156
156
|
return normalized;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
function resolveAgent(workType, config,
|
|
159
|
+
function resolveAgent(workType, config, opts = {}) {
|
|
160
|
+
const { authorAgent = null, agentHints = null } = opts || {};
|
|
160
161
|
const routes = getRoutingTableCached();
|
|
161
162
|
const route = routes[workType] || routes['implement'] || { preferred: '_any_', fallback: '_any_' };
|
|
162
163
|
const agents = config.agents || {};
|
|
@@ -360,6 +360,137 @@ function parseError(rawOutput) {
|
|
|
360
360
|
return { message: '', code: null, retriable: true };
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
// ── Stream Consumer ─────────────────────────────────────────────────────────
|
|
364
|
+
//
|
|
365
|
+
// Per-stream consumer factory invoked by engine/llm.js's accumulator. The
|
|
366
|
+
// accumulator owns global stream state (stdout/stderr/text dedup/tool dedup)
|
|
367
|
+
// and exposes the `ctx` API below; the consumer owns Claude-specific per-stream
|
|
368
|
+
// state (joined-text accumulator, content-block Map for tool/thinking
|
|
369
|
+
// tracking) and translates Claude event shapes into ctx callbacks.
|
|
370
|
+
//
|
|
371
|
+
// `ctx` shape (provided by accumulator):
|
|
372
|
+
// maxTextLength, pushText(value), pushToolUse(name, input),
|
|
373
|
+
// notifyThinking(), notifyTaskComplete(summary, success),
|
|
374
|
+
// setUsage(usage), setSessionId(id), setText(value),
|
|
375
|
+
// toolUseAlreadySeen(name, input)
|
|
376
|
+
|
|
377
|
+
const THINKING_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking']);
|
|
378
|
+
|
|
379
|
+
function createStreamConsumer(ctx) {
|
|
380
|
+
// Per-stream local state. `claudeStreamBlocks` is kept for Map-based
|
|
381
|
+
// bookkeeping (tool-use blocks, thinking events, out-of-order text-block
|
|
382
|
+
// reassembly). The incremental `claudeJoinedText` string is the hot-path
|
|
383
|
+
// accumulator — appending one delta at a time keeps the stream loop O(n).
|
|
384
|
+
let claudeJoinedText = '';
|
|
385
|
+
const claudeStreamBlocks = new Map();
|
|
386
|
+
|
|
387
|
+
function _rebuildClaudeJoinedText() {
|
|
388
|
+
claudeJoinedText = Array.from(claudeStreamBlocks.keys()).sort((a, b) => a - b)
|
|
389
|
+
.map(index => claudeStreamBlocks.get(index))
|
|
390
|
+
.filter(block => block && block.type === 'text' && block.text)
|
|
391
|
+
.map(block => block.text)
|
|
392
|
+
.join('');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function _consumeStreamEvent(obj) {
|
|
396
|
+
const event = obj?.event;
|
|
397
|
+
if (!event || typeof event !== 'object') return;
|
|
398
|
+
if (event.type === 'message_start') {
|
|
399
|
+
claudeStreamBlocks.clear();
|
|
400
|
+
claudeJoinedText = '';
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (event.type === 'content_block_start') {
|
|
404
|
+
const index = Number.isInteger(event.index) ? event.index : Number(event.index) || 0;
|
|
405
|
+
const block = event.content_block || {};
|
|
406
|
+
claudeStreamBlocks.set(index, { type: block.type || '', text: block.text || '' });
|
|
407
|
+
if (THINKING_BLOCK_TYPES.has(block.type)) ctx.notifyThinking();
|
|
408
|
+
// Out-of-order block landing: rebuild from the Map. Common case is
|
|
409
|
+
// monotonic in-order arrival, where the trailing-append branch wins.
|
|
410
|
+
const indices = Array.from(claudeStreamBlocks.keys());
|
|
411
|
+
const isTrailing = indices.every(i => i <= index);
|
|
412
|
+
if (!isTrailing) {
|
|
413
|
+
_rebuildClaudeJoinedText();
|
|
414
|
+
} else if (block.type === 'text' && block.text) {
|
|
415
|
+
claudeJoinedText += block.text;
|
|
416
|
+
}
|
|
417
|
+
if (claudeJoinedText) ctx.pushText(claudeJoinedText);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (event.type === 'content_block_delta') {
|
|
421
|
+
const index = Number.isInteger(event.index) ? event.index : Number(event.index) || 0;
|
|
422
|
+
const delta = event.delta || {};
|
|
423
|
+
if (delta.type === 'thinking_delta' || typeof delta.thinking === 'string') ctx.notifyThinking();
|
|
424
|
+
if (delta.type === 'text_delta' && typeof delta.text === 'string' && delta.text) {
|
|
425
|
+
const block = claudeStreamBlocks.get(index) || { type: 'text', text: '' };
|
|
426
|
+
block.type = 'text';
|
|
427
|
+
block.text = (block.text || '') + delta.text;
|
|
428
|
+
claudeStreamBlocks.set(index, block);
|
|
429
|
+
// Common case: deltas arrive monotonically per index — append directly.
|
|
430
|
+
claudeJoinedText += delta.text;
|
|
431
|
+
ctx.pushText(claudeJoinedText);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// content_block_stop / message_delta / message_stop are observed but the
|
|
436
|
+
// accumulator doesn't need to act on them — terminal text comes via the
|
|
437
|
+
// result event below.
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function consume(obj) {
|
|
441
|
+
if (!obj || typeof obj !== 'object') return;
|
|
442
|
+
|
|
443
|
+
if (obj.session_id) ctx.setSessionId(obj.session_id);
|
|
444
|
+
|
|
445
|
+
if (obj.type === 'stream_event') {
|
|
446
|
+
_consumeStreamEvent(obj);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
451
|
+
// Claude result event: terminal text + usage. Override any previously
|
|
452
|
+
// streamed text — this is the authoritative final answer.
|
|
453
|
+
ctx.setText(obj.result);
|
|
454
|
+
if (obj.total_cost_usd || obj.usage) {
|
|
455
|
+
ctx.setUsage({
|
|
456
|
+
costUsd: obj.total_cost_usd || 0,
|
|
457
|
+
inputTokens: obj.usage?.input_tokens || 0,
|
|
458
|
+
outputTokens: obj.usage?.output_tokens || 0,
|
|
459
|
+
cacheRead: obj.usage?.cache_read_input_tokens || obj.usage?.cacheReadInputTokens || 0,
|
|
460
|
+
cacheCreation: obj.usage?.cache_creation_input_tokens || obj.usage?.cacheCreationInputTokens || 0,
|
|
461
|
+
durationMs: obj.duration_ms || 0,
|
|
462
|
+
numTurns: obj.num_turns || 0,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (obj.type === 'assistant' && Array.isArray(obj.message?.content)) {
|
|
469
|
+
// Claude assistant turn: content blocks (text + tool_use).
|
|
470
|
+
// Multi-text-block messages (with --include-partial-messages) need their
|
|
471
|
+
// text JOINED before pushText, otherwise each block overwrites the prior.
|
|
472
|
+
let assistantText = '';
|
|
473
|
+
for (const block of obj.message.content) {
|
|
474
|
+
if (block?.type === 'text' && block.text) {
|
|
475
|
+
assistantText += block.text;
|
|
476
|
+
} else if (THINKING_BLOCK_TYPES.has(block?.type)) {
|
|
477
|
+
ctx.notifyThinking();
|
|
478
|
+
} else if (block?.type === 'tool_use' && block.name) {
|
|
479
|
+
ctx.pushToolUse(block.name, block.input || {});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (assistantText) ctx.pushText(assistantText);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function reset() {
|
|
487
|
+
claudeJoinedText = '';
|
|
488
|
+
claudeStreamBlocks.clear();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return { consume, reset };
|
|
492
|
+
}
|
|
493
|
+
|
|
363
494
|
// ── Capability Block ────────────────────────────────────────────────────────
|
|
364
495
|
|
|
365
496
|
const capabilities = {
|
|
@@ -387,6 +518,8 @@ const capabilities = {
|
|
|
387
518
|
fallbackModel: true,
|
|
388
519
|
// Engine controls session persistence (writes session.json on completion)
|
|
389
520
|
sessionPersistenceControl: true,
|
|
521
|
+
// Adapter implements createStreamConsumer(ctx) — required by llm.js accumulator
|
|
522
|
+
streamConsumer: true,
|
|
390
523
|
};
|
|
391
524
|
|
|
392
525
|
// Install hint surfaced when `resolveBinary()` returns null. Consumed by
|
|
@@ -409,6 +542,8 @@ module.exports = {
|
|
|
409
542
|
parseOutput,
|
|
410
543
|
parseStreamChunk,
|
|
411
544
|
parseError,
|
|
545
|
+
createStreamConsumer,
|
|
412
546
|
// Exposed for unit tests — never imported by engine code
|
|
413
547
|
_CLAUDE_SHORTHANDS,
|
|
548
|
+
THINKING_BLOCK_TYPES,
|
|
414
549
|
};
|
|
@@ -529,6 +529,112 @@ async function listModels({ env = process.env, timeoutMs = 10000 } = {}) {
|
|
|
529
529
|
return models;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
// ── Stream Consumer ─────────────────────────────────────────────────────────
|
|
533
|
+
//
|
|
534
|
+
// Per-stream consumer factory invoked by engine/llm.js's accumulator. Owns
|
|
535
|
+
// Copilot-specific per-stream state (delta-content buffer, task_complete
|
|
536
|
+
// signal). Translates Copilot event shapes into ctx callbacks.
|
|
537
|
+
//
|
|
538
|
+
// `ctx` shape (provided by accumulator):
|
|
539
|
+
// maxTextLength, pushText(value), pushToolUse(name, input),
|
|
540
|
+
// notifyThinking(), notifyTaskComplete(summary, success),
|
|
541
|
+
// setUsage(usage), setSessionId(id), setText(value),
|
|
542
|
+
// toolUseAlreadySeen(name, input)
|
|
543
|
+
|
|
544
|
+
function _copilotAssistantMessageHasTools(obj) {
|
|
545
|
+
const requests = obj?.data?.toolRequests;
|
|
546
|
+
return Array.isArray(requests) && requests.length > 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function createStreamConsumer(ctx) {
|
|
550
|
+
// Copilot streams `assistant.message_delta` with `data.deltaContent` chunks
|
|
551
|
+
// before emitting `assistant.message`. Tool-request narration ("I'll
|
|
552
|
+
// inspect...") is progress text only — terminal text comes from non-tool
|
|
553
|
+
// assistant messages or trailing deltas.
|
|
554
|
+
let copilotMessageBuffer = '';
|
|
555
|
+
let copilotTaskCompleteSeen = false;
|
|
556
|
+
|
|
557
|
+
function _captureTaskComplete(summary, success = true) {
|
|
558
|
+
if (typeof summary !== 'string' || !summary) return;
|
|
559
|
+
copilotTaskCompleteSeen = true;
|
|
560
|
+
copilotMessageBuffer = '';
|
|
561
|
+
ctx.notifyTaskComplete(summary, success !== false);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function consume(obj) {
|
|
565
|
+
if (!obj || typeof obj !== 'object') return;
|
|
566
|
+
|
|
567
|
+
if (obj.type === 'result' && typeof obj.sessionId === 'string') {
|
|
568
|
+
ctx.setSessionId(obj.sessionId);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (obj.type === 'session.task_complete') {
|
|
572
|
+
_captureTaskComplete(obj.data?.summary, obj.data?.success);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (obj.type === 'assistant.reasoning' || obj.type === 'assistant.reasoning_delta') {
|
|
577
|
+
ctx.notifyThinking();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (obj.type === 'assistant.message_delta' && typeof obj.data?.deltaContent === 'string') {
|
|
582
|
+
if (copilotTaskCompleteSeen) return;
|
|
583
|
+
copilotMessageBuffer += obj.data.deltaContent;
|
|
584
|
+
ctx.pushText(copilotMessageBuffer);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (obj.type === 'assistant.message') {
|
|
589
|
+
// Process toolRequests EVEN WHEN data.content is undefined — tool-only
|
|
590
|
+
// assistant messages would otherwise be dropped (earlier review bug:
|
|
591
|
+
// the `typeof data.content === 'string'` gate skipped them entirely).
|
|
592
|
+
const data = obj.data || {};
|
|
593
|
+
const content = data.content;
|
|
594
|
+
const hasTools = _copilotAssistantMessageHasTools(obj);
|
|
595
|
+
if (typeof content === 'string') {
|
|
596
|
+
// Tool-request narration is progress text only — don't let it become
|
|
597
|
+
// the terminal answer. A non-tool assistant.message overrides any
|
|
598
|
+
// streamed deltas (Copilot's authoritative final text for the turn).
|
|
599
|
+
if (content && !hasTools) ctx.setText(content);
|
|
600
|
+
copilotMessageBuffer = '';
|
|
601
|
+
}
|
|
602
|
+
if (Array.isArray(data.toolRequests)) {
|
|
603
|
+
for (const tr of data.toolRequests) {
|
|
604
|
+
if (!tr || !tr.name) continue;
|
|
605
|
+
if (tr.name === 'task_complete') {
|
|
606
|
+
_captureTaskComplete(tr.arguments?.summary || tr.intentionSummary);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
ctx.pushToolUse(tr.name, tr.arguments || {});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (obj.type === 'tool.execution_start' && obj.data?.toolName) {
|
|
616
|
+
if (obj.data.toolName === 'task_complete') {
|
|
617
|
+
_captureTaskComplete(obj.data.arguments?.summary);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const name = obj.data.toolName;
|
|
621
|
+
const input = obj.data.arguments || {};
|
|
622
|
+
// Dedup against assistant.message.toolRequests — accumulator tracks
|
|
623
|
+
// the toolUses array and exposes a same-name+input check.
|
|
624
|
+
if (!ctx.toolUseAlreadySeen(name, input)) {
|
|
625
|
+
ctx.pushToolUse(name, input);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function reset() {
|
|
631
|
+
copilotMessageBuffer = '';
|
|
632
|
+
copilotTaskCompleteSeen = false;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return { consume, reset };
|
|
636
|
+
}
|
|
637
|
+
|
|
532
638
|
// ── Capability Block ────────────────────────────────────────────────────────
|
|
533
639
|
|
|
534
640
|
const capabilities = {
|
|
@@ -556,6 +662,8 @@ const capabilities = {
|
|
|
556
662
|
fallbackModel: false,
|
|
557
663
|
// Copilot manages session state internally in ~/.copilot/session-state/
|
|
558
664
|
sessionPersistenceControl: false,
|
|
665
|
+
// Adapter implements createStreamConsumer(ctx) — required by llm.js accumulator
|
|
666
|
+
streamConsumer: true,
|
|
559
667
|
};
|
|
560
668
|
|
|
561
669
|
// Install hint surfaced when `resolveBinary()` returns null. Covers all
|
|
@@ -582,10 +690,12 @@ module.exports = {
|
|
|
582
690
|
parseOutput,
|
|
583
691
|
parseStreamChunk,
|
|
584
692
|
parseError,
|
|
693
|
+
createStreamConsumer,
|
|
585
694
|
// Exposed for unit tests — engine code MUST go through resolveRuntime + the
|
|
586
695
|
// adapter contract; never reach into these helpers directly.
|
|
587
696
|
_CLAUDE_SHORTHANDS,
|
|
588
697
|
_resetShorthandWarning,
|
|
589
698
|
_mapEffort,
|
|
699
|
+
_copilotAssistantMessageHasTools,
|
|
590
700
|
KNOWN_EVENT_TYPES,
|
|
591
701
|
};
|
package/engine.js
CHANGED
|
@@ -2171,7 +2171,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2171
2171
|
if (evalLoopEnabled && reviewStatus === 'changes-requested' && !awaitingReReview && !evalEscalated) {
|
|
2172
2172
|
const key = `fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2173
2173
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2174
|
-
const agentId = resolveAgent('fix', config, pr.agent);
|
|
2174
|
+
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2175
2175
|
if (!agentId) continue;
|
|
2176
2176
|
|
|
2177
2177
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
@@ -2210,7 +2210,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2210
2210
|
}
|
|
2211
2211
|
continue;
|
|
2212
2212
|
}
|
|
2213
|
-
const agentId = resolveAgent('fix', config, pr.agent);
|
|
2213
|
+
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2214
2214
|
if (!agentId) continue;
|
|
2215
2215
|
|
|
2216
2216
|
const coalesced = [...staleCoalesced, ...getCoalescedContexts(key)];
|
|
@@ -2290,7 +2290,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2290
2290
|
}
|
|
2291
2291
|
} catch (e) { log('warn', `Pre-dispatch build check for ${pr.id}: ${e.message} — skipping dispatch`); continue; }
|
|
2292
2292
|
|
|
2293
|
-
const agentId = resolveAgent('fix', config, pr.agent);
|
|
2293
|
+
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2294
2294
|
if (!agentId) continue;
|
|
2295
2295
|
|
|
2296
2296
|
let reviewNote = `Build is failing: ${pr.buildFailReason || 'Check CI pipeline for details'}. Fix the build errors and push.`;
|
|
@@ -2365,7 +2365,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2365
2365
|
} catch (e) { log('warn', `Pre-dispatch conflict check for ${pr.id}: ${e.message} — skipping dispatch`); liveSkip = true; }
|
|
2366
2366
|
|
|
2367
2367
|
if (!liveSkip) {
|
|
2368
|
-
const agentId = resolveAgent('fix', config, pr.agent);
|
|
2368
|
+
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2369
2369
|
if (agentId) {
|
|
2370
2370
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2371
2371
|
pr_id: pr.id, pr_branch: pr.branch || '',
|
|
@@ -2502,7 +2502,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2502
2502
|
needsWrite = true;
|
|
2503
2503
|
}
|
|
2504
2504
|
const agentHints = routing.extractAgentHints(item);
|
|
2505
|
-
const agentId = item.agent || resolveAgent(workType, config,
|
|
2505
|
+
const agentId = item.agent || resolveAgent(workType, config, { agentHints });
|
|
2506
2506
|
if (!agentId) {
|
|
2507
2507
|
// Check if reason is budget
|
|
2508
2508
|
const cfgAgents = config.agents || {};
|
|
@@ -3022,7 +3022,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3022
3022
|
} else {
|
|
3023
3023
|
// ─── Normal: single agent dispatch ──────────────────────────────
|
|
3024
3024
|
const agentHints = routing.extractAgentHints(item);
|
|
3025
|
-
const agentId = item.agent || resolveAgent(workType, config,
|
|
3025
|
+
const agentId = item.agent || resolveAgent(workType, config, { agentHints });
|
|
3026
3026
|
if (!agentId) continue;
|
|
3027
3027
|
|
|
3028
3028
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
@@ -3664,7 +3664,7 @@ async function tickInner() {
|
|
|
3664
3664
|
// be of type string. Received undefined` and re-queues — every tick. Try to
|
|
3665
3665
|
// resolve a fallback via routing; if none is available, skip this tick.
|
|
3666
3666
|
if (!item.agent || typeof item.agent !== 'string') {
|
|
3667
|
-
const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config,
|
|
3667
|
+
const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config, { agentHints: routing.extractAgentHints(item.meta?.item) });
|
|
3668
3668
|
if (!fallback) {
|
|
3669
3669
|
log('warn', `Pending dispatch ${item.id} has no agent and routing returned no fallback — skipping`);
|
|
3670
3670
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1622",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|