@link-assistant/hive-mind 2.1.2 ā 2.1.3
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 +6 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +132 -32
- package/src/bidirectional-interactive.lib.mjs +169 -71
- package/src/live-input-capabilities.lib.mjs +221 -0
- package/src/solve.auto-merge-helpers.lib.mjs +66 -0
- package/src/solve.auto-merge.lib.mjs +34 -5
- package/src/solve.config.lib.mjs +10 -10
- package/src/solve.validation.lib.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5c4150b: Make live issue/PR event input available for every tool via `--auto-input-until-mergeable` (issue #2007). Claude and Agent stream events into the live process through `--input-format stream-json`; codex, opencode, gemini, qwen, and unknown tools use a universal restart/resume fallback that waits for the current turn to finish in the JSON output, stops the process, and resumes the AI session with the new events. Adds issue title/description edit detection as a restart trigger, reworks the capability matrix to report each tool's delivery mode, and records the `@link-assistant/agent` 0.24.1 live stream-json contract.
|
|
8
|
+
|
|
3
9
|
## 2.1.2
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -20,10 +20,12 @@ import { timeouts, retryLimits } from './config.lib.mjs';
|
|
|
20
20
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
21
21
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
22
22
|
import Decimal from 'decimal.js-light';
|
|
23
|
+
import semver from 'semver';
|
|
23
24
|
import { agentModels, defaultModels, freeToBaseModelMap } from './models/index.mjs';
|
|
24
25
|
import { checkPlaywrightMcpPackageAvailability, getAgentPlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
|
|
25
26
|
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage } from './agent-token-usage.lib.mjs';
|
|
26
27
|
import { classifyRetryableError, getRetryDelayMs, maybeSwitchToFallbackModel, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
28
|
+
import { attachStreamingInput, finalizeBidirectionalHandler, setupBidirectionalHandler } from './bidirectional-interactive.lib.mjs';
|
|
27
29
|
|
|
28
30
|
export { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage };
|
|
29
31
|
|
|
@@ -248,10 +250,22 @@ export const mapModelToId = model => {
|
|
|
248
250
|
return agentModels[model] || model;
|
|
249
251
|
};
|
|
250
252
|
|
|
253
|
+
export const MIN_AGENT_LIVE_INPUT_VERSION = '0.24.1';
|
|
254
|
+
|
|
255
|
+
export const getAgentCliVersion = versionOutput => {
|
|
256
|
+
return semver.clean(versionOutput) || semver.coerce(versionOutput)?.version || null;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export const agentCliSupportsLiveInput = versionOutput => {
|
|
260
|
+
const version = getAgentCliVersion(versionOutput);
|
|
261
|
+
return !!version && semver.gte(version, MIN_AGENT_LIVE_INPUT_VERSION);
|
|
262
|
+
};
|
|
263
|
+
|
|
251
264
|
// Function to validate Agent connection
|
|
252
|
-
export const validateAgentConnection = async (model = defaultModels.agent) => {
|
|
265
|
+
export const validateAgentConnection = async (model = defaultModels.agent, options = {}) => {
|
|
253
266
|
// Map model alias to full ID
|
|
254
267
|
const mappedModel = mapModelToId(model);
|
|
268
|
+
const requireLiveInput = !!options.requireLiveInput;
|
|
255
269
|
|
|
256
270
|
// Retry configuration
|
|
257
271
|
const maxRetries = 3;
|
|
@@ -266,10 +280,12 @@ export const validateAgentConnection = async (model = defaultModels.agent) => {
|
|
|
266
280
|
}
|
|
267
281
|
|
|
268
282
|
// Check if Agent CLI is installed and get version
|
|
283
|
+
let agentVersion = null;
|
|
269
284
|
try {
|
|
270
285
|
const versionResult = await $`timeout ${Math.floor(timeouts.opencodeCli / 1000)} agent --version`;
|
|
271
286
|
if (versionResult.code === 0) {
|
|
272
287
|
const version = versionResult.stdout?.toString().trim();
|
|
288
|
+
agentVersion = getAgentCliVersion(version);
|
|
273
289
|
if (retryCount === 0) {
|
|
274
290
|
await log(`š¦ Agent CLI version: ${version}`);
|
|
275
291
|
}
|
|
@@ -280,6 +296,17 @@ export const validateAgentConnection = async (model = defaultModels.agent) => {
|
|
|
280
296
|
}
|
|
281
297
|
}
|
|
282
298
|
|
|
299
|
+
if (requireLiveInput && (!agentVersion || !semver.gte(agentVersion, MIN_AGENT_LIVE_INPUT_VERSION))) {
|
|
300
|
+
await log(`ā Agent live stream-json input requires @link-assistant/agent >= ${MIN_AGENT_LIVE_INPUT_VERSION}`, { level: 'error' });
|
|
301
|
+
if (agentVersion) {
|
|
302
|
+
await log(` Installed Agent CLI version: ${agentVersion}`, { level: 'error' });
|
|
303
|
+
} else {
|
|
304
|
+
await log(' Could not determine the installed Agent CLI version.', { level: 'error' });
|
|
305
|
+
}
|
|
306
|
+
await log(' Update with: bun install -g @link-assistant/agent@latest', { level: 'error' });
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
283
310
|
// Test basic Agent functionality with a simple "hi" message
|
|
284
311
|
// Agent uses the same JSON interface as OpenCode
|
|
285
312
|
const testResult = await $`printf "hi" | timeout ${Math.floor(timeouts.opencodeCli / 1000)} agent --model ${mappedModel}`;
|
|
@@ -406,13 +433,17 @@ export const executeAgent = async params => {
|
|
|
406
433
|
getResourceSnapshot,
|
|
407
434
|
forkedRepo,
|
|
408
435
|
feedbackLines,
|
|
436
|
+
owner,
|
|
437
|
+
repo,
|
|
438
|
+
prNumber,
|
|
439
|
+
issueNumber,
|
|
409
440
|
agentPath,
|
|
410
441
|
$,
|
|
411
442
|
});
|
|
412
443
|
};
|
|
413
444
|
|
|
414
445
|
export const executeAgentCommand = async params => {
|
|
415
|
-
const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, agentPath, $, waitForRetryDelay = waitWithCountdown } = params;
|
|
446
|
+
const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, owner, repo, prNumber, issueNumber, agentPath, $, calculatePricing = calculateAgentPricing, waitForRetryDelay = waitWithCountdown } = params;
|
|
416
447
|
|
|
417
448
|
// Retry configuration
|
|
418
449
|
let retryCount = 0;
|
|
@@ -459,6 +490,15 @@ export const executeAgentCommand = async params => {
|
|
|
459
490
|
|
|
460
491
|
// Build Agent command
|
|
461
492
|
let execCommand;
|
|
493
|
+
let bidirectionalHandler = null;
|
|
494
|
+
let bidirectionalHandlerFinalized = false;
|
|
495
|
+
let queuedFeedback = [];
|
|
496
|
+
const finalizeAgentBidirectionalHandler = async () => {
|
|
497
|
+
if (bidirectionalHandlerFinalized) return queuedFeedback;
|
|
498
|
+
bidirectionalHandlerFinalized = true;
|
|
499
|
+
queuedFeedback = await finalizeBidirectionalHandler(bidirectionalHandler, log);
|
|
500
|
+
return queuedFeedback;
|
|
501
|
+
};
|
|
462
502
|
|
|
463
503
|
// Map model alias to full ID
|
|
464
504
|
const mappedModel = mapModelToId(argv.model);
|
|
@@ -480,27 +520,57 @@ export const executeAgentCommand = async params => {
|
|
|
480
520
|
// We'll combine system and user prompts into a single message
|
|
481
521
|
const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
|
|
482
522
|
|
|
483
|
-
// Write the combined prompt to a file for piping
|
|
484
|
-
// Use OS temporary directory instead of repository workspace to avoid polluting the repo
|
|
485
|
-
const promptFile = path.join(os.tmpdir(), `agent_prompt_${Date.now()}_${process.pid}.txt`);
|
|
486
|
-
await fs.writeFile(promptFile, combinedPrompt);
|
|
487
|
-
|
|
488
|
-
// Build the full command - pipe the prompt file to agent
|
|
489
|
-
const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${agentPath} ${agentArgs})`;
|
|
490
|
-
|
|
491
|
-
await log(`\n${formatAligned('š', 'Raw command:', '')}`);
|
|
492
|
-
await log(`${fullCommand}`);
|
|
493
|
-
await log('');
|
|
494
|
-
|
|
495
523
|
try {
|
|
496
|
-
|
|
497
|
-
|
|
524
|
+
if (argv.acceptIncommingCommentsAsInput) {
|
|
525
|
+
bidirectionalHandler = await setupBidirectionalHandler({
|
|
526
|
+
argv,
|
|
527
|
+
owner,
|
|
528
|
+
repo,
|
|
529
|
+
prNumber,
|
|
530
|
+
issueNumber,
|
|
531
|
+
tempDir,
|
|
532
|
+
$,
|
|
533
|
+
log,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const streamingInput = !!bidirectionalHandler;
|
|
537
|
+
if (streamingInput) {
|
|
538
|
+
agentArgs += ' --input-format stream-json --output-format stream-json';
|
|
539
|
+
}
|
|
498
540
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
541
|
+
let promptFile = null;
|
|
542
|
+
if (!streamingInput) {
|
|
543
|
+
// Write the combined prompt to a file for piping.
|
|
544
|
+
// Use OS temporary directory instead of repository workspace to avoid polluting the repo.
|
|
545
|
+
promptFile = path.join(os.tmpdir(), `agent_prompt_${Date.now()}_${process.pid}.txt`);
|
|
546
|
+
await fs.writeFile(promptFile, combinedPrompt);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const fullCommand = streamingInput ? `(cd "${tempDir}" && ${agentPath} ${agentArgs})` : `(cd "${tempDir}" && cat "${promptFile}" | ${agentPath} ${agentArgs})`;
|
|
550
|
+
|
|
551
|
+
await log(`\n${formatAligned('š', 'Raw command:', '')}`);
|
|
552
|
+
await log(`${fullCommand}`);
|
|
553
|
+
await log('');
|
|
554
|
+
|
|
555
|
+
if (streamingInput) {
|
|
556
|
+
execCommand = $({
|
|
557
|
+
cwd: tempDir,
|
|
558
|
+
stdin: 'pipe',
|
|
559
|
+
mirror: false,
|
|
560
|
+
env: agentEnv,
|
|
561
|
+
})`${agentPath} ${agentArgs}`;
|
|
562
|
+
const attached = await attachStreamingInput(bidirectionalHandler, execCommand, combinedPrompt, log, !!argv.verbose, { toolLabel: 'Agent' });
|
|
563
|
+
if (!attached) {
|
|
564
|
+
throw new Error('Agent live stream-json input requested, but stdin attachment failed');
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
// Pipe the prompt file to agent via stdin for the legacy one-shot path.
|
|
568
|
+
execCommand = $({
|
|
569
|
+
cwd: tempDir,
|
|
570
|
+
mirror: false,
|
|
571
|
+
env: agentEnv,
|
|
572
|
+
})`cat ${promptFile} | ${agentPath} ${agentArgs}`;
|
|
573
|
+
}
|
|
504
574
|
|
|
505
575
|
await log(`${formatAligned('š', 'Command details:', '')}`);
|
|
506
576
|
await log(formatAligned('š', 'Working directory:', tempDir, 2));
|
|
@@ -530,6 +600,30 @@ export const executeAgentCommand = async params => {
|
|
|
530
600
|
// This fixes the issue where NDJSON lines get concatenated without newlines, breaking JSON.parse
|
|
531
601
|
const streamingTokenUsage = createAgentTokenUsage();
|
|
532
602
|
const accumulateTokenUsage = data => accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
603
|
+
const isAgentSuccessfulCompletionEvent = data => {
|
|
604
|
+
if (data.type === 'session.idle' || data.type === 'session_idle' || data.type === 'idle') return true;
|
|
605
|
+
if (data.type === 'log' && data.message === 'exiting loop') return true;
|
|
606
|
+
if (data.type === 'step_finish' && data.part?.reason === 'stop') return true;
|
|
607
|
+
if (data.type === 'result' && (data.status === 'success' || data.subtype === 'success')) return true;
|
|
608
|
+
return false;
|
|
609
|
+
};
|
|
610
|
+
const markBidirectionalStateFromAgentEvent = async data => {
|
|
611
|
+
if (!bidirectionalHandler) return;
|
|
612
|
+
if (isAgentSuccessfulCompletionEvent(data)) {
|
|
613
|
+
if (typeof bidirectionalHandler.markAiIdle === 'function') {
|
|
614
|
+
try {
|
|
615
|
+
await bidirectionalHandler.markAiIdle();
|
|
616
|
+
} catch (idleErr) {
|
|
617
|
+
if (argv.verbose) await log(`ā ļø Bidirectional mode: markAiIdle error: ${idleErr.message}`, { verbose: true });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const busyEventTypes = new Set(['init', 'session_start', 'session.started', 'message', 'assistant', 'text', 'tool_use', 'tool_result', 'step_start', 'step_delta']);
|
|
623
|
+
if (busyEventTypes.has(data.type) && typeof bidirectionalHandler.markAiBusy === 'function') {
|
|
624
|
+
bidirectionalHandler.markAiBusy();
|
|
625
|
+
}
|
|
626
|
+
};
|
|
533
627
|
|
|
534
628
|
for await (const chunk of execCommand.stream()) {
|
|
535
629
|
if (chunk.type === 'stdout') {
|
|
@@ -548,12 +642,14 @@ export const executeAgentCommand = async params => {
|
|
|
548
642
|
// Output formatted JSON
|
|
549
643
|
await log(JSON.stringify(data, null, 2));
|
|
550
644
|
// Capture session ID from the first message
|
|
551
|
-
|
|
552
|
-
|
|
645
|
+
const eventSessionId = data.sessionID || data.session_id || data.sessionId;
|
|
646
|
+
if (!sessionId && eventSessionId) {
|
|
647
|
+
sessionId = eventSessionId;
|
|
553
648
|
await log(`š Session ID: ${sessionId}`);
|
|
554
649
|
}
|
|
555
650
|
// Issue #1250: Accumulate token usage during streaming
|
|
556
651
|
accumulateTokenUsage(data);
|
|
652
|
+
await markBidirectionalStateFromAgentEvent(data);
|
|
557
653
|
// Issue #1201: Detect error events during streaming for reliable detection
|
|
558
654
|
if (data.type === 'error' || data.type === 'step_error') {
|
|
559
655
|
streamingErrorDetected = true;
|
|
@@ -590,16 +686,14 @@ export const executeAgentCommand = async params => {
|
|
|
590
686
|
// Issue #1276: Detect successful completion events
|
|
591
687
|
// When agent emits session.idle or log with "exiting loop" message, it completed successfully
|
|
592
688
|
// This means any previous error events were recovered from (e.g., timeout then retry)
|
|
593
|
-
if (
|
|
689
|
+
if (isAgentSuccessfulCompletionEvent(data)) {
|
|
594
690
|
agentCompletedSuccessfully = true;
|
|
595
691
|
}
|
|
596
692
|
// Issue #1296: Detect step_finish with reason "stop" as successful completion
|
|
597
693
|
// This is a clear marker of success - agent finished normally, not due to error or limit
|
|
598
694
|
// When this event appears, we should ignore any error events that appeared earlier in the stream
|
|
599
695
|
// (e.g., timeout errors that were recovered from via retry logic)
|
|
600
|
-
if (data.type === 'step_finish' && data.part?.reason === 'stop')
|
|
601
|
-
agentCompletedSuccessfully = true;
|
|
602
|
-
}
|
|
696
|
+
if (data.type === 'step_finish' && data.part?.reason === 'stop') agentCompletedSuccessfully = true;
|
|
603
697
|
} catch {
|
|
604
698
|
// Not JSON - log as plain text
|
|
605
699
|
await log(line);
|
|
@@ -624,12 +718,14 @@ export const executeAgentCommand = async params => {
|
|
|
624
718
|
// Output formatted JSON (same formatting as stdout)
|
|
625
719
|
await log(JSON.stringify(stderrData, null, 2));
|
|
626
720
|
// Capture session ID from stderr too (agent sends it via stderr)
|
|
627
|
-
|
|
628
|
-
|
|
721
|
+
const eventSessionId = stderrData.sessionID || stderrData.session_id || stderrData.sessionId;
|
|
722
|
+
if (!sessionId && eventSessionId) {
|
|
723
|
+
sessionId = eventSessionId;
|
|
629
724
|
await log(`š Session ID: ${sessionId}`);
|
|
630
725
|
}
|
|
631
726
|
// Issue #1250: Accumulate token usage during streaming (stderr)
|
|
632
727
|
accumulateTokenUsage(stderrData);
|
|
728
|
+
await markBidirectionalStateFromAgentEvent(stderrData);
|
|
633
729
|
// Issue #1201: Detect error events during streaming (stderr) for reliable detection
|
|
634
730
|
if (stderrData.type === 'error' || stderrData.type === 'step_error') {
|
|
635
731
|
streamingErrorDetected = true;
|
|
@@ -661,7 +757,7 @@ export const executeAgentCommand = async params => {
|
|
|
661
757
|
}
|
|
662
758
|
// Issue #1276: Detect successful completion events (stderr)
|
|
663
759
|
// When agent emits session.idle or log with "exiting loop" message, it completed successfully
|
|
664
|
-
if (
|
|
760
|
+
if (isAgentSuccessfulCompletionEvent(stderrData)) {
|
|
665
761
|
agentCompletedSuccessfully = true;
|
|
666
762
|
}
|
|
667
763
|
// Issue #1296: Detect step_finish with reason "stop" as successful completion (stderr)
|
|
@@ -811,6 +907,7 @@ export const executeAgentCommand = async params => {
|
|
|
811
907
|
await log(`\nā ļø ${retryableError.label} detected. Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}${sessionId ? ' (session preserved)' : ''}...`, { level: 'warning' });
|
|
812
908
|
if (sessionId && !argv.resume) argv.resume = sessionId;
|
|
813
909
|
await maybeSwitchToFallbackModel({ tool: 'agent', argv, log, errorMessage: retryableError.message });
|
|
910
|
+
await finalizeAgentBidirectionalHandler();
|
|
814
911
|
await waitForRetryDelay(delay, log);
|
|
815
912
|
await log('\nš Retrying now...');
|
|
816
913
|
retryCount++;
|
|
@@ -887,7 +984,8 @@ export const executeAgentCommand = async params => {
|
|
|
887
984
|
// Issue #1250: Use streaming-accumulated token usage instead of re-parsing fullOutput
|
|
888
985
|
// This fixes the issue where NDJSON lines get concatenated without newlines, breaking JSON.parse
|
|
889
986
|
const tokenUsage = streamingTokenUsage;
|
|
890
|
-
const pricingInfo = await
|
|
987
|
+
const pricingInfo = await calculatePricing(mappedModel, tokenUsage);
|
|
988
|
+
await finalizeAgentBidirectionalHandler();
|
|
891
989
|
|
|
892
990
|
return {
|
|
893
991
|
success: false,
|
|
@@ -907,7 +1005,7 @@ export const executeAgentCommand = async params => {
|
|
|
907
1005
|
// Issue #1250: Use streaming-accumulated token usage instead of re-parsing fullOutput
|
|
908
1006
|
// This fixes the issue where NDJSON lines get concatenated without newlines, breaking JSON.parse
|
|
909
1007
|
const tokenUsage = streamingTokenUsage;
|
|
910
|
-
const pricingInfo = await
|
|
1008
|
+
const pricingInfo = await calculatePricing(mappedModel, tokenUsage);
|
|
911
1009
|
|
|
912
1010
|
// Log pricing information (similar to --tool claude breakdown)
|
|
913
1011
|
if (tokenUsage.stepCount > 0) {
|
|
@@ -958,6 +1056,7 @@ export const executeAgentCommand = async params => {
|
|
|
958
1056
|
if (lastTextContent) {
|
|
959
1057
|
await log('š Captured result summary from Agent output', { verbose: true });
|
|
960
1058
|
}
|
|
1059
|
+
await finalizeAgentBidirectionalHandler();
|
|
961
1060
|
|
|
962
1061
|
return {
|
|
963
1062
|
success: true,
|
|
@@ -977,6 +1076,7 @@ export const executeAgentCommand = async params => {
|
|
|
977
1076
|
operation: 'run_agent_command',
|
|
978
1077
|
});
|
|
979
1078
|
|
|
1079
|
+
await finalizeAgentBidirectionalHandler();
|
|
980
1080
|
await log(`\n\nā Error executing Agent command: ${error.message}`, { level: 'error' });
|
|
981
1081
|
return {
|
|
982
1082
|
success: false,
|