@mikode13/harness 9.3.1 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -7
- package/dist/agent/domain/agent.d.ts +11 -5
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +2 -1
- package/dist/agent/domain/approval.js +2 -1
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/question.d.ts +5 -0
- package/dist/agent/domain/question.js +11 -0
- package/dist/agent/domain/question.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +17 -0
- package/dist/agent/domain/runContext.js +9 -0
- package/dist/agent/domain/runContext.js.map +1 -1
- package/dist/agent/domain/transcript.d.ts +10 -0
- package/dist/agent/domain/transcript.js.map +1 -1
- package/dist/engines/domain/model/askUser.d.ts +0 -5
- package/dist/engines/domain/model/askUser.js +0 -9
- package/dist/engines/domain/model/askUser.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.d.ts +19 -3
- package/dist/engines/domain/model/llmAgent.js +141 -51
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +6 -2
- package/dist/factory/infrastructure/agentLLMFactory.js +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +14 -0
- package/dist/tools/domain/preparedCall.js +12 -0
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/domain/readRegistry.d.ts +7 -0
- package/dist/tools/domain/readRegistry.js.map +1 -1
- package/dist/tools/infrastructure/agentAsTool.d.ts +61 -0
- package/dist/tools/infrastructure/agentAsTool.js +144 -0
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -0
- package/dist/tools/infrastructure/commandTool.js +6 -2
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.js +9 -22
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/dist/tools/infrastructure/fileEditor.d.ts +2 -0
- package/dist/tools/infrastructure/fileEditor.js +5 -1
- package/dist/tools/infrastructure/fileEditor.js.map +1 -1
- package/dist/tools/infrastructure/fileFormatter.d.ts +1 -0
- package/dist/tools/infrastructure/fileFormatter.js +5 -1
- package/dist/tools/infrastructure/fileFormatter.js.map +1 -1
- package/dist/tools/infrastructure/subagents.d.ts +19 -0
- package/dist/tools/infrastructure/subagents.js +29 -0
- package/dist/tools/infrastructure/subagents.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +8 -0
- package/dist/tools/infrastructure/writeSession.js +37 -0
- package/dist/tools/infrastructure/writeSession.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { readDecision, } from '#src/agent/domain/approval';
|
|
2
2
|
import { ignoreProgress, } from '#src/agent/domain/agent';
|
|
3
|
-
import { RunContext, runContextOf, withRunContext, } from '#src/agent/domain/runContext';
|
|
3
|
+
import { delegationOf, RunContext, runContextOf, withRunContext, } from '#src/agent/domain/runContext';
|
|
4
4
|
import { InvalidAgentConfigError, RecoverableError, UnrecoverableError, withSpentTokens, } from '#src/shared/domain/errors';
|
|
5
5
|
import { classifyHostFailure, classifyLocalFailure, classifyProviderFailure, describeFailure, treatErrors, } from '#src/shared/domain/providerFailure';
|
|
6
6
|
import { Conversation } from '#src/llm/domain/conversation';
|
|
7
7
|
import { asNative, nativeOf } from '#src/llm/domain/tool';
|
|
8
|
-
import {
|
|
8
|
+
import { readAnswers } from '#src/agent/domain/question';
|
|
9
|
+
import { prepareCall, RunEndingToolError, ToolExecutionError, } from '#src/tools/domain/preparedCall';
|
|
9
10
|
import { ReadRegistry } from '#src/tools/domain/readRegistry';
|
|
10
11
|
import { InvalidToolInputError } from '#src/tools/domain/tool';
|
|
11
12
|
import { isAbortError } from '#src/shared/domain/isAbortError';
|
|
12
13
|
import { addTokens } from '#src/shared/domain/tokens';
|
|
13
14
|
import { anotherAttempt, askAt, CommandLoops, extensionNote, loopQuestion, loopStop, loopWarning, repeatedAnswersAt, repeatNote, repeatQuestion, repeatStop, stop, warnAt, } from "./commandLoops.js";
|
|
14
15
|
import { measureContext } from "./stepContext.js";
|
|
15
|
-
import { askUserDefinition, askUserName, describeAnswers,
|
|
16
|
+
import { askUserDefinition, askUserName, describeAnswers, readQuestions, summarizeQuestions, } from "./askUser.js";
|
|
16
17
|
import { followHistory, transcriptOf, undoNote, } from "./historyFollower.js";
|
|
17
18
|
function describePart(part) {
|
|
18
19
|
switch (part.type) {
|
|
@@ -305,8 +306,8 @@ export class LLMAgent {
|
|
|
305
306
|
}
|
|
306
307
|
/**
|
|
307
308
|
* Always answers the call: a missing tool, a denied call or a failing one becomes an error
|
|
308
|
-
* result, so the model can correct itself. Only a cancellation,
|
|
309
|
-
*
|
|
309
|
+
* result, so the model can correct itself. Only a cancellation, a failing approver, or host
|
|
310
|
+
* code that failed inside the call, such as an agent's approver, escapes.
|
|
310
311
|
*/
|
|
311
312
|
async runTool(call, { signal, onProgress: callback = ignoreProgress, approve, ask }, effects, toolCall) {
|
|
312
313
|
const result = { type: 'toolResult', callId: call.id, name: call.name };
|
|
@@ -398,6 +399,8 @@ export class LLMAgent {
|
|
|
398
399
|
}
|
|
399
400
|
catch (error) {
|
|
400
401
|
signal.throwIfAborted();
|
|
402
|
+
if (error instanceof RunEndingToolError)
|
|
403
|
+
throw error.failure;
|
|
401
404
|
const ending = prepared.ending?.();
|
|
402
405
|
return {
|
|
403
406
|
result: { ...result, output: describeFailure(error), isError: true },
|
|
@@ -447,60 +450,39 @@ export class LLMAgent {
|
|
|
447
450
|
summary,
|
|
448
451
|
};
|
|
449
452
|
}
|
|
450
|
-
/**
|
|
453
|
+
/**
|
|
454
|
+
* One at a time and in order, so their effects and their narration never interleave. Only
|
|
455
|
+
* calls of a `parallel` tool next to each other run at once, such as agents being delegated
|
|
456
|
+
* to, each narrated as it goes.
|
|
457
|
+
*/
|
|
451
458
|
async runTools(calls, options, effects, toolCall,
|
|
452
459
|
// Throws the failure of a progress callback a call narrated through.
|
|
453
460
|
throwIfProgressFailed, loops) {
|
|
454
|
-
const { signal
|
|
461
|
+
const { signal } = options;
|
|
455
462
|
const results = [];
|
|
463
|
+
// The calls before this index already ran, with the ones they ran beside.
|
|
464
|
+
let ranTo = 0;
|
|
456
465
|
for (const [index, call] of calls.entries()) {
|
|
466
|
+
if (index < ranTo)
|
|
467
|
+
continue;
|
|
457
468
|
// A call before this one may have ignored the cancellation; this one must not start,
|
|
458
469
|
// nor be announced as running.
|
|
459
470
|
signal.throwIfAborted();
|
|
471
|
+
const together = this.parallelFrom(calls, index);
|
|
472
|
+
if (together.length > 1) {
|
|
473
|
+
results.push(...(await this.runTogether(together, options, effects, toolCall, loops)));
|
|
474
|
+
throwIfProgressFailed();
|
|
475
|
+
ranTo = index + together.length;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
460
478
|
// `runTool` announces the call only once it is allowed to run, so a consumer never
|
|
461
479
|
// shows a call as running early, nor while the user is still being asked.
|
|
462
480
|
const ran = await this.runTool(call, options, effects, toolCall);
|
|
463
|
-
const { denied, diff, summary, exitCode, ending } = ran;
|
|
464
|
-
let { result } = ran;
|
|
465
481
|
// A call that failed to narrate ends the run before the next one can start.
|
|
466
482
|
throwIfProgressFailed();
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const key =
|
|
470
|
-
const label = summary ?? call.name;
|
|
471
|
-
const alike = ending === undefined ? undefined : loops.observe(key, ending);
|
|
472
|
-
// A command that changed what the history cannot copy hides that change from the next.
|
|
473
|
-
if (ending !== undefined && ending.state === undefined)
|
|
474
|
-
loops.unrecordedCallRan();
|
|
475
|
-
// Narrated with it, so the person watching reads what the model is told.
|
|
476
|
-
if (alike?.times === warnAt) {
|
|
477
|
-
result = {
|
|
478
|
-
...result,
|
|
479
|
-
output: `${result.output}\n\n${loopWarning(label, alike, options.ask !== undefined && !loops.wentOn)}`,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
if (denied) {
|
|
483
|
-
emit({
|
|
484
|
-
type: 'tool',
|
|
485
|
-
id: call.id,
|
|
486
|
-
name: call.name,
|
|
487
|
-
status: 'denied',
|
|
488
|
-
...(summary === undefined ? {} : { summary }),
|
|
489
|
-
}, callback);
|
|
490
|
-
}
|
|
491
|
-
// The diff of a change it made, for the person watching; only once it was made.
|
|
492
|
-
else if (diff !== undefined) {
|
|
493
|
-
emit({
|
|
494
|
-
type: 'tool',
|
|
495
|
-
id: call.id,
|
|
496
|
-
name: call.name,
|
|
497
|
-
status: 'completed',
|
|
498
|
-
diff,
|
|
499
|
-
...(summary === undefined ? {} : { summary }),
|
|
500
|
-
}, callback);
|
|
501
|
-
}
|
|
502
|
-
else
|
|
503
|
-
narrate(result, callback, summary, exitCode);
|
|
483
|
+
const settled = this.settle(call, ran, loops, options);
|
|
484
|
+
let { result } = settled;
|
|
485
|
+
const { alike, key, label } = settled;
|
|
504
486
|
// After its result is narrated, so the user is asked about the ending they have seen.
|
|
505
487
|
if (alike !== undefined && alike.times >= askAt) {
|
|
506
488
|
const decision = await this.decideOnLoop({
|
|
@@ -526,6 +508,93 @@ export class LLMAgent {
|
|
|
526
508
|
}
|
|
527
509
|
return { message: { role: 'tool', content: results } };
|
|
528
510
|
}
|
|
511
|
+
/** The calls from `index` on whose tool is `parallel`, up to the first that is not. */
|
|
512
|
+
parallelFrom(calls, index) {
|
|
513
|
+
const together = [];
|
|
514
|
+
for (const call of calls.slice(index)) {
|
|
515
|
+
const tool = this.tools.get(call.name);
|
|
516
|
+
if (!tool || !('parallel' in tool) || tool.parallel !== true)
|
|
517
|
+
break;
|
|
518
|
+
together.push(call);
|
|
519
|
+
}
|
|
520
|
+
return together;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Runs the calls at once, their results in the order of the calls. The first failure that
|
|
524
|
+
* ends the run stops the others, and the run ends once every one has.
|
|
525
|
+
*/
|
|
526
|
+
async runTogether(calls, options, effects, toolCall, loops) {
|
|
527
|
+
const stopping = new AbortController();
|
|
528
|
+
const together = { ...options, signal: AbortSignal.any([options.signal, stopping.signal]) };
|
|
529
|
+
const ended = {};
|
|
530
|
+
const settled = await Promise.allSettled(calls.map(async (call) => {
|
|
531
|
+
try {
|
|
532
|
+
const ran = await this.runTool(call, together, effects, toolCall);
|
|
533
|
+
return this.settle(call, ran, loops, options).result;
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
ended.failure ??= { error };
|
|
537
|
+
stopping.abort();
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}));
|
|
541
|
+
// The others were stopped for it: what ended the run is the first failure, or the user.
|
|
542
|
+
options.signal.throwIfAborted();
|
|
543
|
+
if (ended.failure) {
|
|
544
|
+
const { error } = ended.failure;
|
|
545
|
+
// A call stopped on its way may have spent on the model, and a stop carries no tokens:
|
|
546
|
+
// the run's total is unknown, not short.
|
|
547
|
+
const stopped = settled.some(outcome => outcome.status === 'rejected' && outcome.reason !== error);
|
|
548
|
+
throw stopped ? withSpentTokens(error, undefined, true) : error;
|
|
549
|
+
}
|
|
550
|
+
return settled.flatMap(outcome => (outcome.status === 'fulfilled' ? [outcome.value] : []));
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Narrates how a call ended and notes its command's ending, with the warning for the model
|
|
554
|
+
* when that ending keeps coming back alike.
|
|
555
|
+
*/
|
|
556
|
+
settle(call, ran, loops, { onProgress: callback = ignoreProgress, ask }) {
|
|
557
|
+
const { denied, diff, summary, exitCode, ending } = ran;
|
|
558
|
+
let { result } = ran;
|
|
559
|
+
if (ran.unrecorded)
|
|
560
|
+
loops.unrecordedCallRan();
|
|
561
|
+
const key = callKey(call);
|
|
562
|
+
const label = summary ?? call.name;
|
|
563
|
+
const alike = ending === undefined ? undefined : loops.observe(key, ending);
|
|
564
|
+
// A command that changed what the history cannot copy hides that change from the next.
|
|
565
|
+
if (ending !== undefined && ending.state === undefined)
|
|
566
|
+
loops.unrecordedCallRan();
|
|
567
|
+
// Narrated with it, so the person watching reads what the model is told.
|
|
568
|
+
if (alike?.times === warnAt) {
|
|
569
|
+
result = {
|
|
570
|
+
...result,
|
|
571
|
+
output: `${result.output}\n\n${loopWarning(label, alike, ask !== undefined && !loops.wentOn)}`,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (denied) {
|
|
575
|
+
emit({
|
|
576
|
+
type: 'tool',
|
|
577
|
+
id: call.id,
|
|
578
|
+
name: call.name,
|
|
579
|
+
status: 'denied',
|
|
580
|
+
...(summary === undefined ? {} : { summary }),
|
|
581
|
+
}, callback);
|
|
582
|
+
}
|
|
583
|
+
// The diff of a change it made, for the person watching; only once it was made.
|
|
584
|
+
else if (diff !== undefined) {
|
|
585
|
+
emit({
|
|
586
|
+
type: 'tool',
|
|
587
|
+
id: call.id,
|
|
588
|
+
name: call.name,
|
|
589
|
+
status: 'completed',
|
|
590
|
+
diff,
|
|
591
|
+
...(summary === undefined ? {} : { summary }),
|
|
592
|
+
}, callback);
|
|
593
|
+
}
|
|
594
|
+
else
|
|
595
|
+
narrate(result, callback, summary, exitCode);
|
|
596
|
+
return { result, alike, key, label };
|
|
597
|
+
}
|
|
529
598
|
/**
|
|
530
599
|
* On a loop found, the fifth ending alike of a command or the fifth answer in a row asking for
|
|
531
600
|
* the same calls: the note for the model if the user lets the run go on, or the failure that
|
|
@@ -607,16 +676,17 @@ export class LLMAgent {
|
|
|
607
676
|
response = await this.takeSteps(prompt, options, context, seen);
|
|
608
677
|
}
|
|
609
678
|
catch (error) {
|
|
610
|
-
await this.transcribe(seen, isAbortError(error) ? 'cancelled' : 'failed', error);
|
|
679
|
+
await this.transcribe(seen, options, isAbortError(error) ? 'cancelled' : 'failed', error);
|
|
611
680
|
throw error;
|
|
612
681
|
}
|
|
613
|
-
await this.transcribe(seen, 'completed');
|
|
682
|
+
await this.transcribe(seen, options, 'completed');
|
|
614
683
|
return response;
|
|
615
684
|
}
|
|
616
685
|
/** Hands the run's transcript to the consumer, whose failure is logged, not the run's. */
|
|
617
|
-
async transcribe(seen, outcome, error) {
|
|
686
|
+
async transcribe(seen, options, outcome, error) {
|
|
618
687
|
if (!this.onTranscript)
|
|
619
688
|
return;
|
|
689
|
+
const delegation = delegationOf(options);
|
|
620
690
|
try {
|
|
621
691
|
// A copy, so a consumer that changes it cannot change the agent's tools or conversation.
|
|
622
692
|
const content = structuredClone({
|
|
@@ -629,7 +699,19 @@ export class LLMAgent {
|
|
|
629
699
|
messages: seen.messages(),
|
|
630
700
|
...(outcome === 'completed' ? {} : { error: describeFailure(error) }),
|
|
631
701
|
});
|
|
632
|
-
await this.onTranscript({
|
|
702
|
+
await this.onTranscript({
|
|
703
|
+
...(delegation
|
|
704
|
+
? {
|
|
705
|
+
delegation: {
|
|
706
|
+
tool: delegation.tool,
|
|
707
|
+
callId: delegation.callId,
|
|
708
|
+
subagent: delegation.subagent,
|
|
709
|
+
},
|
|
710
|
+
}
|
|
711
|
+
: {}),
|
|
712
|
+
outcome,
|
|
713
|
+
content,
|
|
714
|
+
});
|
|
633
715
|
}
|
|
634
716
|
catch (failure) {
|
|
635
717
|
treatErrors(() => {
|
|
@@ -801,7 +883,15 @@ export class LLMAgent {
|
|
|
801
883
|
}
|
|
802
884
|
// On the last step no call is left to send the results to, so the tools do not run.
|
|
803
885
|
if (step < this.maxSteps) {
|
|
804
|
-
const { message, stopped } = await this.runTools(calls, options, effects, {
|
|
886
|
+
const { message, stopped } = await this.runTools(calls, options, effects, {
|
|
887
|
+
run: context,
|
|
888
|
+
reads,
|
|
889
|
+
onProgress,
|
|
890
|
+
spend,
|
|
891
|
+
depth: delegationOf(options)?.depth ?? 0,
|
|
892
|
+
...(options.approve ? { approve: options.approve } : {}),
|
|
893
|
+
...(options.ask ? { ask: options.ask } : {}),
|
|
894
|
+
}, throwIfProgressFailed, loops);
|
|
805
895
|
runMessages.push(repeatedNote === undefined ? message : withNote(message, repeatedNote));
|
|
806
896
|
// Kept like a run cut by the output limit: the user may take it up.
|
|
807
897
|
if (stopped) {
|