@yemi33/minions 0.1.1619 → 0.1.1621
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 +10 -0
- package/dashboard/js/command-center.js +25 -1
- package/dashboard.js +115 -42
- package/engine/copilot-models.json +1 -1
- package/engine/llm.js +100 -6
- package/engine/routing.js +41 -8
- package/engine/runtimes/claude.js +1 -0
- package/engine.js +6 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1621 (2026-04-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- harden action block parsing (#1863)
|
|
7
|
+
|
|
8
|
+
## 0.1.1620 (2026-04-29)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- progressive Claude token streaming + simplify-pass cleanup
|
|
12
|
+
|
|
3
13
|
## 0.1.1619 (2026-04-29)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
@@ -14,6 +14,22 @@ var _ccSending = false; // true if active tab is sending (UI indicator only)
|
|
|
14
14
|
// Clear stale sending state on page load — SSE streams don't survive refresh
|
|
15
15
|
try { localStorage.removeItem('cc-sending'); } catch {}
|
|
16
16
|
|
|
17
|
+
function _ccStripActionBlockFromText(value) {
|
|
18
|
+
var text = value || '';
|
|
19
|
+
if (!text) return text;
|
|
20
|
+
var full = /(?:^|\r?\n)===ACTIONS={0,3}[ \t]*(?=\r?\n|$)/m.exec(text);
|
|
21
|
+
if (full) return text.slice(0, full.index + full[0].indexOf('===ACTIONS')).trim();
|
|
22
|
+
var block = /(?:^|\r?\n)===ACTIONS\b[^\r\n]*(?=\r?\n|$)/m.exec(text);
|
|
23
|
+
if (block) return text.slice(0, block.index + block[0].indexOf('===ACTIONS')).trim();
|
|
24
|
+
var delimiter = '===ACTIONS===';
|
|
25
|
+
var lineStart = Math.max(text.lastIndexOf('\n'), text.lastIndexOf('\r')) + 1;
|
|
26
|
+
var trailingLine = text.slice(lineStart).trimEnd();
|
|
27
|
+
if (trailingLine.length >= 3 && trailingLine.length < delimiter.length && delimiter.indexOf(trailingLine) === 0) {
|
|
28
|
+
return text.slice(0, lineStart).trimEnd();
|
|
29
|
+
}
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
// ── Migration from legacy single-session format ─────────────────────────────
|
|
18
34
|
(function _ccMigrateLegacy() {
|
|
19
35
|
try {
|
|
@@ -55,8 +71,12 @@ function _ccActiveTab() {
|
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
function _ccMergeStreamText(prev, incoming) {
|
|
74
|
+
// `prev` is already merged-clean from prior frames (server strips actions
|
|
75
|
+
// before SSE emission, and any leaked partial was sanitized by the previous
|
|
76
|
+
// _ccMergeStreamText call). Only strip `incoming` defensively — re-stripping
|
|
77
|
+
// `prev` every frame is O(n²) over the response length for nothing.
|
|
58
78
|
var current = prev || '';
|
|
59
|
-
var next = incoming || '';
|
|
79
|
+
var next = _ccStripActionBlockFromText(incoming || '');
|
|
60
80
|
if (!current) return next;
|
|
61
81
|
if (!next) return current;
|
|
62
82
|
if (next === current) return current;
|
|
@@ -601,6 +621,10 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
601
621
|
updateStreamDiv();
|
|
602
622
|
} else if (evt.type === 'heartbeat') {
|
|
603
623
|
return;
|
|
624
|
+
} else if (evt.type === 'thinking') {
|
|
625
|
+
streamStatusNote = evt.text || 'Thinking...';
|
|
626
|
+
if (activeTab) activeTab._streamStatusNote = streamStatusNote;
|
|
627
|
+
updateStreamDiv();
|
|
604
628
|
} else if (evt.type === 'tool') {
|
|
605
629
|
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
606
630
|
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
package/dashboard.js
CHANGED
|
@@ -584,6 +584,7 @@ function _ensureCcLiveStream(tabId) {
|
|
|
584
584
|
tabId,
|
|
585
585
|
text: '',
|
|
586
586
|
tools: [],
|
|
587
|
+
thinking: false,
|
|
587
588
|
donePayload: null,
|
|
588
589
|
writer: null,
|
|
589
590
|
endResponse: null,
|
|
@@ -766,10 +767,67 @@ For all state files, look under \`${MINIONS_DIR}\`.`;
|
|
|
766
767
|
}
|
|
767
768
|
|
|
768
769
|
function findCCActionsDelimiter(text) {
|
|
770
|
+
const header = findCCActionsHeader(text);
|
|
771
|
+
return header && header.parseable ? header.index : -1;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Single helper that handles both the strict (well-formed) and loose forms of
|
|
775
|
+
// the ===ACTIONS=== delimiter. `parseable` is true only for the strict form
|
|
776
|
+
// that parseCCActions can JSON.parse; loose matches still split display text
|
|
777
|
+
// but are not parsed (they shouldn't reach the user as actions).
|
|
778
|
+
function findCCActionsHeader(text) {
|
|
779
|
+
if (!text) return null;
|
|
780
|
+
// Strict: ===ACTIONS={0,3} on its own line, optional trailing whitespace.
|
|
781
|
+
const strict = /(?:^|\r?\n)===ACTIONS={0,3}[ \t]*(?=\r?\n|$)/m.exec(text);
|
|
782
|
+
if (strict) {
|
|
783
|
+
const headerStart = strict.index + strict[0].indexOf('===ACTIONS');
|
|
784
|
+
const headerMatch = text.slice(headerStart).match(/^===ACTIONS={0,3}[ \t]*/);
|
|
785
|
+
return {
|
|
786
|
+
index: headerStart,
|
|
787
|
+
headerLength: headerMatch ? headerMatch[0].length : '===ACTIONS==='.length,
|
|
788
|
+
parseable: true,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
// Loose: sentinel-looking malformed delimiters such as ===ACTIONS -> should
|
|
792
|
+
// still be hidden, but prose like "===ACTIONS are documented" must render.
|
|
793
|
+
const loose = /(?:^|\r?\n)===ACTIONS(?:[ \t]*(?:[-=]>?|={1,}|$)|[^A-Za-z0-9_\s\r\n][^\r\n]*)(?=\r?\n|$)/m.exec(text);
|
|
794
|
+
if (loose) {
|
|
795
|
+
const headerStart = loose.index + loose[0].indexOf('===ACTIONS');
|
|
796
|
+
return { index: headerStart, headerLength: 0, parseable: false };
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function findCCActionsPartialDelimiter(text) {
|
|
769
802
|
if (!text) return -1;
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
803
|
+
const delimiter = '===ACTIONS===';
|
|
804
|
+
const lineStart = Math.max(text.lastIndexOf('\n'), text.lastIndexOf('\r')) + 1;
|
|
805
|
+
const trailingLine = text.slice(lineStart).trimEnd();
|
|
806
|
+
if (trailingLine.length >= 3 && trailingLine.length < delimiter.length && delimiter.startsWith(trailingLine)) {
|
|
807
|
+
return lineStart;
|
|
808
|
+
}
|
|
809
|
+
return -1;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function stripCCActionsForStream(text) {
|
|
813
|
+
if (!text) return '';
|
|
814
|
+
// Fast path: 95% of streamed chunks contain no '=' before any actions block
|
|
815
|
+
// appears. Skip all regex work in that case.
|
|
816
|
+
if (text.indexOf('===') < 0) return text;
|
|
817
|
+
const header = findCCActionsHeader(text);
|
|
818
|
+
if (header) return text.slice(0, header.index).trim();
|
|
819
|
+
const partialIdx = findCCActionsPartialDelimiter(text);
|
|
820
|
+
if (partialIdx >= 0) return text.slice(0, partialIdx).trimEnd();
|
|
821
|
+
return text;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function stripCCActionsForDisplay(text) {
|
|
825
|
+
if (!text) return '';
|
|
826
|
+
const header = findCCActionsHeader(text);
|
|
827
|
+
if (header) return text.slice(0, header.index).trim();
|
|
828
|
+
const partialIdx = findCCActionsPartialDelimiter(text);
|
|
829
|
+
if (partialIdx >= 0) return text.slice(0, partialIdx).trimEnd();
|
|
830
|
+
return text;
|
|
773
831
|
}
|
|
774
832
|
|
|
775
833
|
// Issue #1834: non-Claude runtimes (Copilot/GPT) routinely wrap the action JSON
|
|
@@ -817,22 +875,25 @@ function _extractActionsJson(segment) {
|
|
|
817
875
|
|
|
818
876
|
function parseCCActions(text) {
|
|
819
877
|
let actions = [];
|
|
820
|
-
let displayText = text;
|
|
878
|
+
let displayText = stripCCActionsForDisplay(text);
|
|
821
879
|
let parseError = null;
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
880
|
+
const header = findCCActionsHeader(text);
|
|
881
|
+
let segment = '';
|
|
882
|
+
if (header) {
|
|
883
|
+
displayText = text.slice(0, header.index).trim();
|
|
884
|
+
if (header.parseable) {
|
|
885
|
+
segment = text.slice(header.index + header.headerLength);
|
|
886
|
+
const jsonStr = _extractActionsJson(segment);
|
|
887
|
+
if (jsonStr) {
|
|
888
|
+
try {
|
|
889
|
+
const parsed = JSON.parse(jsonStr);
|
|
890
|
+
actions = Array.isArray(parsed) ? parsed : [parsed];
|
|
891
|
+
} catch (e) {
|
|
892
|
+
parseError = e.message || 'invalid JSON';
|
|
893
|
+
}
|
|
894
|
+
} else if (segment.trim()) {
|
|
895
|
+
parseError = 'no JSON value found after ===ACTIONS=== delimiter';
|
|
833
896
|
}
|
|
834
|
-
} else if (segment.trim()) {
|
|
835
|
-
parseError = 'no JSON value found after ===ACTIONS=== delimiter';
|
|
836
897
|
}
|
|
837
898
|
}
|
|
838
899
|
if (actions.length === 0) {
|
|
@@ -851,7 +912,7 @@ function parseCCActions(text) {
|
|
|
851
912
|
result._actionParseError = parseError;
|
|
852
913
|
// Visibility for the engine log — silent failure here previously masked issue #1834.
|
|
853
914
|
try {
|
|
854
|
-
const snippet = (
|
|
915
|
+
const snippet = (segment.trim() || '').slice(0, 200);
|
|
855
916
|
console.warn(`[CC] action JSON parse failed (${parseError}); raw segment: ${snippet}`);
|
|
856
917
|
if (typeof shared !== 'undefined' && shared && typeof shared.log === 'function') {
|
|
857
918
|
shared.log('warn', `CC action JSON parse failed: ${parseError} — segment: ${snippet}`);
|
|
@@ -864,8 +925,8 @@ function parseCCActions(text) {
|
|
|
864
925
|
function stripCCActionSyntax(text) {
|
|
865
926
|
if (!text) return '';
|
|
866
927
|
let displayText = text;
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
928
|
+
const header = findCCActionsHeader(text);
|
|
929
|
+
if (header) displayText = text.slice(0, header.index).trim();
|
|
869
930
|
return displayText.replace(/`{3,}\s*action\s*\r?\n[\s\S]*?`{3,}\n?/g, '').trim();
|
|
870
931
|
}
|
|
871
932
|
|
|
@@ -4532,11 +4593,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4532
4593
|
});
|
|
4533
4594
|
}
|
|
4534
4595
|
|
|
4535
|
-
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
4536
|
-
if (!tabId.startsWith('teams-')) {
|
|
4537
|
-
teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
|
|
4538
|
-
}
|
|
4539
|
-
|
|
4540
4596
|
const parsed = parseCCActions(result.text);
|
|
4541
4597
|
const toolUses = Array.isArray(result.toolUses) ? result.toolUses : _extractToolUsesFromRaw(result.raw);
|
|
4542
4598
|
// Safety net: detect /loop invocation and convert to create-watch
|
|
@@ -4549,6 +4605,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4549
4605
|
if (parsed.actions.length > 0) {
|
|
4550
4606
|
parsed.actionResults = await executeCCActions(parsed.actions);
|
|
4551
4607
|
}
|
|
4608
|
+
// Mirror only user-facing text to Teams; never send the internal action block.
|
|
4609
|
+
if (!tabId.startsWith('teams-')) {
|
|
4610
|
+
teams.teamsPostCCResponse(body.message, parsed.text).catch(() => {});
|
|
4611
|
+
}
|
|
4552
4612
|
// Issue #1834: rename _actionParseError → actionParseError (public field)
|
|
4553
4613
|
// so the client can surface a warning when the model emitted ===ACTIONS===
|
|
4554
4614
|
// but the JSON couldn't be recovered.
|
|
@@ -4642,6 +4702,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4642
4702
|
for (const tool of live.tools || []) {
|
|
4643
4703
|
writeCcEvent({ type: 'tool', name: tool.name, input: _lightToolInput(tool.input) });
|
|
4644
4704
|
}
|
|
4705
|
+
if (live.thinking && !live.text) writeCcEvent({ type: 'thinking', text: 'Thinking...' });
|
|
4645
4706
|
if (live.text) writeCcEvent({ type: 'chunk', text: live.text });
|
|
4646
4707
|
if (live.donePayload) {
|
|
4647
4708
|
writeCcEvent(live.donePayload);
|
|
@@ -4723,15 +4784,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4723
4784
|
sessionId, effort: streamEffort, direct: true,
|
|
4724
4785
|
engineConfig: CONFIG.engine,
|
|
4725
4786
|
onChunk: (text) => {
|
|
4726
|
-
const
|
|
4727
|
-
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
4787
|
+
const display = stripCCActionsForStream(text);
|
|
4728
4788
|
liveState.text = display;
|
|
4729
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;
|
|
4730
4793
|
},
|
|
4731
4794
|
onToolUse: (name, input) => {
|
|
4732
4795
|
toolUses.push({ name, input: input || {} });
|
|
4733
4796
|
liveState.tools.push({ name, input: input || {} });
|
|
4734
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...' });
|
|
4735
4802
|
}
|
|
4736
4803
|
});
|
|
4737
4804
|
_ccStreamAbort = llmPromise.abort;
|
|
@@ -4749,20 +4816,26 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4749
4816
|
toolUses = []; // discard stale metadata from the failed resume attempt
|
|
4750
4817
|
const retryPromise = callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
4751
4818
|
timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
}
|
|
4819
|
+
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
4820
|
+
effort: streamEffort, direct: true,
|
|
4821
|
+
engineConfig: CONFIG.engine,
|
|
4822
|
+
onChunk: (text) => {
|
|
4823
|
+
const display = stripCCActionsForStream(text);
|
|
4824
|
+
liveState.text = display;
|
|
4825
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4826
|
+
// Same reset as the initial path so resume-fail retries don't
|
|
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
|
+
}
|
|
4766
4839
|
});
|
|
4767
4840
|
_ccStreamAbort = retryPromise.abort;
|
|
4768
4841
|
liveState.abortFn = _ccStreamAbort;
|
|
@@ -4834,7 +4907,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4834
4907
|
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
4835
4908
|
const _streamTabId = body.tabId || 'default';
|
|
4836
4909
|
if (!_streamTabId.startsWith('teams-')) {
|
|
4837
|
-
teams.teamsPostCCResponse(body.message,
|
|
4910
|
+
teams.teamsPostCCResponse(body.message, displayText).catch(() => {});
|
|
4838
4911
|
}
|
|
4839
4912
|
|
|
4840
4913
|
if (liveState.endResponse) liveState.endResponse();
|
package/engine/llm.js
CHANGED
|
@@ -24,6 +24,10 @@ 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
|
+
|
|
27
31
|
// ─── Engine-Usage Metrics ────────────────────────────────────────────────────
|
|
28
32
|
|
|
29
33
|
function trackEngineUsage(category, usage) {
|
|
@@ -261,6 +265,7 @@ function _createStreamAccumulator({
|
|
|
261
265
|
onChunk = null,
|
|
262
266
|
onToolUse = null,
|
|
263
267
|
onTaskComplete = null,
|
|
268
|
+
onThinking = null,
|
|
264
269
|
}) {
|
|
265
270
|
let stdout = '';
|
|
266
271
|
let stderr = '';
|
|
@@ -278,9 +283,15 @@ function _createStreamAccumulator({
|
|
|
278
283
|
let copilotMessageBuffer = '';
|
|
279
284
|
let copilotTaskCompleteSeen = false;
|
|
280
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
|
+
let thinkingSent = false;
|
|
281
292
|
|
|
282
293
|
function _streamText(value) {
|
|
283
|
-
return maxTextLength ? value.slice(-maxTextLength) : value;
|
|
294
|
+
return (maxTextLength && value.length > maxTextLength) ? value.slice(-maxTextLength) : value;
|
|
284
295
|
}
|
|
285
296
|
|
|
286
297
|
function _copilotAssistantMessageHasTools(obj) {
|
|
@@ -288,6 +299,79 @@ function _createStreamAccumulator({
|
|
|
288
299
|
return Array.isArray(requests) && requests.length > 0;
|
|
289
300
|
}
|
|
290
301
|
|
|
302
|
+
function _notifyThinking() {
|
|
303
|
+
if (!onThinking || thinkingSent) return;
|
|
304
|
+
thinkingSent = true;
|
|
305
|
+
onThinking();
|
|
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
|
+
|
|
291
375
|
function _captureCopilotTaskComplete(summary, success = true) {
|
|
292
376
|
if (typeof summary !== 'string' || !summary) return;
|
|
293
377
|
const finalSummary = _streamText(summary);
|
|
@@ -311,6 +395,9 @@ function _createStreamAccumulator({
|
|
|
311
395
|
|
|
312
396
|
// ── Claude shape ────────────────────────────────────────────────────────
|
|
313
397
|
if (obj.session_id) sessionId = obj.session_id;
|
|
398
|
+
if (obj.type === 'stream_event') {
|
|
399
|
+
_captureClaudeStreamEvent(obj);
|
|
400
|
+
}
|
|
314
401
|
if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
315
402
|
// Claude result event: terminal text + usage.
|
|
316
403
|
text = maxTextLength ? obj.result.slice(-maxTextLength) : obj.result;
|
|
@@ -328,19 +415,22 @@ function _createStreamAccumulator({
|
|
|
328
415
|
}
|
|
329
416
|
if (obj.type === 'assistant' && Array.isArray(obj.message?.content)) {
|
|
330
417
|
// Claude assistant turn: content blocks (text + tool_use).
|
|
418
|
+
// Multi-text-block messages (common with --include-partial-messages) need
|
|
419
|
+
// their text joined before _captureClaudeText, otherwise each block
|
|
420
|
+
// overwrites the prior one.
|
|
421
|
+
let assistantText = '';
|
|
331
422
|
for (const block of obj.message.content) {
|
|
332
423
|
if (block?.type === 'text' && block.text) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
onChunk(block.text);
|
|
337
|
-
}
|
|
424
|
+
assistantText += block.text;
|
|
425
|
+
} else if (THINKING_BLOCK_TYPES.has(block?.type)) {
|
|
426
|
+
_notifyThinking();
|
|
338
427
|
} else if (block?.type === 'tool_use' && block.name) {
|
|
339
428
|
const toolUse = { name: block.name, input: block.input || {} };
|
|
340
429
|
toolUses.push(toolUse);
|
|
341
430
|
if (onToolUse) onToolUse(toolUse.name, toolUse.input);
|
|
342
431
|
}
|
|
343
432
|
}
|
|
433
|
+
if (assistantText) _captureClaudeText(assistantText);
|
|
344
434
|
}
|
|
345
435
|
|
|
346
436
|
// ── Copilot shape ───────────────────────────────────────────────────────
|
|
@@ -348,6 +438,9 @@ function _createStreamAccumulator({
|
|
|
348
438
|
if (obj.type === 'session.task_complete') {
|
|
349
439
|
_captureCopilotTaskComplete(obj.data?.summary, obj.data?.success);
|
|
350
440
|
}
|
|
441
|
+
if (obj.type === 'assistant.reasoning' || obj.type === 'assistant.reasoning_delta') {
|
|
442
|
+
_notifyThinking();
|
|
443
|
+
}
|
|
351
444
|
if (obj.type === 'assistant.message_delta' && typeof obj.data?.deltaContent === 'string') {
|
|
352
445
|
if (copilotTaskCompleteSeen) return;
|
|
353
446
|
copilotMessageBuffer += obj.data.deltaContent;
|
|
@@ -611,6 +704,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
611
704
|
onChunk,
|
|
612
705
|
onToolUse,
|
|
613
706
|
onTaskComplete: scheduleTaskCompleteClose,
|
|
707
|
+
onThinking: opts.onThinking || null,
|
|
614
708
|
});
|
|
615
709
|
|
|
616
710
|
_abort = () => { shared.killImmediate(proc); };
|
package/engine/routing.js
CHANGED
|
@@ -28,7 +28,7 @@ let _routingCache = null;
|
|
|
28
28
|
let _routingCacheMtime = 0;
|
|
29
29
|
|
|
30
30
|
function parseRoutingTable() {
|
|
31
|
-
const content = getRouting();
|
|
31
|
+
const content = getRouting() || '';
|
|
32
32
|
const routes = {};
|
|
33
33
|
const lines = content.split('\n');
|
|
34
34
|
let inTable = false;
|
|
@@ -116,12 +116,44 @@ function setTempBudget(n) {
|
|
|
116
116
|
}
|
|
117
117
|
function getTempBudget() { return _tempBudget; }
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
// Centralizes the work-item shape used to derive routing hints. Engine code
|
|
120
|
+
// previously inlined `item.preferred_agent || item.agents || null` at four
|
|
121
|
+
// call sites; hoisting keeps the contract in one place.
|
|
122
|
+
function extractAgentHints(item) {
|
|
123
|
+
if (!item || typeof item !== 'object') return null;
|
|
124
|
+
return item.preferred_agent || item.agents || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Normalize a list of agent-hint inputs. Accepts:
|
|
128
|
+
// - Comma-separated string ("dallas,ripley")
|
|
129
|
+
// - Array of strings
|
|
130
|
+
// - Single string
|
|
131
|
+
// Resolves the `_author_` token to authorAgent (when provided), validates
|
|
132
|
+
// each hint against the configured agents map (case-insensitive lookup,
|
|
133
|
+
// returning the canonical ID), dedups, and drops anything unknown.
|
|
134
|
+
function normalizeAgentHints(agentHints, authorAgent = null, agents = null) {
|
|
135
|
+
const raw = Array.isArray(agentHints)
|
|
136
|
+
? agentHints
|
|
137
|
+
: (agentHints ? String(agentHints).split(',') : []);
|
|
138
|
+
const expanded = raw
|
|
139
|
+
.map(id => String(id).trim())
|
|
140
|
+
.map(id => id.toLowerCase() === '_author_' && authorAgent ? String(authorAgent).trim() : id)
|
|
124
141
|
.filter(Boolean);
|
|
142
|
+
// When no agents map is supplied, return lowercased IDs (legacy behaviour
|
|
143
|
+
// used by tests and pre-validation callers).
|
|
144
|
+
if (!agents || typeof agents !== 'object') {
|
|
145
|
+
return expanded.map(id => id.toLowerCase());
|
|
146
|
+
}
|
|
147
|
+
const byLower = new Map(Object.keys(agents).map(id => [id.toLowerCase(), id]));
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
const normalized = [];
|
|
150
|
+
for (const hint of expanded) {
|
|
151
|
+
const id = byLower.get(hint.toLowerCase());
|
|
152
|
+
if (!id || seen.has(id)) continue;
|
|
153
|
+
seen.add(id);
|
|
154
|
+
normalized.push(id);
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
function resolveAgent(workType, config, authorAgent = null, agentHints = null) {
|
|
@@ -153,7 +185,7 @@ function resolveAgent(workType, config, authorAgent = null, agentHints = null) {
|
|
|
153
185
|
return null;
|
|
154
186
|
};
|
|
155
187
|
|
|
156
|
-
const hintedAgents = normalizeAgentHints(agentHints, authorAgent);
|
|
188
|
+
const hintedAgents = normalizeAgentHints(agentHints, authorAgent, agents);
|
|
157
189
|
if (hintedAgents.length > 0) {
|
|
158
190
|
for (const id of hintedAgents) {
|
|
159
191
|
if (isAvailable(id)) { _claimedAgents.add(id); return id; }
|
|
@@ -203,10 +235,11 @@ module.exports = {
|
|
|
203
235
|
getMonthlySpend,
|
|
204
236
|
getAgentErrorRate,
|
|
205
237
|
isAgentIdle,
|
|
238
|
+
normalizeAgentHints,
|
|
239
|
+
extractAgentHints,
|
|
206
240
|
_claimedAgents,
|
|
207
241
|
resetClaimedAgents,
|
|
208
242
|
resolveAgent,
|
|
209
243
|
setTempBudget,
|
|
210
244
|
getTempBudget,
|
|
211
|
-
normalizeAgentHints,
|
|
212
245
|
};
|
|
@@ -209,6 +209,7 @@ function buildArgs(opts = {}) {
|
|
|
209
209
|
} = opts;
|
|
210
210
|
|
|
211
211
|
const args = ['-p', '--output-format', outputFormat];
|
|
212
|
+
if (outputFormat === 'stream-json') args.push('--include-partial-messages');
|
|
212
213
|
if (maxTurns != null) args.push('--max-turns', String(maxTurns));
|
|
213
214
|
if (model) args.push('--model', String(model));
|
|
214
215
|
if (verbose) args.push('--verbose');
|
package/engine.js
CHANGED
|
@@ -121,9 +121,10 @@ const { getConfig, getControl, getDispatch, getNotes,
|
|
|
121
121
|
|
|
122
122
|
// ─── Routing (extracted to engine/routing.js) ───────────────────────────────
|
|
123
123
|
|
|
124
|
+
const routing = require('./engine/routing');
|
|
124
125
|
const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
|
|
125
126
|
getAgentErrorRate, isAgentIdle, resolveAgent, resetClaimedAgents,
|
|
126
|
-
setTempBudget, tempAgents } =
|
|
127
|
+
setTempBudget, tempAgents } = routing;
|
|
127
128
|
|
|
128
129
|
// ─── Playbook, system prompt, agent context (extracted to engine/playbook.js) ─
|
|
129
130
|
|
|
@@ -2500,7 +2501,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2500
2501
|
item._decomposing = true;
|
|
2501
2502
|
needsWrite = true;
|
|
2502
2503
|
}
|
|
2503
|
-
const agentHints =
|
|
2504
|
+
const agentHints = routing.extractAgentHints(item);
|
|
2504
2505
|
const agentId = item.agent || resolveAgent(workType, config, null, agentHints);
|
|
2505
2506
|
if (!agentId) {
|
|
2506
2507
|
// Check if reason is budget
|
|
@@ -3020,7 +3021,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3020
3021
|
|
|
3021
3022
|
} else {
|
|
3022
3023
|
// ─── Normal: single agent dispatch ──────────────────────────────
|
|
3023
|
-
const agentHints =
|
|
3024
|
+
const agentHints = routing.extractAgentHints(item);
|
|
3024
3025
|
const agentId = item.agent || resolveAgent(workType, config, null, agentHints);
|
|
3025
3026
|
if (!agentId) continue;
|
|
3026
3027
|
|
|
@@ -3663,8 +3664,7 @@ async function tickInner() {
|
|
|
3663
3664
|
// be of type string. Received undefined` and re-queues — every tick. Try to
|
|
3664
3665
|
// resolve a fallback via routing; if none is available, skip this tick.
|
|
3665
3666
|
if (!item.agent || typeof item.agent !== 'string') {
|
|
3666
|
-
const
|
|
3667
|
-
const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config, null, agentHints);
|
|
3667
|
+
const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config, null, 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;
|
|
@@ -3722,7 +3722,7 @@ async function tickInner() {
|
|
|
3722
3722
|
// Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
|
|
3723
3723
|
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
3724
3724
|
const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
|
|
3725
|
-
const isExplicitReassign = !!(item.meta?.item?.agent ||
|
|
3725
|
+
const isExplicitReassign = !!(item.meta?.item?.agent || routing.extractAgentHints(item.meta?.item));
|
|
3726
3726
|
if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
|
|
3727
3727
|
const busySinceMs = new Date(item._agentBusySince).getTime();
|
|
3728
3728
|
if (Date.now() - busySinceMs > reassignMs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1621",
|
|
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"
|