omk-agent-core 0.98.5 → 1.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/CHANGELOG.md +8 -0
- package/dist/agent-loop.d.ts +0 -12
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +247 -152
- package/dist/agent-loop.js.map +1 -1
- package/dist/tool-dag-ecraf.d.ts +73 -0
- package/dist/tool-dag-ecraf.d.ts.map +1 -0
- package/dist/tool-dag-ecraf.js +190 -0
- package/dist/tool-dag-ecraf.js.map +1 -0
- package/dist/tool-dag-memo.d.ts +21 -0
- package/dist/tool-dag-memo.d.ts.map +1 -0
- package/dist/tool-dag-memo.js +75 -0
- package/dist/tool-dag-memo.js.map +1 -0
- package/dist/tool-dag-scheduler.d.ts +23 -0
- package/dist/tool-dag-scheduler.d.ts.map +1 -1
- package/dist/tool-dag-scheduler.js +34 -1
- package/dist/tool-dag-scheduler.js.map +1 -1
- package/package.json +2 -2
package/dist/agent-loop.js
CHANGED
|
@@ -7,8 +7,10 @@ import { bindToolIdentity } from "./builtin-tool-resource-claims.js";
|
|
|
7
7
|
import { partitionToolBatchWaves } from "./parallel-tool-batch.js";
|
|
8
8
|
import { pinProviderConfig, requestAssistantResponse } from "./provider-request.js";
|
|
9
9
|
export { getVisionRouteModel, isVisionRouteModel, VISION_ROUTE_MODEL } from "./vision-route.js";
|
|
10
|
-
import {
|
|
10
|
+
import { scheduleDagLevelsMemo as scheduleDagLevelsMemoFromModule } from "./tool-dag-memo.js";
|
|
11
|
+
import { applyConcurrencyCap, assignDagDependencies, conflictsWithUnsettledClaim } from "./tool-dag-scheduler.js";
|
|
11
12
|
import { awaitWithAbort, createErrorToolResult, createImmutableJsonSnapshot, createImmutableSnapshot, finalizeExecutedToolCall, parseJsonValue, stampToolResultEnvelope, } from "./tool-execution-boundary.js";
|
|
13
|
+
import { resolveToolClaimsForCall } from "./tool-resource-claims.js";
|
|
12
14
|
import { resolveToolTimeoutMs, runToolCallWithTimeout } from "./tool-timeout.js";
|
|
13
15
|
import { hasUnsettledTimeout } from "./tool-timeout-settlement.js";
|
|
14
16
|
import { createSyntheticToolResult, inspectTranscriptIntegrity, repairTranscriptIntegrity, } from "./tool-transcript-integrity.js";
|
|
@@ -536,62 +538,6 @@ async function executeToolCallsInWaves(currentContext, assistantMessage, toolCal
|
|
|
536
538
|
}
|
|
537
539
|
return { messages, terminate: terminated, stopRun };
|
|
538
540
|
}
|
|
539
|
-
const DAG_SCHEDULE_CACHE_LIMIT = 64;
|
|
540
|
-
/**
|
|
541
|
-
* Canonical key covering every input claim resolution depends on. A custom
|
|
542
|
-
* `resourceKeyResolver` function cannot be fingerprinted, so callers skip the
|
|
543
|
-
* memo entirely when one is configured. Within a run, tool definitions (and
|
|
544
|
-
* their `resourceClaims` closures) are stable, so name/mode/claims-presence
|
|
545
|
-
* fingerprints are sufficient.
|
|
546
|
-
*/
|
|
547
|
-
function dagScheduleCacheKey(toolCalls, options) {
|
|
548
|
-
const policies = [...(options.toolPolicies?.entries() ?? [])].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
549
|
-
const registered = (options.registeredTools ?? []).map((tool) => [
|
|
550
|
-
tool.name,
|
|
551
|
-
tool.executionMode ?? "",
|
|
552
|
-
typeof tool.resourceClaims === "function" ? "1" : "0",
|
|
553
|
-
]);
|
|
554
|
-
return JSON.stringify([
|
|
555
|
-
toolCalls.map((call) => [call.name, call.arguments ?? null]),
|
|
556
|
-
options.cwd,
|
|
557
|
-
options.strictExtensionClaims === true,
|
|
558
|
-
options.maxConcurrency ?? null,
|
|
559
|
-
policies,
|
|
560
|
-
registered,
|
|
561
|
-
]);
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Schedule with a per-run memo. Identical batches (provider retries, stubborn
|
|
565
|
-
* re-emissions) re-resolve path identities and custom claims; the plan is a
|
|
566
|
-
* pure function of the canonical inputs, so replaying it is safe. Returns
|
|
567
|
-
* `null` when the underlying schedule was aborted. Cached levels are handed
|
|
568
|
-
* out as copies because callers append to and reorder them.
|
|
569
|
-
*/
|
|
570
|
-
export async function scheduleDagLevelsMemo(toolCalls, options, signal, cache) {
|
|
571
|
-
if (options.resourceKeyResolver) {
|
|
572
|
-
const scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);
|
|
573
|
-
return scheduled.kind === "aborted" ? null : scheduled.value;
|
|
574
|
-
}
|
|
575
|
-
const key = dagScheduleCacheKey(toolCalls, options);
|
|
576
|
-
const cached = cache.get(key);
|
|
577
|
-
if (cached) {
|
|
578
|
-
cache.delete(key);
|
|
579
|
-
cache.set(key, cached);
|
|
580
|
-
return { levels: cached.levels.map((level) => level.slice()), planKey: cached.planKey };
|
|
581
|
-
}
|
|
582
|
-
const scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);
|
|
583
|
-
if (scheduled.kind === "aborted") {
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
if (cache.size >= DAG_SCHEDULE_CACHE_LIMIT) {
|
|
587
|
-
const oldest = cache.keys().next();
|
|
588
|
-
if (!oldest.done) {
|
|
589
|
-
cache.delete(oldest.value);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
cache.set(key, { levels: scheduled.value.levels.map((level) => level.slice()), planKey: scheduled.value.planKey });
|
|
593
|
-
return scheduled.value;
|
|
594
|
-
}
|
|
595
541
|
/**
|
|
596
542
|
* Schedule planned calls into candidate DAG levels. Immediate plans fail
|
|
597
543
|
* before any tool executes, so they carry no claims and fold into the first
|
|
@@ -612,7 +558,7 @@ async function schedulePlannedDagLevels(plans, toolPolicies, boundTools, config,
|
|
|
612
558
|
immediateSourceIndices.push(sourceIndex);
|
|
613
559
|
}
|
|
614
560
|
});
|
|
615
|
-
const scheduled = await
|
|
561
|
+
const scheduled = await scheduleDagLevelsMemoFromModule(claimableCalls, {
|
|
616
562
|
cwd: config.cwd ?? process.cwd(),
|
|
617
563
|
toolPolicies,
|
|
618
564
|
registeredTools: boundTools,
|
|
@@ -621,17 +567,29 @@ async function schedulePlannedDagLevels(plans, toolPolicies, boundTools, config,
|
|
|
621
567
|
resourceKeyResolver: config.resourceKeyResolver,
|
|
622
568
|
}, signal, dagScheduleCache);
|
|
623
569
|
if (scheduled === null) {
|
|
624
|
-
return [];
|
|
570
|
+
return { levels: [], resolutions: new Map(), dependencies: new Map() };
|
|
625
571
|
}
|
|
572
|
+
const resolutions = new Map();
|
|
573
|
+
scheduled.entries.forEach((entry, position) => {
|
|
574
|
+
resolutions.set(schedulableSourceIndices[position], entry.resolution);
|
|
575
|
+
});
|
|
576
|
+
// Precedence graph in source-index space. Immediate plans carry no claims and
|
|
577
|
+
// therefore no predecessors; they already fail before any tool executes.
|
|
578
|
+
const dependencies = new Map();
|
|
579
|
+
for (const sourceIndex of immediateSourceIndices)
|
|
580
|
+
dependencies.set(sourceIndex, []);
|
|
581
|
+
assignDagDependencies(scheduled.entries).forEach((blockers, position) => {
|
|
582
|
+
dependencies.set(schedulableSourceIndices[position], blockers.map((blocker) => schedulableSourceIndices[blocker]));
|
|
583
|
+
});
|
|
626
584
|
const levels = scheduled.levels.map((level) => level.map((position) => schedulableSourceIndices[position]));
|
|
627
585
|
if (immediateSourceIndices.length === 0) {
|
|
628
|
-
return levels;
|
|
586
|
+
return { levels, resolutions, dependencies };
|
|
629
587
|
}
|
|
630
588
|
if (levels.length === 0) {
|
|
631
|
-
return [[...immediateSourceIndices]];
|
|
589
|
+
return { levels: [[...immediateSourceIndices]], resolutions, dependencies };
|
|
632
590
|
}
|
|
633
591
|
levels[0] = [...levels[0], ...immediateSourceIndices].sort((left, right) => left - right);
|
|
634
|
-
return levels;
|
|
592
|
+
return { levels, resolutions, dependencies };
|
|
635
593
|
}
|
|
636
594
|
/**
|
|
637
595
|
* Execute a tool-call batch using the dag-v2 scheduler.
|
|
@@ -654,27 +612,25 @@ async function executeToolCallsDagLevels(currentContext, assistantMessage, toolC
|
|
|
654
612
|
if (mode && !toolPolicies.has(tool.name))
|
|
655
613
|
toolPolicies.set(tool.name, mode);
|
|
656
614
|
}
|
|
657
|
-
const
|
|
615
|
+
const schedule = await schedulePlannedDagLevels(plans, toolPolicies, boundTools, config, signal, dagScheduleCache);
|
|
616
|
+
const levels = schedule.levels;
|
|
617
|
+
const batchState = {
|
|
618
|
+
resolutions: schedule.resolutions,
|
|
619
|
+
deferred: new Map(),
|
|
620
|
+
settled: new Set(),
|
|
621
|
+
};
|
|
658
622
|
const finalizedByIndex = new Array(toolCalls.length).fill(undefined);
|
|
659
623
|
let skippedReason;
|
|
660
624
|
let stoppedByUnsettledTimeout = false;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
skippedReason = "Skipped because a preceding DAG tool timed out before its execution promise settled";
|
|
671
|
-
stoppedByUnsettledTimeout = true;
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
if (shouldTerminateToolBatch(executedLevel.outcomes.map((outcome) => outcome.finalized))) {
|
|
675
|
-
skippedReason = "Skipped because the preceding DAG level requested termination";
|
|
676
|
-
break;
|
|
677
|
-
}
|
|
625
|
+
const frontier = await runDagFrontier(currentContext, assistantMessage, levels.flat(), schedule.dependencies, toolCalls, plans, toolPolicies, config, signal, emit, batchState);
|
|
626
|
+
for (const outcome of frontier.outcomes)
|
|
627
|
+
finalizedByIndex[outcome.sourceIndex] = outcome.finalized;
|
|
628
|
+
if (frontier.stoppedByUnsettledTimeout) {
|
|
629
|
+
skippedReason = "Skipped because a preceding DAG tool timed out before its execution promise settled";
|
|
630
|
+
stoppedByUnsettledTimeout = true;
|
|
631
|
+
}
|
|
632
|
+
else if (frontier.terminated) {
|
|
633
|
+
skippedReason = "Skipped because the preceding DAG level requested termination";
|
|
678
634
|
}
|
|
679
635
|
const messages = [];
|
|
680
636
|
const finalizedCalls = [];
|
|
@@ -710,85 +666,224 @@ async function executeToolCallsDagLevels(currentContext, assistantMessage, toolC
|
|
|
710
666
|
stopRun: stoppedByUnsettledTimeout,
|
|
711
667
|
};
|
|
712
668
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
669
|
+
// Re-resolve claims for a prepared call from its exact post-hook arguments.
|
|
670
|
+
// Resolution failures fail closed as an immediate error rather than guessing
|
|
671
|
+
// at a safe scope.
|
|
672
|
+
async function resolveFinalResolution(preparation, toolPolicies, config, signal) {
|
|
673
|
+
if (signal?.aborted)
|
|
674
|
+
return immediateOutcome("aborted", "Operation aborted");
|
|
675
|
+
try {
|
|
676
|
+
return await resolveToolClaimsForCall({ id: preparation.toolCall.id, name: preparation.toolCall.name, arguments: preparation.args }, {
|
|
677
|
+
cwd: config.cwd ?? process.cwd(),
|
|
678
|
+
toolPolicies,
|
|
679
|
+
registeredTools: [preparation.tool],
|
|
680
|
+
strictExtensionClaims: config.strictExtensionClaims,
|
|
681
|
+
resourceKeyResolver: config.resourceKeyResolver,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
return immediateOutcome("failed", error instanceof Error ? error.message : String(error));
|
|
686
|
+
}
|
|
729
687
|
}
|
|
730
|
-
/**
|
|
731
|
-
|
|
688
|
+
/** No call in the ready queue is pre-ordered against another, so drift checks see no exempt peers. */
|
|
689
|
+
const NO_READY_PEERS = new Set();
|
|
690
|
+
/**
|
|
691
|
+
* Execute a planned batch as a dependency ready queue.
|
|
692
|
+
*
|
|
693
|
+
* States are `pending -> ready -> running -> settled`. A call is admitted once
|
|
694
|
+
* every call it actually conflicts with has settled, rather than once its whole
|
|
695
|
+
* barrier level has, so one slow unrelated call no longer delays independent
|
|
696
|
+
* work queued behind it.
|
|
697
|
+
*
|
|
698
|
+
* Admission — authorize, re-resolve claims from exact post-hook arguments, and
|
|
699
|
+
* enforce the final-claims contract — runs one call at a time in source order.
|
|
700
|
+
* That keeps hook ordering and the conflict contract identical to the barrier
|
|
701
|
+
* executor and leaves the shared batch state free of concurrent mutation; only
|
|
702
|
+
* tool execution overlaps. Simultaneously-ready calls are admitted by source
|
|
703
|
+
* index, so admission order stays deterministic even though completion order
|
|
704
|
+
* is not.
|
|
705
|
+
*
|
|
706
|
+
* A call whose post-hook claims newly conflict with an earlier unsettled call
|
|
707
|
+
* yields its turn and is retried after the next settle, which preserves the
|
|
708
|
+
* source-order conflict contract without a separate drain pass.
|
|
709
|
+
*/
|
|
710
|
+
async function runDagFrontier(currentContext, assistantMessage, order, dependencies, toolCalls, plans, toolPolicies, config, signal, emit, state) {
|
|
732
711
|
const outcomes = [];
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
712
|
+
const pending = [...order].sort((left, right) => left - right);
|
|
713
|
+
const running = new Map();
|
|
714
|
+
// Mirrors applyConcurrencyCap: absent, non-finite, or non-positive is unbounded.
|
|
715
|
+
const cap = typeof config.maxToolConcurrency === "number" &&
|
|
716
|
+
Number.isFinite(config.maxToolConcurrency) &&
|
|
717
|
+
config.maxToolConcurrency > 0
|
|
718
|
+
? Math.max(1, Math.floor(config.maxToolConcurrency))
|
|
719
|
+
: Number.POSITIVE_INFINITY;
|
|
720
|
+
let stoppedByUnsettledTimeout = false;
|
|
721
|
+
let terminated = false;
|
|
722
|
+
// Readiness bookkeeping: each pending call tracks how many of its
|
|
723
|
+
// predecessors are still unsettled, and each settle decrements only its
|
|
724
|
+
// successors — the per-settle cost is proportional to the out-degree of the
|
|
725
|
+
// finished call, not to a full rescan of every pending dependency list.
|
|
726
|
+
const remainingPredecessors = new Map();
|
|
727
|
+
const successors = new Map();
|
|
728
|
+
for (const sourceIndex of pending) {
|
|
729
|
+
const blockers = dependencies.get(sourceIndex) ?? [];
|
|
730
|
+
remainingPredecessors.set(sourceIndex, blockers.filter((blocker) => !state.settled.has(blocker)).length);
|
|
731
|
+
for (const blocker of blockers) {
|
|
732
|
+
const followers = successors.get(blocker);
|
|
733
|
+
if (followers)
|
|
734
|
+
followers.push(sourceIndex);
|
|
735
|
+
else
|
|
736
|
+
successors.set(blocker, [sourceIndex]);
|
|
753
737
|
}
|
|
754
|
-
if (signal?.aborted)
|
|
755
|
-
return { outcomes, stoppedByUnsettledTimeout: false };
|
|
756
738
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
return { outcomes, stoppedByUnsettledTimeout: false };
|
|
764
|
-
for (const executionLevel of executionLevels) {
|
|
765
|
-
if (signal?.aborted)
|
|
766
|
-
break;
|
|
767
|
-
for (const entryIndex of executionLevel) {
|
|
768
|
-
await emitToolExecutionStart(runnable[entryIndex].preparation, emit);
|
|
739
|
+
const settle = async (outcome) => {
|
|
740
|
+
outcomes.push(outcome);
|
|
741
|
+
state.settled.add(outcome.sourceIndex);
|
|
742
|
+
for (const follower of successors.get(outcome.sourceIndex) ?? []) {
|
|
743
|
+
const remaining = (remainingPredecessors.get(follower) ?? 0) - 1;
|
|
744
|
+
remainingPredecessors.set(follower, Math.max(0, remaining));
|
|
769
745
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
746
|
+
if (await hasUnsettledTimeout([outcome.finalized]))
|
|
747
|
+
stoppedByUnsettledTimeout = true;
|
|
748
|
+
};
|
|
749
|
+
try {
|
|
750
|
+
while (!signal?.aborted && !stoppedByUnsettledTimeout && !terminated) {
|
|
751
|
+
// Termination is a property of a settled group, not of one call:
|
|
752
|
+
// shouldTerminateToolBatch requires every member to terminate. Evaluating it
|
|
753
|
+
// only when nothing is in flight keeps the barrier executor's semantics,
|
|
754
|
+
// where a call sharing a level with a non-terminating peer did not end the
|
|
755
|
+
// batch. Judging a single outcome would terminate on the first terminating
|
|
756
|
+
// call and strand its concurrent peers.
|
|
757
|
+
if (running.size === 0 && shouldTerminateToolBatch(outcomes.map((outcome) => outcome.finalized))) {
|
|
758
|
+
terminated = true;
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
let admitted = false;
|
|
762
|
+
for (let position = 0; position < pending.length && running.size < cap;) {
|
|
763
|
+
if (signal?.aborted || stoppedByUnsettledTimeout)
|
|
764
|
+
break;
|
|
765
|
+
const sourceIndex = pending[position];
|
|
766
|
+
if ((remainingPredecessors.get(sourceIndex) ?? 0) > 0) {
|
|
767
|
+
position++;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const cached = state.deferred.get(sourceIndex);
|
|
771
|
+
let preparation;
|
|
772
|
+
let resolved;
|
|
773
|
+
if (cached) {
|
|
774
|
+
preparation = cached.preparation;
|
|
775
|
+
resolved = cached.resolution;
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
const plan = plans[sourceIndex];
|
|
779
|
+
const preparedOutcome = plan.kind === "immediate"
|
|
780
|
+
? plan
|
|
781
|
+
: await authorizePlannedToolCall(currentContext, assistantMessage, plan, config, signal);
|
|
782
|
+
if (preparedOutcome.kind === "immediate") {
|
|
783
|
+
pending.splice(position, 1);
|
|
784
|
+
admitted = true;
|
|
785
|
+
await settle({
|
|
786
|
+
sourceIndex,
|
|
787
|
+
finalized: {
|
|
788
|
+
toolCall: toolCalls[sourceIndex],
|
|
789
|
+
result: preparedOutcome.result,
|
|
790
|
+
isError: preparedOutcome.isError,
|
|
791
|
+
envelope: preparedOutcome.envelope,
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
preparation = preparedOutcome;
|
|
797
|
+
const resolution = await resolveFinalResolution(preparation, toolPolicies, config, signal);
|
|
798
|
+
if (resolution.kind === "immediate") {
|
|
799
|
+
pending.splice(position, 1);
|
|
800
|
+
admitted = true;
|
|
801
|
+
await settle({
|
|
802
|
+
sourceIndex,
|
|
803
|
+
finalized: {
|
|
804
|
+
toolCall: preparation.toolCall,
|
|
805
|
+
result: resolution.result,
|
|
806
|
+
isError: resolution.isError,
|
|
807
|
+
envelope: resolution.envelope,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
resolved = resolution;
|
|
813
|
+
}
|
|
814
|
+
state.resolutions.set(sourceIndex, resolved);
|
|
815
|
+
if (conflictsWithUnsettledClaim(sourceIndex, resolved, NO_READY_PEERS, state.settled, new Set(running.keys()), state.resolutions)) {
|
|
816
|
+
// Defer without losing the prepared call: a later scan retries
|
|
817
|
+
// admission from this cache instead of re-invoking authorization.
|
|
818
|
+
if (!cached)
|
|
819
|
+
state.deferred.set(sourceIndex, { preparation, resolution: resolved });
|
|
820
|
+
position++;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
state.deferred.delete(sourceIndex);
|
|
824
|
+
pending.splice(position, 1);
|
|
825
|
+
admitted = true;
|
|
826
|
+
await emitToolExecutionStart(preparation, emit);
|
|
827
|
+
running.set(sourceIndex, (async () => {
|
|
828
|
+
const executed = await executePreparedToolCall(preparation, config, signal, emit);
|
|
829
|
+
const finalized = await finalizeExecutedToolCall({
|
|
830
|
+
currentContext,
|
|
831
|
+
assistantMessage,
|
|
832
|
+
prepared: preparation,
|
|
833
|
+
executed,
|
|
834
|
+
afterToolCall: config.afterToolCall,
|
|
835
|
+
signal,
|
|
836
|
+
});
|
|
837
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
838
|
+
return { sourceIndex, finalized };
|
|
839
|
+
})());
|
|
840
|
+
}
|
|
841
|
+
if (running.size === 0) {
|
|
842
|
+
// Nothing is in flight: either every call settled, or the remainder is
|
|
843
|
+
// blocked with nothing left that could unblock it. A pending call that
|
|
844
|
+
// can never become ready still gets an explicit terminal outcome rather
|
|
845
|
+
// than vanishing from the batch.
|
|
846
|
+
if (!admitted) {
|
|
847
|
+
for (const sourceIndex of pending.splice(0)) {
|
|
848
|
+
await settle({
|
|
849
|
+
sourceIndex,
|
|
850
|
+
finalized: {
|
|
851
|
+
toolCall: toolCalls[sourceIndex],
|
|
852
|
+
result: createErrorToolResult("DAG dependency deadlock: call could not become ready and nothing is running to unblock it"),
|
|
853
|
+
isError: true,
|
|
854
|
+
envelope: createToolResultEnvelope({
|
|
855
|
+
disposition: "failed",
|
|
856
|
+
synthetic: true,
|
|
857
|
+
executionStarted: false,
|
|
858
|
+
reason: "DAG dependency deadlock",
|
|
859
|
+
}),
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const finished = await Promise.race([...running.values()]);
|
|
868
|
+
running.delete(finished.sourceIndex);
|
|
869
|
+
await settle(finished);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
finally {
|
|
873
|
+
// Join work already in flight: cancellation, termination, and propagation
|
|
874
|
+
// of preparation/event errors all preserve ownership of started
|
|
875
|
+
// executions rather than abandoning them mid-flight.
|
|
876
|
+
while (running.size > 0) {
|
|
877
|
+
const entries = [...running.entries()];
|
|
878
|
+
const drained = await Promise.allSettled(entries.map(([, task]) => task));
|
|
879
|
+
for (const [index, drainedResult] of drained.entries()) {
|
|
880
|
+
running.delete(entries[index][0]);
|
|
881
|
+
if (drainedResult.status === "fulfilled")
|
|
882
|
+
await settle(drainedResult.value);
|
|
883
|
+
}
|
|
789
884
|
}
|
|
790
885
|
}
|
|
791
|
-
return { outcomes, stoppedByUnsettledTimeout
|
|
886
|
+
return { outcomes, stoppedByUnsettledTimeout, terminated };
|
|
792
887
|
}
|
|
793
888
|
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
794
889
|
const finalizedCalls = [];
|