@mikode13/harness 10.0.0 → 11.0.1
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 +31 -16
- package/dist/agent/domain/agent.d.ts +24 -6
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/engines/domain/model/backgroundAgents.d.ts +60 -0
- package/dist/engines/domain/model/backgroundAgents.js +165 -0
- package/dist/engines/domain/model/backgroundAgents.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +4 -11
- package/dist/engines/domain/model/llmAgent.js +103 -83
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.js +2 -2
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +43 -7
- package/dist/tools/domain/preparedCall.js +3 -0
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/infrastructure/agentAsTool.d.ts +11 -9
- package/dist/tools/infrastructure/agentAsTool.js +39 -40
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -1
- package/dist/tools/infrastructure/defineTool.d.ts +14 -1
- package/dist/tools/infrastructure/defineTool.js +24 -0
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.d.ts +2 -2
- package/dist/tools/infrastructure/exploreTool.js +35 -42
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,12 +6,13 @@ import { classifyHostFailure, classifyLocalFailure, classifyProviderFailure, des
|
|
|
6
6
|
import { Conversation } from '#src/llm/domain/conversation';
|
|
7
7
|
import { asNative, nativeOf } from '#src/llm/domain/tool';
|
|
8
8
|
import { readAnswers } from '#src/agent/domain/question';
|
|
9
|
-
import { prepareCall, RunEndingToolError, ToolExecutionError, } from '#src/tools/domain/preparedCall';
|
|
9
|
+
import { isDelegating, prepareCall, RunEndingToolError, ToolExecutionError, } from '#src/tools/domain/preparedCall';
|
|
10
10
|
import { ReadRegistry } from '#src/tools/domain/readRegistry';
|
|
11
11
|
import { InvalidToolInputError } from '#src/tools/domain/tool';
|
|
12
12
|
import { isAbortError } from '#src/shared/domain/isAbortError';
|
|
13
13
|
import { addTokens } from '#src/shared/domain/tokens';
|
|
14
14
|
import { anotherAttempt, askAt, CommandLoops, extensionNote, loopQuestion, loopStop, loopWarning, repeatedAnswersAt, repeatNote, repeatQuestion, repeatStop, stop, warnAt, } from "./commandLoops.js";
|
|
15
|
+
import { BackgroundAgents, sentNote } from "./backgroundAgents.js";
|
|
15
16
|
import { measureContext } from "./stepContext.js";
|
|
16
17
|
import { askUserDefinition, askUserName, describeAnswers, readQuestions, summarizeQuestions, } from "./askUser.js";
|
|
17
18
|
import { followHistory, transcriptOf, undoNote, } from "./historyFollower.js";
|
|
@@ -270,9 +271,11 @@ export class LLMAgent {
|
|
|
270
271
|
* place. Only a `destructive` call is asked about: git can undo a `mutating` one. A throwing
|
|
271
272
|
* approver ends the run, as any host callback does.
|
|
272
273
|
*/
|
|
273
|
-
async denial(call,
|
|
274
|
+
async denial(call, prepared, signal, approve) {
|
|
275
|
+
const { risk } = prepared;
|
|
274
276
|
if (this.autoApprove || risk !== 'destructive')
|
|
275
277
|
return undefined;
|
|
278
|
+
const command = 'command' in prepared ? prepared.command : undefined;
|
|
276
279
|
const retry = 'Do not retry the same call; choose another way, or ask the user.';
|
|
277
280
|
if (!approve) {
|
|
278
281
|
return `The call to "${call.name}" was denied: it is destructive, and no one was asked to allow it. ${retry}`;
|
|
@@ -307,9 +310,10 @@ export class LLMAgent {
|
|
|
307
310
|
/**
|
|
308
311
|
* Always answers the call: a missing tool, a denied call or a failing one becomes an error
|
|
309
312
|
* result, so the model can correct itself. Only a cancellation, a failing approver, or host
|
|
310
|
-
* code that failed inside the call
|
|
313
|
+
* code that failed inside the call escapes. A call that hands tasks to agents answers once
|
|
314
|
+
* it sent them, and `background` watches them from then on.
|
|
311
315
|
*/
|
|
312
|
-
async runTool(call, { signal, onProgress: callback = ignoreProgress, approve, ask }, effects, toolCall) {
|
|
316
|
+
async runTool(call, { signal, onProgress: callback = ignoreProgress, approve, ask }, effects, toolCall, background) {
|
|
313
317
|
const result = { type: 'toolResult', callId: call.id, name: call.name };
|
|
314
318
|
signal.throwIfAborted();
|
|
315
319
|
if (call.name === askUserName && ask && !this.tools.has(askUserName)) {
|
|
@@ -347,7 +351,10 @@ export class LLMAgent {
|
|
|
347
351
|
}
|
|
348
352
|
let prepared;
|
|
349
353
|
try {
|
|
350
|
-
|
|
354
|
+
const context = { ...toolCall, callId: call.id };
|
|
355
|
+
prepared = isDelegating(tool)
|
|
356
|
+
? await tool.delegate(call.input, signal, context)
|
|
357
|
+
: await prepareCall(tool, call.input, signal, context);
|
|
351
358
|
}
|
|
352
359
|
catch (error) {
|
|
353
360
|
signal.throwIfAborted();
|
|
@@ -385,6 +392,25 @@ export class LLMAgent {
|
|
|
385
392
|
// Set before `execute`, so a tool that fails halfway still counts: it may have had effects.
|
|
386
393
|
effects.toolRan = true;
|
|
387
394
|
const unrecorded = prepared.unrecorded === true && prepared.risk !== 'safe';
|
|
395
|
+
if ('start' in prepared) {
|
|
396
|
+
// Before its agents start, so the person watching reads it before anything they narrate.
|
|
397
|
+
emit({
|
|
398
|
+
type: 'tool',
|
|
399
|
+
id: call.id,
|
|
400
|
+
name: call.name,
|
|
401
|
+
status: 'sent',
|
|
402
|
+
...(summary === undefined ? {} : { summary }),
|
|
403
|
+
}, callback);
|
|
404
|
+
const agents = prepared.start(background.signal);
|
|
405
|
+
background.add(call.id, agents, unrecorded);
|
|
406
|
+
return {
|
|
407
|
+
result: { ...result, output: sentNote(agents), isError: false },
|
|
408
|
+
denied: false,
|
|
409
|
+
sent: true,
|
|
410
|
+
unrecorded,
|
|
411
|
+
...(summary === undefined ? {} : { summary }),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
388
414
|
try {
|
|
389
415
|
const output = await prepared.run(signal);
|
|
390
416
|
const ending = prepared.ending?.();
|
|
@@ -451,35 +477,26 @@ export class LLMAgent {
|
|
|
451
477
|
};
|
|
452
478
|
}
|
|
453
479
|
/**
|
|
454
|
-
* One at a time and in order, so their effects and their narration never interleave.
|
|
455
|
-
*
|
|
456
|
-
* to, each narrated as it goes.
|
|
480
|
+
* One at a time and in order, so their effects and their narration never interleave. Agents a
|
|
481
|
+
* call hands tasks to keep working in the background once it answered.
|
|
457
482
|
*/
|
|
458
483
|
async runTools(calls, options, effects, toolCall,
|
|
459
484
|
// Throws the failure of a progress callback a call narrated through.
|
|
460
|
-
throwIfProgressFailed, loops) {
|
|
485
|
+
throwIfProgressFailed, loops, background) {
|
|
461
486
|
const { signal } = options;
|
|
462
487
|
const results = [];
|
|
463
|
-
// The calls before this index already ran, with the ones they ran beside.
|
|
464
|
-
let ranTo = 0;
|
|
465
488
|
for (const [index, call] of calls.entries()) {
|
|
466
|
-
if (index < ranTo)
|
|
467
|
-
continue;
|
|
468
489
|
// A call before this one may have ignored the cancellation; this one must not start,
|
|
469
490
|
// nor be announced as running.
|
|
470
491
|
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
|
-
}
|
|
478
492
|
// `runTool` announces the call only once it is allowed to run, so a consumer never
|
|
479
493
|
// shows a call as running early, nor while the user is still being asked.
|
|
480
|
-
const ran = await this.runTool(call, options, effects, toolCall);
|
|
494
|
+
const ran = await this.runTool(call, options, effects, toolCall, background);
|
|
481
495
|
// A call that failed to narrate ends the run before the next one can start.
|
|
482
496
|
throwIfProgressFailed();
|
|
497
|
+
// An agent at work since the last call may have changed what the history cannot copy.
|
|
498
|
+
if (background.mayHaveChangedUnseen())
|
|
499
|
+
loops.unrecordedCallRan();
|
|
483
500
|
const settled = this.settle(call, ran, loops, options);
|
|
484
501
|
let { result } = settled;
|
|
485
502
|
const { alike, key, label } = settled;
|
|
@@ -508,53 +525,12 @@ export class LLMAgent {
|
|
|
508
525
|
}
|
|
509
526
|
return { message: { role: 'tool', content: results } };
|
|
510
527
|
}
|
|
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
528
|
/**
|
|
553
529
|
* Narrates how a call ended and notes its command's ending, with the warning for the model
|
|
554
530
|
* when that ending keeps coming back alike.
|
|
555
531
|
*/
|
|
556
532
|
settle(call, ran, loops, { onProgress: callback = ignoreProgress, ask }) {
|
|
557
|
-
const { denied, diff, summary, exitCode, ending } = ran;
|
|
533
|
+
const { denied, diff, summary, exitCode, ending, sent } = ran;
|
|
558
534
|
let { result } = ran;
|
|
559
535
|
if (ran.unrecorded)
|
|
560
536
|
loops.unrecordedCallRan();
|
|
@@ -580,6 +556,9 @@ export class LLMAgent {
|
|
|
580
556
|
...(summary === undefined ? {} : { summary }),
|
|
581
557
|
}, callback);
|
|
582
558
|
}
|
|
559
|
+
else if (sent === true) {
|
|
560
|
+
// Narrated as it sent its agents, which go on after it until each one ends.
|
|
561
|
+
}
|
|
583
562
|
// The diff of a change it made, for the person watching; only once it was made.
|
|
584
563
|
else if (diff !== undefined) {
|
|
585
564
|
emit({
|
|
@@ -721,6 +700,22 @@ export class LLMAgent {
|
|
|
721
700
|
}
|
|
722
701
|
async takeSteps(prompt, options, context, seen) {
|
|
723
702
|
const { signal, onProgress: callback = ignoreProgress } = options;
|
|
703
|
+
// A tool narrates through this without ever failing: a failing callback would reach
|
|
704
|
+
// only the tool, which turns any failure into a result for the model. The run ends on
|
|
705
|
+
// it once that tool returns.
|
|
706
|
+
let progressFailure;
|
|
707
|
+
const onProgress = (event) => {
|
|
708
|
+
if (progressFailure)
|
|
709
|
+
return;
|
|
710
|
+
try {
|
|
711
|
+
emit(event, callback);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
progressFailure = { error };
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
// The agents the run's calls send to work in the background.
|
|
718
|
+
const background = new BackgroundAgents(signal, onProgress);
|
|
724
719
|
const start = Date.now();
|
|
725
720
|
const runMessages = [{ role: 'user', content: [{ type: 'text', text: prompt }] }];
|
|
726
721
|
let tokens;
|
|
@@ -764,23 +759,11 @@ export class LLMAgent {
|
|
|
764
759
|
tokens = addTokens(tokens, spent);
|
|
765
760
|
unreported ||= unreportedCall;
|
|
766
761
|
};
|
|
767
|
-
//
|
|
768
|
-
// only the tool, which turns any failure into a result for the model. The run ends on
|
|
769
|
-
// it once that tool returns.
|
|
770
|
-
let progressFailure;
|
|
771
|
-
const onProgress = (event) => {
|
|
772
|
-
if (progressFailure)
|
|
773
|
-
return;
|
|
774
|
-
try {
|
|
775
|
-
emit(event, callback);
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
progressFailure = { error };
|
|
779
|
-
}
|
|
780
|
-
};
|
|
762
|
+
// Also ends the run at its next boundary when an agent at work failed in a way that ends it.
|
|
781
763
|
const throwIfProgressFailed = () => {
|
|
782
764
|
if (progressFailure)
|
|
783
765
|
throw progressFailure.error;
|
|
766
|
+
background.throwIfEnded();
|
|
784
767
|
};
|
|
785
768
|
// The agent's own `askUser` exists only for a run someone can answer, and never beside
|
|
786
769
|
// a consumer's tool of that name, which the consumer gave first and which stays theirs.
|
|
@@ -796,7 +779,18 @@ export class LLMAgent {
|
|
|
796
779
|
if (followed)
|
|
797
780
|
this.seenRevision = followed.latestRevision;
|
|
798
781
|
};
|
|
782
|
+
// A run kept although it failed stops its agents first, and tells the next run what
|
|
783
|
+
// became of each one, which would otherwise wait for answers that never come.
|
|
784
|
+
const keepFailed = async () => {
|
|
785
|
+
await background.stop();
|
|
786
|
+
const ended = background.takeAnswers();
|
|
787
|
+
if (ended)
|
|
788
|
+
runMessages.push(ended);
|
|
789
|
+
remember();
|
|
790
|
+
};
|
|
799
791
|
for (let step = 1; step <= this.maxSteps; step++) {
|
|
792
|
+
// An agent at work may have narrated through a callback that failed.
|
|
793
|
+
throwIfProgressFailed();
|
|
800
794
|
const { response, context: sent } = await this.llmCall(runMessages, after, {
|
|
801
795
|
signal,
|
|
802
796
|
step,
|
|
@@ -808,6 +802,7 @@ export class LLMAgent {
|
|
|
808
802
|
// After the tokens are counted, so a throwing callback still reports them, and for an
|
|
809
803
|
// unusable answer too: the call was sent, and billed.
|
|
810
804
|
this.narrateStep(step, response.usage, { context: sent, tools }, callback);
|
|
805
|
+
throwIfProgressFailed();
|
|
811
806
|
if (response.stopReason === 'truncated') {
|
|
812
807
|
// What it wrote before the output limit, kept with the run so the next run can
|
|
813
808
|
// take it up. Not its calls, whose input may be cut, nor what only the provider
|
|
@@ -817,7 +812,7 @@ export class LLMAgent {
|
|
|
817
812
|
narrate(part, callback);
|
|
818
813
|
if (written.length > 0)
|
|
819
814
|
runMessages.push({ role: 'assistant', content: written });
|
|
820
|
-
|
|
815
|
+
await keepFailed();
|
|
821
816
|
throw new UnrecoverableError('The LLM stopped before completing its answer', {
|
|
822
817
|
cause: 'The model stopped with "truncated". The run, and what the answer held before the limit, are kept in the conversation.',
|
|
823
818
|
});
|
|
@@ -837,6 +832,24 @@ export class LLMAgent {
|
|
|
837
832
|
}
|
|
838
833
|
const calls = response.message.content.filter(part => part.type === 'toolCall');
|
|
839
834
|
if (calls.length === 0) {
|
|
835
|
+
// An answer given while agents work, or before the model read what they answered,
|
|
836
|
+
// is a wait: the model goes on with their answers.
|
|
837
|
+
let answers = background.takeAnswers();
|
|
838
|
+
if (!answers && background.working && step < this.maxSteps) {
|
|
839
|
+
await background.nextAnswer();
|
|
840
|
+
throwIfProgressFailed();
|
|
841
|
+
answers = background.takeAnswers();
|
|
842
|
+
}
|
|
843
|
+
if (answers || background.working) {
|
|
844
|
+
if (step === this.maxSteps) {
|
|
845
|
+
throw new UnrecoverableError('The LLM did not finish within its step limit', {
|
|
846
|
+
cause: `The model was still waiting for the agents it sent after ${String(this.maxSteps)} steps.`,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
if (answers)
|
|
850
|
+
runMessages.push(answers);
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
840
853
|
remember();
|
|
841
854
|
return {
|
|
842
855
|
// Only the final answer: text from earlier steps was narrated as it came.
|
|
@@ -844,7 +857,7 @@ export class LLMAgent {
|
|
|
844
857
|
.filter(part => part.type === 'text')
|
|
845
858
|
.map(part => part.text)
|
|
846
859
|
.join('\n'),
|
|
847
|
-
tokens: unreported ? undefined : tokens,
|
|
860
|
+
tokens: unreported || background.usageUnknown ? undefined : tokens,
|
|
848
861
|
duration: (Date.now() - start) / 1000,
|
|
849
862
|
};
|
|
850
863
|
}
|
|
@@ -875,7 +888,7 @@ export class LLMAgent {
|
|
|
875
888
|
...rest,
|
|
876
889
|
],
|
|
877
890
|
});
|
|
878
|
-
|
|
891
|
+
await keepFailed();
|
|
879
892
|
throw decision;
|
|
880
893
|
}
|
|
881
894
|
repeats.reset();
|
|
@@ -891,13 +904,17 @@ export class LLMAgent {
|
|
|
891
904
|
depth: delegationOf(options)?.depth ?? 0,
|
|
892
905
|
...(options.approve ? { approve: options.approve } : {}),
|
|
893
906
|
...(options.ask ? { ask: options.ask } : {}),
|
|
894
|
-
}, throwIfProgressFailed, loops);
|
|
907
|
+
}, throwIfProgressFailed, loops, background);
|
|
895
908
|
runMessages.push(repeatedNote === undefined ? message : withNote(message, repeatedNote));
|
|
896
909
|
// Kept like a run cut by the output limit: the user may take it up.
|
|
897
910
|
if (stopped) {
|
|
898
|
-
|
|
911
|
+
await keepFailed();
|
|
899
912
|
throw stopped;
|
|
900
913
|
}
|
|
914
|
+
// What agents answered meanwhile, after the results the model is waiting for.
|
|
915
|
+
const answers = background.takeAnswers();
|
|
916
|
+
if (answers)
|
|
917
|
+
runMessages.push(answers);
|
|
901
918
|
}
|
|
902
919
|
}
|
|
903
920
|
throw new UnrecoverableError('The LLM did not finish within its step limit', {
|
|
@@ -905,6 +922,9 @@ export class LLMAgent {
|
|
|
905
922
|
});
|
|
906
923
|
}
|
|
907
924
|
catch (error) {
|
|
925
|
+
// The run ends only after the agents still working, which stop with it: what they
|
|
926
|
+
// changed and spent is the run's.
|
|
927
|
+
await background.stop();
|
|
908
928
|
let failure = error;
|
|
909
929
|
if (effects.toolRan && error instanceof RecoverableError) {
|
|
910
930
|
// RetryingAgent would run the whole prompt again, and with it every tool this run
|
|
@@ -918,7 +938,7 @@ export class LLMAgent {
|
|
|
918
938
|
failure = unrecoverable;
|
|
919
939
|
}
|
|
920
940
|
// The run spent the tokens of every step before the one that failed.
|
|
921
|
-
throw withSpentTokens(failure, tokens, unreported);
|
|
941
|
+
throw withSpentTokens(failure, tokens, unreported || background.usageUnknown);
|
|
922
942
|
}
|
|
923
943
|
}
|
|
924
944
|
}
|