agent-swarm-kit 4.0.0 → 5.1.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/build/index.cjs +397 -126
- package/build/index.mjs +397 -126
- package/package.json +2 -2
- package/types.d.ts +60 -13
package/build/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import * as path from 'path';
|
|
|
8
8
|
import path__default, { join } from 'path';
|
|
9
9
|
import crypto, { createHash } from 'crypto';
|
|
10
10
|
import os from 'os';
|
|
11
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
11
12
|
import { getMomentStamp, getTimeStamp } from 'get-moment-stamp';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -3502,8 +3503,23 @@ const createToolCall = async (idx, tool, toolCalls, targetFn, reason, mode, outp
|
|
|
3502
3503
|
// handles this late error (e.g. changeToAgent recursion guard thrown after
|
|
3503
3504
|
// commitToolOutput). Without a recovery emission the pending waitForOutput
|
|
3504
3505
|
// of the enclosing execution would hang forever.
|
|
3505
|
-
|
|
3506
|
-
|
|
3506
|
+
try {
|
|
3507
|
+
const result = await self._resurrectModel(mode, `Late tool error after commit: name=${tool.function.name} error=${getErrorMessage(error)}`);
|
|
3508
|
+
await self._emitOutput(mode, result, outputEpoch);
|
|
3509
|
+
}
|
|
3510
|
+
catch (recoveryError) {
|
|
3511
|
+
// createToolCall is fired without await: a throwing recovery
|
|
3512
|
+
// (_emitOutput throws by contract when validation fails twice) would
|
|
3513
|
+
// be an unhandled rejection. Resolve the waiter directly instead.
|
|
3514
|
+
console.error(`agent-swarm late tool error recovery failed functionName=${tool.function.name} error=${getErrorMessage(recoveryError)}`);
|
|
3515
|
+
await errorSubject.next([
|
|
3516
|
+
self.params.clientId,
|
|
3517
|
+
recoveryError,
|
|
3518
|
+
]);
|
|
3519
|
+
if (outputEpoch === self._outputEpoch) {
|
|
3520
|
+
await self._outputSubject.next(createPlaceholder());
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3507
3523
|
}
|
|
3508
3524
|
}
|
|
3509
3525
|
finally {
|
|
@@ -3568,7 +3584,7 @@ const RUN_FN = async (incoming, self) => {
|
|
|
3568
3584
|
agentName: self.params.agentName,
|
|
3569
3585
|
content: incoming,
|
|
3570
3586
|
mode: "user",
|
|
3571
|
-
role: "
|
|
3587
|
+
role: "user",
|
|
3572
3588
|
});
|
|
3573
3589
|
const args = {
|
|
3574
3590
|
clientId: self.params.clientId,
|
|
@@ -3667,19 +3683,29 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3667
3683
|
if (!toolCalls.length) {
|
|
3668
3684
|
// maxToolCalls=0 dropped every call: the model wanted tools but none may
|
|
3669
3685
|
// run. Emit the (tool-stripped) content as the answer instead of leaving
|
|
3670
|
-
// the pending waitForOutput hanging forever.
|
|
3671
|
-
|
|
3686
|
+
// the pending waitForOutput hanging forever. _emitOutput owns the
|
|
3687
|
+
// transform, so pass the raw content — transforming here too would
|
|
3688
|
+
// apply a non-idempotent transform twice.
|
|
3672
3689
|
await self.params.history.push({
|
|
3673
3690
|
...message,
|
|
3674
3691
|
tool_calls: [],
|
|
3675
3692
|
agentName: self.params.agentName,
|
|
3676
3693
|
});
|
|
3677
|
-
await self._emitOutput(mode,
|
|
3694
|
+
await self._emitOutput(mode, message.content, outputEpoch);
|
|
3678
3695
|
return;
|
|
3679
3696
|
}
|
|
3680
3697
|
{
|
|
3681
|
-
|
|
3682
|
-
|
|
3698
|
+
// Navigation/action tools are registered by toolName, while the model
|
|
3699
|
+
// calls tools by function.name — check both, since they diverge for MCP
|
|
3700
|
+
// tools (mcp_ prefix) and dynamic tool schemas.
|
|
3701
|
+
const priorityTool = toolCalls.find((call) => {
|
|
3702
|
+
const targetFn = toolList.find((t) => t.function.name === call.function.name);
|
|
3703
|
+
return (swarm$1.navigationSchemaService.hasTool(call.function.name) ||
|
|
3704
|
+
swarm$1.actionSchemaService.hasTool(call.function.name) ||
|
|
3705
|
+
(targetFn &&
|
|
3706
|
+
(swarm$1.navigationSchemaService.hasTool(targetFn.toolName) ||
|
|
3707
|
+
swarm$1.actionSchemaService.hasTool(targetFn.toolName))));
|
|
3708
|
+
});
|
|
3683
3709
|
if (priorityTool) {
|
|
3684
3710
|
toolCalls = [priorityTool];
|
|
3685
3711
|
}
|
|
@@ -3704,6 +3730,11 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3704
3730
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3705
3731
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${result}`);
|
|
3706
3732
|
await self._emitOutput(mode, result, outputEpoch);
|
|
3733
|
+
// The chain's .finally decrement is attached only after the loop, so
|
|
3734
|
+
// an early return must roll the counter back itself — otherwise a
|
|
3735
|
+
// late tool error would forever think a chain is still consuming
|
|
3736
|
+
// events and skip its own recovery.
|
|
3737
|
+
self._activeToolChains -= 1;
|
|
3707
3738
|
run(false);
|
|
3708
3739
|
return;
|
|
3709
3740
|
}
|
|
@@ -3737,6 +3768,9 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3737
3768
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3738
3769
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${result}`);
|
|
3739
3770
|
await self._emitOutput(mode, result, outputEpoch);
|
|
3771
|
+
// See the "tool function not found" branch above: roll back the
|
|
3772
|
+
// counter that the post-loop .finally would otherwise decrement.
|
|
3773
|
+
self._activeToolChains -= 1;
|
|
3740
3774
|
run(false);
|
|
3741
3775
|
return;
|
|
3742
3776
|
}
|
|
@@ -3768,6 +3802,12 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3768
3802
|
if (lastStatus === TOOL_ERROR_SYMBOL) {
|
|
3769
3803
|
return lastStatus;
|
|
3770
3804
|
}
|
|
3805
|
+
if (lastStatus === CANCEL_OUTPUT_SYMBOL) {
|
|
3806
|
+
// commitCancelOutput halts the chain exactly like stop/error/change:
|
|
3807
|
+
// without this guard the next tool would still run after the user
|
|
3808
|
+
// cancelled the output.
|
|
3809
|
+
return lastStatus;
|
|
3810
|
+
}
|
|
3771
3811
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3772
3812
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} functionName=${tool.function.name} tool call executing`);
|
|
3773
3813
|
const statusAwaiter = Promise.race([
|
|
@@ -3793,39 +3833,53 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3793
3833
|
if (status === MODEL_RESQUE_SYMBOL) {
|
|
3794
3834
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3795
3835
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} functionName=${tool.function.name} the next tool execution stopped due to the model resque`);
|
|
3796
|
-
self.params.onAfterToolCalls &&
|
|
3797
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3798
3836
|
}
|
|
3799
3837
|
if (status === AGENT_CHANGE_SYMBOL) {
|
|
3800
3838
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3801
3839
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} functionName=${tool.function.name} the next tool execution stopped due to the agent changed`);
|
|
3802
|
-
self.params.onAfterToolCalls &&
|
|
3803
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3804
3840
|
}
|
|
3805
3841
|
if (status === TOOL_STOP_SYMBOL) {
|
|
3806
3842
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3807
3843
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} functionName=${tool.function.name} the next tool execution stopped due to the commitStopTools call`);
|
|
3808
|
-
self.params.onAfterToolCalls &&
|
|
3809
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3810
3844
|
}
|
|
3811
3845
|
if (status === CANCEL_OUTPUT_SYMBOL) {
|
|
3812
3846
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3813
3847
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} functionName=${tool.function.name} the next tool execution stopped due to the commitCancelOutput call`);
|
|
3814
|
-
self.params.onAfterToolCalls &&
|
|
3815
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3816
3848
|
}
|
|
3817
3849
|
if (status === TOOL_ERROR_SYMBOL) {
|
|
3818
3850
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3819
3851
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} functionName=${tool.function.name} the next tool execution stopped due to the call error`);
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3852
|
+
try {
|
|
3853
|
+
const result = await self._resurrectModel(mode, `Function call failed with error: name=${tool.function.name} arguments=${JSON.stringify(tool.function.arguments)}`);
|
|
3854
|
+
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3855
|
+
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${result}`);
|
|
3856
|
+
await self._emitOutput(mode, result, outputEpoch);
|
|
3857
|
+
}
|
|
3858
|
+
catch (recoveryError) {
|
|
3859
|
+
// Nobody awaits the status chain: a throwing recovery
|
|
3860
|
+
// (_emitOutput throws by contract when validation fails twice)
|
|
3861
|
+
// would reject it unhandled. Resolve the waiter directly instead.
|
|
3862
|
+
console.error(`agent-swarm tool error recovery failed functionName=${tool.function.name} error=${getErrorMessage(recoveryError)}`);
|
|
3863
|
+
await errorSubject.next([
|
|
3864
|
+
self.params.clientId,
|
|
3865
|
+
recoveryError,
|
|
3866
|
+
]);
|
|
3867
|
+
if (outputEpoch === self._outputEpoch) {
|
|
3868
|
+
await self._outputSubject.next(createPlaceholder());
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3824
3871
|
}
|
|
3825
3872
|
return status;
|
|
3826
3873
|
});
|
|
3827
3874
|
}
|
|
3828
|
-
lastToolStatusRef
|
|
3875
|
+
lastToolStatusRef
|
|
3876
|
+
.catch((error) => {
|
|
3877
|
+
// The chain is fire-and-forget; a rejected link (e.g. a throwing
|
|
3878
|
+
// params.map/bus adapter) must not surface as an unhandled rejection.
|
|
3879
|
+
console.error(`agent-swarm tool status chain error agentName=${self.params.agentName} clientId=${self.params.clientId} error=${getErrorMessage(error)}`);
|
|
3880
|
+
return null;
|
|
3881
|
+
})
|
|
3882
|
+
.finally(() => {
|
|
3829
3883
|
self._activeToolChains -= 1;
|
|
3830
3884
|
self.params.onAfterToolCalls &&
|
|
3831
3885
|
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
@@ -3837,22 +3891,16 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3837
3891
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3838
3892
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute no tool calls detected`);
|
|
3839
3893
|
}
|
|
3840
|
-
const result = await self.params.transform(message.content, self.params.clientId, self.params.agentName);
|
|
3841
3894
|
await self.params.history.push({
|
|
3842
3895
|
...message,
|
|
3843
3896
|
agentName: self.params.agentName,
|
|
3844
3897
|
});
|
|
3845
|
-
let validation = null;
|
|
3846
|
-
if ((validation = await self.params.validate(result))) {
|
|
3847
|
-
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3848
|
-
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute invalid tool call detected: ${validation}`, { result });
|
|
3849
|
-
const result1 = await self._resurrectModel(mode, `Invalid model output: ${result}`);
|
|
3850
|
-
await self._emitOutput(mode, result1, outputEpoch);
|
|
3851
|
-
return;
|
|
3852
|
-
}
|
|
3853
3898
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3854
|
-
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${
|
|
3855
|
-
|
|
3899
|
+
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${message.content}`);
|
|
3900
|
+
// _emitOutput owns transform + validate (with resurrect on failure), so the
|
|
3901
|
+
// raw content goes straight through — transforming/validating here as well
|
|
3902
|
+
// ran every transform twice per answer and broke non-idempotent transforms.
|
|
3903
|
+
await self._emitOutput(mode, message.content, outputEpoch);
|
|
3856
3904
|
};
|
|
3857
3905
|
/**
|
|
3858
3906
|
* Represents a client-side agent in the swarm system, implementing the IAgent interface.
|
|
@@ -3945,9 +3993,30 @@ class ClientAgent {
|
|
|
3945
3993
|
*/
|
|
3946
3994
|
this.execute = queued(async (incoming, mode) => {
|
|
3947
3995
|
this._activeExecutions += 1;
|
|
3996
|
+
const outputEpoch = this._outputEpoch;
|
|
3948
3997
|
try {
|
|
3949
3998
|
return await EXECUTE_FN(incoming, mode, this);
|
|
3950
3999
|
}
|
|
4000
|
+
catch (error) {
|
|
4001
|
+
// EXECUTE_FN is fired without await from ClientSession.execute and
|
|
4002
|
+
// queued() rejects internally: an unguarded rejection (e.g. a throwing
|
|
4003
|
+
// completion in getCompletion) crashes the host process and leaves the
|
|
4004
|
+
// pending waitForOutput hanging forever. Recover with the resque flow.
|
|
4005
|
+
console.error(`agent-swarm execute error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
4006
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4007
|
+
try {
|
|
4008
|
+
const result = await this._resurrectModel(mode, `Execution failed: ${getErrorMessage(error)}`);
|
|
4009
|
+
await this._emitOutput(mode, result, outputEpoch);
|
|
4010
|
+
}
|
|
4011
|
+
catch (recoveryError) {
|
|
4012
|
+
// Even the recovery failed (e.g. recomplete strategy hitting the same
|
|
4013
|
+
// broken completion): resolve the waiter directly so nothing hangs.
|
|
4014
|
+
console.error(`agent-swarm execute recovery error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(recoveryError)}`);
|
|
4015
|
+
if (outputEpoch === this._outputEpoch) {
|
|
4016
|
+
await this._outputSubject.next(createPlaceholder());
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
3951
4020
|
finally {
|
|
3952
4021
|
this._activeExecutions -= 1;
|
|
3953
4022
|
}
|
|
@@ -3956,7 +4025,19 @@ class ClientAgent {
|
|
|
3956
4025
|
* Runs a stateless completion for the incoming message, queued to prevent overlapping executions.
|
|
3957
4026
|
* Implements IAgent.run, delegating to RUN_FN with queuing via functools-kit’s queued decorator.
|
|
3958
4027
|
*/
|
|
3959
|
-
this.run = queued(async (incoming) =>
|
|
4028
|
+
this.run = queued(async (incoming) => {
|
|
4029
|
+
try {
|
|
4030
|
+
return await RUN_FN(incoming, this);
|
|
4031
|
+
}
|
|
4032
|
+
catch (error) {
|
|
4033
|
+
// queued() rejects internally, so a throwing completion would raise an
|
|
4034
|
+
// unhandled rejection besides failing the caller: degrade to an empty
|
|
4035
|
+
// stateless result and surface the error through errorSubject.
|
|
4036
|
+
console.error(`agent-swarm run error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
4037
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4038
|
+
return "";
|
|
4039
|
+
}
|
|
4040
|
+
});
|
|
3960
4041
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3961
4042
|
this.params.logger.debug(`ClientAgent agentName=${this.params.agentName} clientId=${this.params.clientId} CTOR`, {
|
|
3962
4043
|
params,
|
|
@@ -3973,11 +4054,15 @@ class ClientAgent {
|
|
|
3973
4054
|
const seen = new Set();
|
|
3974
4055
|
const agentToolList = [];
|
|
3975
4056
|
if (this.params.tools) {
|
|
3976
|
-
|
|
4057
|
+
// Resolve availability/dynamic schemas concurrently, but keep the RESULT
|
|
4058
|
+
// in declaration order: pushing from within the concurrent map would order
|
|
4059
|
+
// tools by resolution time, making the tool list, the seen-based dedup and
|
|
4060
|
+
// the single-commit-action filter all non-deterministic across runs.
|
|
4061
|
+
const resolvedTools = await Promise.all(this.params.tools.map(async (tool) => {
|
|
3977
4062
|
const { toolName, function: upperFn, isAvailable = TOOL_AVAILABLE_DEFAULT, ...other } = tool;
|
|
3978
4063
|
try {
|
|
3979
4064
|
if (await not(isAvailable(this.params.clientId, this.params.agentName, toolName))) {
|
|
3980
|
-
return;
|
|
4065
|
+
return null;
|
|
3981
4066
|
}
|
|
3982
4067
|
}
|
|
3983
4068
|
catch (error) {
|
|
@@ -3985,7 +4070,7 @@ class ClientAgent {
|
|
|
3985
4070
|
// hang the pending waitForOutput: treat it as "not available".
|
|
3986
4071
|
console.error(`agent-swarm isAvailable error toolName=${toolName} error=${getErrorMessage(error)}`);
|
|
3987
4072
|
await errorSubject.next([this.params.clientId, error]);
|
|
3988
|
-
return;
|
|
4073
|
+
return null;
|
|
3989
4074
|
}
|
|
3990
4075
|
let fn;
|
|
3991
4076
|
try {
|
|
@@ -3999,14 +4084,19 @@ class ClientAgent {
|
|
|
3999
4084
|
// the execution — the tool is skipped for this completion.
|
|
4000
4085
|
console.error(`agent-swarm dynamic tool function error toolName=${toolName} error=${getErrorMessage(error)}`);
|
|
4001
4086
|
await errorSubject.next([this.params.clientId, error]);
|
|
4002
|
-
return;
|
|
4087
|
+
return null;
|
|
4003
4088
|
}
|
|
4004
|
-
|
|
4089
|
+
return {
|
|
4005
4090
|
...other,
|
|
4006
4091
|
toolName,
|
|
4007
4092
|
function: fn,
|
|
4008
|
-
}
|
|
4093
|
+
};
|
|
4009
4094
|
}));
|
|
4095
|
+
for (const tool of resolvedTools) {
|
|
4096
|
+
if (tool) {
|
|
4097
|
+
agentToolList.push(tool);
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4010
4100
|
}
|
|
4011
4101
|
let mcpToolList = [];
|
|
4012
4102
|
try {
|
|
@@ -4155,6 +4245,10 @@ class ClientAgent {
|
|
|
4155
4245
|
}
|
|
4156
4246
|
this.params.onOutput &&
|
|
4157
4247
|
this.params.onOutput(this.params.clientId, this.params.agentName, result);
|
|
4248
|
+
// Keep the callback surface symmetric with the resurrect branch above:
|
|
4249
|
+
// the emitted output is an assistant message on both paths.
|
|
4250
|
+
this.params.onAssistantMessage &&
|
|
4251
|
+
this.params.onAssistantMessage(this.params.clientId, this.params.agentName, result);
|
|
4158
4252
|
await this._outputSubject.next(result);
|
|
4159
4253
|
await this.params.bus.emit(this.params.clientId, {
|
|
4160
4254
|
type: "emit-output",
|
|
@@ -4209,7 +4303,21 @@ class ClientAgent {
|
|
|
4209
4303
|
await this._resqueSubject.next(MODEL_RESQUE_SYMBOL);
|
|
4210
4304
|
return placeholder;
|
|
4211
4305
|
}
|
|
4212
|
-
|
|
4306
|
+
let rawMessage;
|
|
4307
|
+
try {
|
|
4308
|
+
rawMessage = await this.getCompletion(mode, []);
|
|
4309
|
+
}
|
|
4310
|
+
catch (error) {
|
|
4311
|
+
// _resurrectModel IS the recovery path: if the resque completion itself
|
|
4312
|
+
// rejects (e.g. the provider is down — often the very reason we are
|
|
4313
|
+
// here), rethrowing would surface as an unhandled rejection from the
|
|
4314
|
+
// fire-and-forget callers (createToolCall, tool status chain). Degrade
|
|
4315
|
+
// to the placeholder instead.
|
|
4316
|
+
console.error(`agent-swarm _resurrectModel completion error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
4317
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4318
|
+
await this._resqueSubject.next(MODEL_RESQUE_SYMBOL);
|
|
4319
|
+
return placeholder;
|
|
4320
|
+
}
|
|
4213
4321
|
if (rawMessage.tool_calls?.length) {
|
|
4214
4322
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4215
4323
|
this.params.logger.debug(`ClientAgent agentName=${this.params.agentName} clientId=${this.params.clientId} _resurrectModel should not emit tool_calls`);
|
|
@@ -4698,6 +4806,21 @@ class OperatorSignal {
|
|
|
4698
4806
|
sendMessage(message, next) {
|
|
4699
4807
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4700
4808
|
this.params.logger.debug(`OperatorSignal agentName=${this.params.agentName} clientId=${this.params.clientId} sendMessage`, { message });
|
|
4809
|
+
// Dispose the previous signal before opening a new one: overwriting the
|
|
4810
|
+
// ref leaks the pending signal, and its late answer would resolve the
|
|
4811
|
+
// NEXT waitForOutput with a reply to the previous question.
|
|
4812
|
+
if (this._disposeRef) {
|
|
4813
|
+
const disposeRef = this._disposeRef;
|
|
4814
|
+
this._disposeRef = null;
|
|
4815
|
+
try {
|
|
4816
|
+
disposeRef();
|
|
4817
|
+
}
|
|
4818
|
+
catch (error) {
|
|
4819
|
+
// A throwing user dispose must not prevent the next signal from
|
|
4820
|
+
// opening — the previous one is torn down on a best-effort basis.
|
|
4821
|
+
console.error(`agent-swarm operator signal dispose error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4701
4824
|
this._disposeRef = this._signalFactory(message, next);
|
|
4702
4825
|
}
|
|
4703
4826
|
/**
|
|
@@ -4709,7 +4832,14 @@ class OperatorSignal {
|
|
|
4709
4832
|
if (this._disposeRef) {
|
|
4710
4833
|
const disposeRef = this._disposeRef;
|
|
4711
4834
|
this._disposeRef = null;
|
|
4712
|
-
|
|
4835
|
+
try {
|
|
4836
|
+
await disposeRef();
|
|
4837
|
+
}
|
|
4838
|
+
catch (error) {
|
|
4839
|
+
// dispose is awaited from waitForOutput's timeout path: a throwing
|
|
4840
|
+
// user dispose would reject waitForOutput and hang the swarm race.
|
|
4841
|
+
console.error(`agent-swarm operator signal dispose error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
4842
|
+
}
|
|
4713
4843
|
}
|
|
4714
4844
|
}
|
|
4715
4845
|
}
|
|
@@ -4752,7 +4882,17 @@ class ClientOperator {
|
|
|
4752
4882
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4753
4883
|
this.params.logger.debug(`ClientOperator agentName=${this.params.agentName} clientId=${this.params.clientId} execute tool mode forwarded to operator`);
|
|
4754
4884
|
}
|
|
4755
|
-
|
|
4885
|
+
try {
|
|
4886
|
+
this._operatorSignal.sendMessage(input, this._outgoingSubject.next);
|
|
4887
|
+
}
|
|
4888
|
+
catch (error) {
|
|
4889
|
+
// execute is fired without await from ClientSession.execute: a throwing
|
|
4890
|
+
// operator connector would surface as an unhandled rejection and crash
|
|
4891
|
+
// the host process. Surface the error instead; the pending waitForOutput
|
|
4892
|
+
// resolves through the operator timeout.
|
|
4893
|
+
console.error(`agent-swarm operator connector error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
4894
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4895
|
+
}
|
|
4756
4896
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4757
4897
|
this.params.logger.debug(`ClientOperator agentName=${this.params.agentName} clientId=${this.params.clientId} execute end`, { input, mode });
|
|
4758
4898
|
}
|
|
@@ -5830,6 +5970,8 @@ class ClientHistory {
|
|
|
5830
5970
|
// the error through errorSubject so the caller's complete() rejects.
|
|
5831
5971
|
console.error(`agent-swarm history push error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
5832
5972
|
await errorSubject.next([this.params.clientId, error]);
|
|
5973
|
+
// The record was dropped: do not announce a successful push on the bus.
|
|
5974
|
+
return;
|
|
5833
5975
|
}
|
|
5834
5976
|
await this.params.bus.emit(this.params.clientId, {
|
|
5835
5977
|
type: "push",
|
|
@@ -6288,6 +6430,7 @@ class NoopAgent {
|
|
|
6288
6430
|
const message = `called commitCancelOutput on agent which not in the swarm clientId=${this.clientId} agentName=${this.agentName} swarmName=${this.swarmName}`;
|
|
6289
6431
|
this.logger.log(message);
|
|
6290
6432
|
console.error(message);
|
|
6433
|
+
return await this.defaultAgent.commitCancelOutput();
|
|
6291
6434
|
}
|
|
6292
6435
|
/**
|
|
6293
6436
|
* Logs an attempt to run the missing agent and delegates to the default agent's run method.
|
|
@@ -6484,7 +6627,10 @@ class ClientSwarm {
|
|
|
6484
6627
|
setBusy(isBusy) {
|
|
6485
6628
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
6486
6629
|
this.params.logger.debug(`ClientSwarm swarmName=${this.params.swarmName} clientId=${this.params.clientId} setBusy`, { isBusy });
|
|
6487
|
-
|
|
6630
|
+
// Clamp at zero: an unmatched setBusy(false) (reachable through the public
|
|
6631
|
+
// SwarmPublicService.setBusy) would drive the counter negative, and a
|
|
6632
|
+
// negative counter reads as "busy" forever — deadlocking the session lock.
|
|
6633
|
+
this._isBusy = Math.max(0, this._isBusy + (isBusy ? 1 : -1));
|
|
6488
6634
|
}
|
|
6489
6635
|
/**
|
|
6490
6636
|
* Getter for the busy state of the swarm.
|
|
@@ -6715,7 +6861,14 @@ class ClientSwarm {
|
|
|
6715
6861
|
});
|
|
6716
6862
|
if (!result) {
|
|
6717
6863
|
console.error(`agent-swarm ClientSwarm getAgent current agent is not in the swarm agentName=${agentName} clientId=${this.params.clientId} swarmName=${this.params.swarmName}`);
|
|
6718
|
-
|
|
6864
|
+
const defaultAgent = this.params.agentMap[this.params.defaultAgent];
|
|
6865
|
+
if (!defaultAgent) {
|
|
6866
|
+
// NoopAgent delegates every call to the default agent: with that one
|
|
6867
|
+
// missing too the delegation would die with a bare TypeError instead
|
|
6868
|
+
// of an actionable message.
|
|
6869
|
+
throw new Error(`agent-swarm ClientSwarm getAgent default agent is not in the swarm either agentName=${agentName} defaultAgent=${this.params.defaultAgent} clientId=${this.params.clientId} swarmName=${this.params.swarmName}`);
|
|
6870
|
+
}
|
|
6871
|
+
return new NoopAgent(this.params.clientId, this.params.swarmName, agentName, defaultAgent, this.params.logger);
|
|
6719
6872
|
}
|
|
6720
6873
|
return result;
|
|
6721
6874
|
}
|
|
@@ -10862,15 +11015,49 @@ class ClientStorage {
|
|
|
10862
11015
|
*/
|
|
10863
11016
|
this._itemMap = new Map();
|
|
10864
11017
|
/**
|
|
10865
|
-
*
|
|
10866
|
-
*
|
|
11018
|
+
* Queued core of dispatch. The wrapped function must never reject: queued()
|
|
11019
|
+
* chains every call on the previous promise, so a rejection inside the queue
|
|
11020
|
+
* (e.g. a throwing createIndex/createEmbedding/setData) would reject every
|
|
11021
|
+
* already-queued dispatch with this foreign error WITHOUT running it — a
|
|
11022
|
+
* concurrent upsert/remove/clear would be silently dropped. Errors are boxed
|
|
11023
|
+
* here and rethrown outside the queue, so only the failing caller observes them.
|
|
10867
11024
|
*/
|
|
10868
|
-
this.
|
|
11025
|
+
this._dispatchQueue = queued(async (action, payload) => {
|
|
11026
|
+
try {
|
|
11027
|
+
await DISPATCH_FN$1(action, this, payload);
|
|
11028
|
+
return null;
|
|
11029
|
+
}
|
|
11030
|
+
catch (error) {
|
|
11031
|
+
return { error };
|
|
11032
|
+
}
|
|
11033
|
+
});
|
|
11034
|
+
/**
|
|
11035
|
+
* Dispatches a storage action (upsert, remove, or clear) through the serialized
|
|
11036
|
+
* queue, delegating to DISPATCH_FN. Ensures sequential execution of storage
|
|
11037
|
+
* operations, supporting thread-safe updates from ClientAgent or tools.
|
|
11038
|
+
*/
|
|
11039
|
+
this.dispatch = async (action, payload) => {
|
|
11040
|
+
const result = await this._dispatchQueue(action, payload);
|
|
11041
|
+
if (result) {
|
|
11042
|
+
throw result.error;
|
|
11043
|
+
}
|
|
11044
|
+
};
|
|
10869
11045
|
/**
|
|
10870
11046
|
* Creates embeddings for the given item, memoized by item ID to avoid redundant calculations via CREATE_EMBEDDING_FN.
|
|
10871
11047
|
* Caches results for efficiency, cleared on upsert/remove to ensure freshness, supporting EmbeddingSchemaService.
|
|
10872
11048
|
*/
|
|
10873
|
-
this._createEmbedding = memoize(([{ id }]) => id, async (item) =>
|
|
11049
|
+
this._createEmbedding = memoize(([{ id }]) => id, async (item) => {
|
|
11050
|
+
try {
|
|
11051
|
+
return await CREATE_EMBEDDING_FN(item, this);
|
|
11052
|
+
}
|
|
11053
|
+
catch (error) {
|
|
11054
|
+
// Never cache a rejected embedding: the memoized promise would keep
|
|
11055
|
+
// this item permanently broken (every take re-throwing the same
|
|
11056
|
+
// error) even after the embedding provider recovers.
|
|
11057
|
+
this._createEmbedding.clear(item.id);
|
|
11058
|
+
throw error;
|
|
11059
|
+
}
|
|
11060
|
+
});
|
|
10874
11061
|
/**
|
|
10875
11062
|
* Waits for the initialization of the storage, loading initial data and creating embeddings via WAIT_FOR_INIT_FN.
|
|
10876
11063
|
* Ensures initialization happens only once using singleshot, supporting StorageConnectionService’s lifecycle.
|
|
@@ -11750,6 +11937,13 @@ const WAIT_FOR_INIT_FN = async (self) => {
|
|
|
11750
11937
|
* SwarmConnectionService (swarm-level state), and BusService (event emission).
|
|
11751
11938
|
*/
|
|
11752
11939
|
class ClientState {
|
|
11940
|
+
/**
|
|
11941
|
+
* True only while the caller runs inside the async context of an active
|
|
11942
|
+
* dispatch on this instance (i.e. a reentrant call from within a dispatchFn).
|
|
11943
|
+
*/
|
|
11944
|
+
get _inDispatch() {
|
|
11945
|
+
return this._dispatchContext.getStore() === true;
|
|
11946
|
+
}
|
|
11753
11947
|
/**
|
|
11754
11948
|
* Constructs a ClientState instance with the provided parameters.
|
|
11755
11949
|
* Invokes the onInit callback if provided and logs construction if debugging is enabled.
|
|
@@ -11758,29 +11952,50 @@ class ClientState {
|
|
|
11758
11952
|
this.params = params;
|
|
11759
11953
|
this.stateChanged = new Subject();
|
|
11760
11954
|
/**
|
|
11761
|
-
*
|
|
11762
|
-
* getState checks it to serve reentrant reads (getState
|
|
11763
|
-
* dispatchFn) without re-entering the queue, which would
|
|
11955
|
+
* Marks the async execution context of a running dispatch (read/write).
|
|
11956
|
+
* getState checks it to serve reentrant reads (getState called from INSIDE a
|
|
11957
|
+
* setState dispatchFn/middleware) without re-entering the queue, which would
|
|
11958
|
+
* deadlock. Scoping this per async-context — instead of a plain instance flag —
|
|
11959
|
+
* ensures an UNRELATED concurrent getState still queues and observes writes in
|
|
11960
|
+
* order, rather than reading a stale field while some other write is in flight.
|
|
11764
11961
|
*/
|
|
11765
|
-
this.
|
|
11962
|
+
this._dispatchContext = new AsyncLocalStorage();
|
|
11766
11963
|
/**
|
|
11767
11964
|
* The current state data, initialized as null and set during waitForInit.
|
|
11768
11965
|
* Updated by setState and clearState, persisted via params.setState if provided.
|
|
11769
11966
|
*/
|
|
11770
11967
|
this._state = null;
|
|
11771
11968
|
/**
|
|
11772
|
-
* Queued
|
|
11773
|
-
*
|
|
11969
|
+
* Queued core of dispatch. The wrapped function must never reject: queued()
|
|
11970
|
+
* chains every call on the previous promise, so a rejection inside the queue
|
|
11971
|
+
* would reject every already-queued dispatch with this foreign error WITHOUT
|
|
11972
|
+
* running it — a concurrent setState would be silently dropped. Errors (e.g.
|
|
11973
|
+
* a throwing user dispatchFn or middleware) are boxed here and rethrown
|
|
11974
|
+
* outside the queue, so only the caller whose operation failed observes them.
|
|
11774
11975
|
*/
|
|
11775
|
-
this.
|
|
11776
|
-
this._inDispatch = true;
|
|
11976
|
+
this._dispatchQueue = queued(async (action, payload) => {
|
|
11777
11977
|
try {
|
|
11778
|
-
return
|
|
11978
|
+
return {
|
|
11979
|
+
ok: true,
|
|
11980
|
+
state: await this._dispatchContext.run(true, async () => await DISPATCH_FN(action, this, payload)),
|
|
11981
|
+
};
|
|
11779
11982
|
}
|
|
11780
|
-
|
|
11781
|
-
|
|
11983
|
+
catch (error) {
|
|
11984
|
+
return { ok: false, error };
|
|
11782
11985
|
}
|
|
11783
11986
|
});
|
|
11987
|
+
/**
|
|
11988
|
+
* Dispatches a read or write of the state through the serialized queue,
|
|
11989
|
+
* delegating to DISPATCH_FN. Ensures thread-safe state operations, supporting
|
|
11990
|
+
* concurrent access from ClientAgent or tools.
|
|
11991
|
+
*/
|
|
11992
|
+
this.dispatch = async (action, payload) => {
|
|
11993
|
+
const result = await this._dispatchQueue(action, payload);
|
|
11994
|
+
if ("error" in result) {
|
|
11995
|
+
throw result.error;
|
|
11996
|
+
}
|
|
11997
|
+
return result.state;
|
|
11998
|
+
};
|
|
11784
11999
|
/**
|
|
11785
12000
|
* Waits for the state to initialize via WAIT_FOR_INIT_FN, ensuring it’s only called once using singleshot.
|
|
11786
12001
|
* Loads the initial state into _state, supporting StateConnectionService’s lifecycle management.
|
|
@@ -11810,8 +12025,18 @@ class ClientState {
|
|
|
11810
12025
|
});
|
|
11811
12026
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
11812
12027
|
this.params.logger.debug(`ClientState stateName=${this.params.stateName} clientId=${this.params.clientId} shared=${this.params.shared} setState output`, { pendingState: this._state });
|
|
11813
|
-
this.params.setState
|
|
11814
|
-
|
|
12028
|
+
if (this.params.setState) {
|
|
12029
|
+
try {
|
|
12030
|
+
await this.params.setState(this._state, this.params.clientId, this.params.stateName);
|
|
12031
|
+
}
|
|
12032
|
+
catch (error) {
|
|
12033
|
+
// Persistence was fired without await before: a rejecting adapter
|
|
12034
|
+
// raised an unhandled rejection. Keep the in-memory state and surface
|
|
12035
|
+
// the error to the caller through errorSubject instead.
|
|
12036
|
+
console.error(`agent-swarm state persist error stateName=${this.params.stateName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
12037
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
12038
|
+
}
|
|
12039
|
+
}
|
|
11815
12040
|
if (this.params.callbacks?.onWrite) {
|
|
11816
12041
|
this.params.callbacks.onWrite(this._state, this.params.clientId, this.params.stateName);
|
|
11817
12042
|
}
|
|
@@ -11843,8 +12068,17 @@ class ClientState {
|
|
|
11843
12068
|
});
|
|
11844
12069
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
11845
12070
|
this.params.logger.debug(`ClientState stateName=${this.params.stateName} clientId=${this.params.clientId} shared=${this.params.shared} clearState output`, { pendingState: this._state });
|
|
11846
|
-
this.params.setState
|
|
11847
|
-
|
|
12071
|
+
if (this.params.setState) {
|
|
12072
|
+
try {
|
|
12073
|
+
await this.params.setState(this._state, this.params.clientId, this.params.stateName);
|
|
12074
|
+
}
|
|
12075
|
+
catch (error) {
|
|
12076
|
+
// See setState above: a rejecting persistence adapter must not raise
|
|
12077
|
+
// an unhandled rejection.
|
|
12078
|
+
console.error(`agent-swarm state persist error stateName=${this.params.stateName} clientId=${this.params.clientId} error=${getErrorMessage(error)}`);
|
|
12079
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
12080
|
+
}
|
|
12081
|
+
}
|
|
11848
12082
|
if (this.params.callbacks?.onWrite) {
|
|
11849
12083
|
this.params.callbacks.onWrite(this._state, this.params.clientId, this.params.stateName);
|
|
11850
12084
|
}
|
|
@@ -14909,7 +15143,6 @@ class PolicyPublicService {
|
|
|
14909
15143
|
}
|
|
14910
15144
|
}
|
|
14911
15145
|
|
|
14912
|
-
const BAN_NEED_FETCH = Symbol("ban-need-fetch");
|
|
14913
15146
|
/**
|
|
14914
15147
|
* Class representing a client policy in the swarm system, implementing the IPolicy interface.
|
|
14915
15148
|
* Manages client bans, input/output validation, and restrictions, with lazy-loaded ban lists and event emission via BusService.
|
|
@@ -14918,6 +15151,32 @@ const BAN_NEED_FETCH = Symbol("ban-need-fetch");
|
|
|
14918
15151
|
* Supports auto-banning on validation failure and customizable ban messages, ensuring swarm security and compliance.
|
|
14919
15152
|
*/
|
|
14920
15153
|
class ClientPolicy {
|
|
15154
|
+
/**
|
|
15155
|
+
* Returns the ban set of the given swarm, fetching it on first access.
|
|
15156
|
+
* Re-checks the map after the await: a ban committed through _banQueue while
|
|
15157
|
+
* the fetch was in flight must not be overwritten by the stale fetch result.
|
|
15158
|
+
*/
|
|
15159
|
+
async _getBanSet(swarmName) {
|
|
15160
|
+
const existing = this._banSetBySwarm.get(swarmName);
|
|
15161
|
+
if (existing) {
|
|
15162
|
+
return existing;
|
|
15163
|
+
}
|
|
15164
|
+
const fetched = new Set(await this.params.getBannedClients(this.params.policyName, swarmName));
|
|
15165
|
+
if (!this._banSetBySwarm.has(swarmName)) {
|
|
15166
|
+
this._banSetBySwarm.set(swarmName, fetched);
|
|
15167
|
+
}
|
|
15168
|
+
return this._banSetBySwarm.get(swarmName);
|
|
15169
|
+
}
|
|
15170
|
+
/**
|
|
15171
|
+
* Runs a ban-set mutation through _banQueue, rethrowing its boxed error
|
|
15172
|
+
* outside the queue so only the failing caller observes it.
|
|
15173
|
+
*/
|
|
15174
|
+
async _runBanQueue(fn) {
|
|
15175
|
+
const result = await this._banQueue(fn);
|
|
15176
|
+
if (result) {
|
|
15177
|
+
throw result.error;
|
|
15178
|
+
}
|
|
15179
|
+
}
|
|
14921
15180
|
/**
|
|
14922
15181
|
* Constructs a ClientPolicy instance with the provided parameters.
|
|
14923
15182
|
* Invokes the onInit callback if defined and logs construction if debugging is enabled.
|
|
@@ -14925,11 +15184,13 @@ class ClientPolicy {
|
|
|
14925
15184
|
constructor(params) {
|
|
14926
15185
|
this.params = params;
|
|
14927
15186
|
/**
|
|
14928
|
-
*
|
|
14929
|
-
*
|
|
14930
|
-
*
|
|
15187
|
+
* Ban sets keyed by swarm name, lazily populated via params.getBannedClients.
|
|
15188
|
+
* A ClientPolicy instance is memoized per policyName and shared by every swarm
|
|
15189
|
+
* that lists the policy, while bans are persisted per (policy, swarm) — a
|
|
15190
|
+
* single shared set would leak bans of one swarm into another and persist
|
|
15191
|
+
* the mixed set into the wrong store.
|
|
14931
15192
|
*/
|
|
14932
|
-
this.
|
|
15193
|
+
this._banSetBySwarm = new Map();
|
|
14933
15194
|
/**
|
|
14934
15195
|
* Serializes the read-modify-write of _banSet shared by banClient/unbanClient.
|
|
14935
15196
|
* Without it two concurrent bans of different clients both read the same ban
|
|
@@ -14937,8 +15198,23 @@ class ClientPolicy {
|
|
|
14937
15198
|
* the earlier one — one ban is silently lost in memory and in the persisted
|
|
14938
15199
|
* store. queued() runs the mutations one at a time so each observes the result
|
|
14939
15200
|
* of the previous one.
|
|
14940
|
-
|
|
14941
|
-
|
|
15201
|
+
*
|
|
15202
|
+
* The wrapped function must never reject: queued() chains every call on the
|
|
15203
|
+
* previous promise, so a rejection inside the queue (e.g. a throwing
|
|
15204
|
+
* setBannedClients adapter) would reject every already-queued ban/unban with
|
|
15205
|
+
* this foreign error WITHOUT running it — a concurrent ban of another client
|
|
15206
|
+
* would be silently lost. Errors are boxed here and rethrown by the caller
|
|
15207
|
+
* outside the queue.
|
|
15208
|
+
*/
|
|
15209
|
+
this._banQueue = queued(async (fn) => {
|
|
15210
|
+
try {
|
|
15211
|
+
await fn();
|
|
15212
|
+
return null;
|
|
15213
|
+
}
|
|
15214
|
+
catch (error) {
|
|
15215
|
+
return { error };
|
|
15216
|
+
}
|
|
15217
|
+
});
|
|
14942
15218
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
14943
15219
|
this.params.logger.debug(`ClientPolicy policyName=${this.params.policyName} CTOR`, {
|
|
14944
15220
|
params,
|
|
@@ -14957,10 +15233,8 @@ class ClientPolicy {
|
|
|
14957
15233
|
clientId,
|
|
14958
15234
|
swarmName,
|
|
14959
15235
|
});
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
}
|
|
14963
|
-
return this._banSet.has(clientId);
|
|
15236
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15237
|
+
return banSet.has(clientId);
|
|
14964
15238
|
}
|
|
14965
15239
|
/**
|
|
14966
15240
|
* Retrieves the ban message for a client, using a custom getBanMessage function if provided or falling back to params.banMessage.
|
|
@@ -15009,10 +15283,8 @@ class ClientPolicy {
|
|
|
15009
15283
|
},
|
|
15010
15284
|
clientId,
|
|
15011
15285
|
});
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
}
|
|
15015
|
-
if (this._banSet.has(clientId)) {
|
|
15286
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15287
|
+
if (banSet.has(clientId)) {
|
|
15016
15288
|
return false;
|
|
15017
15289
|
}
|
|
15018
15290
|
if (!this.params.validateInput) {
|
|
@@ -15054,10 +15326,8 @@ class ClientPolicy {
|
|
|
15054
15326
|
},
|
|
15055
15327
|
clientId,
|
|
15056
15328
|
});
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
}
|
|
15060
|
-
if (this._banSet.has(clientId)) {
|
|
15329
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15330
|
+
if (banSet.has(clientId)) {
|
|
15061
15331
|
return false;
|
|
15062
15332
|
}
|
|
15063
15333
|
if (!this.params.validateOutput) {
|
|
@@ -15082,31 +15352,32 @@ class ClientPolicy {
|
|
|
15082
15352
|
clientId,
|
|
15083
15353
|
swarmName,
|
|
15084
15354
|
});
|
|
15085
|
-
|
|
15086
|
-
this.
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
input: {},
|
|
15092
|
-
output: {},
|
|
15093
|
-
context: {
|
|
15094
|
-
policyName: this.params.policyName,
|
|
15095
|
-
swarmName,
|
|
15096
|
-
},
|
|
15097
|
-
clientId,
|
|
15098
|
-
});
|
|
15099
|
-
await this._banQueue(async () => {
|
|
15100
|
-
if (this._banSet === BAN_NEED_FETCH) {
|
|
15101
|
-
this._banSet = new Set(await this.params.getBannedClients(this.params.policyName, swarmName));
|
|
15102
|
-
}
|
|
15103
|
-
if (this._banSet.has(clientId)) {
|
|
15355
|
+
await this._runBanQueue(async () => {
|
|
15356
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15357
|
+
if (banSet.has(clientId)) {
|
|
15358
|
+
// Already banned: skip the mutation AND the notifications, matching
|
|
15359
|
+
// the documented "skips if already banned" contract — otherwise every
|
|
15360
|
+
// redundant ban re-fires onBanClient and the bus event.
|
|
15104
15361
|
return;
|
|
15105
15362
|
}
|
|
15106
|
-
this.
|
|
15363
|
+
this._banSetBySwarm.set(swarmName, new Set(banSet).add(clientId));
|
|
15107
15364
|
if (this.params.setBannedClients) {
|
|
15108
|
-
await this.params.setBannedClients([...this.
|
|
15365
|
+
await this.params.setBannedClients([...this._banSetBySwarm.get(swarmName)], this.params.policyName, swarmName);
|
|
15366
|
+
}
|
|
15367
|
+
if (this.params.callbacks?.onBanClient) {
|
|
15368
|
+
this.params.callbacks.onBanClient(clientId, swarmName, this.params.policyName);
|
|
15109
15369
|
}
|
|
15370
|
+
await this.params.bus.emit(clientId, {
|
|
15371
|
+
type: "ban-client",
|
|
15372
|
+
source: "policy-bus",
|
|
15373
|
+
input: {},
|
|
15374
|
+
output: {},
|
|
15375
|
+
context: {
|
|
15376
|
+
policyName: this.params.policyName,
|
|
15377
|
+
swarmName,
|
|
15378
|
+
},
|
|
15379
|
+
clientId,
|
|
15380
|
+
});
|
|
15110
15381
|
});
|
|
15111
15382
|
}
|
|
15112
15383
|
/**
|
|
@@ -15120,35 +15391,35 @@ class ClientPolicy {
|
|
|
15120
15391
|
clientId,
|
|
15121
15392
|
swarmName,
|
|
15122
15393
|
});
|
|
15123
|
-
|
|
15124
|
-
this.
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
source: "policy-bus",
|
|
15129
|
-
input: {},
|
|
15130
|
-
output: {},
|
|
15131
|
-
context: {
|
|
15132
|
-
policyName: this.params.policyName,
|
|
15133
|
-
swarmName,
|
|
15134
|
-
},
|
|
15135
|
-
clientId,
|
|
15136
|
-
});
|
|
15137
|
-
await this._banQueue(async () => {
|
|
15138
|
-
if (this._banSet === BAN_NEED_FETCH) {
|
|
15139
|
-
this._banSet = new Set(await this.params.getBannedClients(this.params.policyName, swarmName));
|
|
15140
|
-
}
|
|
15141
|
-
if (!this._banSet.has(clientId)) {
|
|
15394
|
+
await this._runBanQueue(async () => {
|
|
15395
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15396
|
+
if (!banSet.has(clientId)) {
|
|
15397
|
+
// Not banned: skip the mutation and the notifications, matching the
|
|
15398
|
+
// documented "skips if not banned" contract.
|
|
15142
15399
|
return;
|
|
15143
15400
|
}
|
|
15144
15401
|
{
|
|
15145
|
-
const
|
|
15146
|
-
|
|
15147
|
-
this.
|
|
15402
|
+
const nextBanSet = new Set(banSet);
|
|
15403
|
+
nextBanSet.delete(clientId);
|
|
15404
|
+
this._banSetBySwarm.set(swarmName, nextBanSet);
|
|
15148
15405
|
}
|
|
15149
15406
|
if (this.params.setBannedClients) {
|
|
15150
|
-
await this.params.setBannedClients([...this.
|
|
15407
|
+
await this.params.setBannedClients([...this._banSetBySwarm.get(swarmName)], this.params.policyName, swarmName);
|
|
15408
|
+
}
|
|
15409
|
+
if (this.params.callbacks?.onUnbanClient) {
|
|
15410
|
+
this.params.callbacks.onUnbanClient(clientId, swarmName, this.params.policyName);
|
|
15151
15411
|
}
|
|
15412
|
+
await this.params.bus.emit(clientId, {
|
|
15413
|
+
type: "unban-client",
|
|
15414
|
+
source: "policy-bus",
|
|
15415
|
+
input: {},
|
|
15416
|
+
output: {},
|
|
15417
|
+
context: {
|
|
15418
|
+
policyName: this.params.policyName,
|
|
15419
|
+
swarmName,
|
|
15420
|
+
},
|
|
15421
|
+
clientId,
|
|
15422
|
+
});
|
|
15152
15423
|
});
|
|
15153
15424
|
}
|
|
15154
15425
|
}
|