agent-swarm-kit 4.0.0 → 5.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/build/index.cjs +397 -126
- package/build/index.mjs +397 -126
- package/package.json +1 -1
- package/types.d.ts +60 -13
package/build/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var fs = require('fs/promises');
|
|
|
8
8
|
var path = require('path');
|
|
9
9
|
var crypto = require('crypto');
|
|
10
10
|
var os = require('os');
|
|
11
|
+
var async_hooks = require('async_hooks');
|
|
11
12
|
var getMomentStamp = require('get-moment-stamp');
|
|
12
13
|
|
|
13
14
|
function _interopNamespaceDefault(e) {
|
|
@@ -3522,8 +3523,23 @@ const createToolCall = async (idx, tool, toolCalls, targetFn, reason, mode, outp
|
|
|
3522
3523
|
// handles this late error (e.g. changeToAgent recursion guard thrown after
|
|
3523
3524
|
// commitToolOutput). Without a recovery emission the pending waitForOutput
|
|
3524
3525
|
// of the enclosing execution would hang forever.
|
|
3525
|
-
|
|
3526
|
-
|
|
3526
|
+
try {
|
|
3527
|
+
const result = await self._resurrectModel(mode, `Late tool error after commit: name=${tool.function.name} error=${functoolsKit.getErrorMessage(error)}`);
|
|
3528
|
+
await self._emitOutput(mode, result, outputEpoch);
|
|
3529
|
+
}
|
|
3530
|
+
catch (recoveryError) {
|
|
3531
|
+
// createToolCall is fired without await: a throwing recovery
|
|
3532
|
+
// (_emitOutput throws by contract when validation fails twice) would
|
|
3533
|
+
// be an unhandled rejection. Resolve the waiter directly instead.
|
|
3534
|
+
console.error(`agent-swarm late tool error recovery failed functionName=${tool.function.name} error=${functoolsKit.getErrorMessage(recoveryError)}`);
|
|
3535
|
+
await errorSubject.next([
|
|
3536
|
+
self.params.clientId,
|
|
3537
|
+
recoveryError,
|
|
3538
|
+
]);
|
|
3539
|
+
if (outputEpoch === self._outputEpoch) {
|
|
3540
|
+
await self._outputSubject.next(createPlaceholder());
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3527
3543
|
}
|
|
3528
3544
|
}
|
|
3529
3545
|
finally {
|
|
@@ -3588,7 +3604,7 @@ const RUN_FN = async (incoming, self) => {
|
|
|
3588
3604
|
agentName: self.params.agentName,
|
|
3589
3605
|
content: incoming,
|
|
3590
3606
|
mode: "user",
|
|
3591
|
-
role: "
|
|
3607
|
+
role: "user",
|
|
3592
3608
|
});
|
|
3593
3609
|
const args = {
|
|
3594
3610
|
clientId: self.params.clientId,
|
|
@@ -3687,19 +3703,29 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3687
3703
|
if (!toolCalls.length) {
|
|
3688
3704
|
// maxToolCalls=0 dropped every call: the model wanted tools but none may
|
|
3689
3705
|
// run. Emit the (tool-stripped) content as the answer instead of leaving
|
|
3690
|
-
// the pending waitForOutput hanging forever.
|
|
3691
|
-
|
|
3706
|
+
// the pending waitForOutput hanging forever. _emitOutput owns the
|
|
3707
|
+
// transform, so pass the raw content — transforming here too would
|
|
3708
|
+
// apply a non-idempotent transform twice.
|
|
3692
3709
|
await self.params.history.push({
|
|
3693
3710
|
...message,
|
|
3694
3711
|
tool_calls: [],
|
|
3695
3712
|
agentName: self.params.agentName,
|
|
3696
3713
|
});
|
|
3697
|
-
await self._emitOutput(mode,
|
|
3714
|
+
await self._emitOutput(mode, message.content, outputEpoch);
|
|
3698
3715
|
return;
|
|
3699
3716
|
}
|
|
3700
3717
|
{
|
|
3701
|
-
|
|
3702
|
-
|
|
3718
|
+
// Navigation/action tools are registered by toolName, while the model
|
|
3719
|
+
// calls tools by function.name — check both, since they diverge for MCP
|
|
3720
|
+
// tools (mcp_ prefix) and dynamic tool schemas.
|
|
3721
|
+
const priorityTool = toolCalls.find((call) => {
|
|
3722
|
+
const targetFn = toolList.find((t) => t.function.name === call.function.name);
|
|
3723
|
+
return (swarm$1.navigationSchemaService.hasTool(call.function.name) ||
|
|
3724
|
+
swarm$1.actionSchemaService.hasTool(call.function.name) ||
|
|
3725
|
+
(targetFn &&
|
|
3726
|
+
(swarm$1.navigationSchemaService.hasTool(targetFn.toolName) ||
|
|
3727
|
+
swarm$1.actionSchemaService.hasTool(targetFn.toolName))));
|
|
3728
|
+
});
|
|
3703
3729
|
if (priorityTool) {
|
|
3704
3730
|
toolCalls = [priorityTool];
|
|
3705
3731
|
}
|
|
@@ -3724,6 +3750,11 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3724
3750
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3725
3751
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${result}`);
|
|
3726
3752
|
await self._emitOutput(mode, result, outputEpoch);
|
|
3753
|
+
// The chain's .finally decrement is attached only after the loop, so
|
|
3754
|
+
// an early return must roll the counter back itself — otherwise a
|
|
3755
|
+
// late tool error would forever think a chain is still consuming
|
|
3756
|
+
// events and skip its own recovery.
|
|
3757
|
+
self._activeToolChains -= 1;
|
|
3727
3758
|
run(false);
|
|
3728
3759
|
return;
|
|
3729
3760
|
}
|
|
@@ -3757,6 +3788,9 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3757
3788
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3758
3789
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${result}`);
|
|
3759
3790
|
await self._emitOutput(mode, result, outputEpoch);
|
|
3791
|
+
// See the "tool function not found" branch above: roll back the
|
|
3792
|
+
// counter that the post-loop .finally would otherwise decrement.
|
|
3793
|
+
self._activeToolChains -= 1;
|
|
3760
3794
|
run(false);
|
|
3761
3795
|
return;
|
|
3762
3796
|
}
|
|
@@ -3788,6 +3822,12 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3788
3822
|
if (lastStatus === TOOL_ERROR_SYMBOL) {
|
|
3789
3823
|
return lastStatus;
|
|
3790
3824
|
}
|
|
3825
|
+
if (lastStatus === CANCEL_OUTPUT_SYMBOL) {
|
|
3826
|
+
// commitCancelOutput halts the chain exactly like stop/error/change:
|
|
3827
|
+
// without this guard the next tool would still run after the user
|
|
3828
|
+
// cancelled the output.
|
|
3829
|
+
return lastStatus;
|
|
3830
|
+
}
|
|
3791
3831
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3792
3832
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} functionName=${tool.function.name} tool call executing`);
|
|
3793
3833
|
const statusAwaiter = Promise.race([
|
|
@@ -3813,39 +3853,53 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3813
3853
|
if (status === MODEL_RESQUE_SYMBOL) {
|
|
3814
3854
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3815
3855
|
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`);
|
|
3816
|
-
self.params.onAfterToolCalls &&
|
|
3817
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3818
3856
|
}
|
|
3819
3857
|
if (status === AGENT_CHANGE_SYMBOL) {
|
|
3820
3858
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3821
3859
|
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`);
|
|
3822
|
-
self.params.onAfterToolCalls &&
|
|
3823
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3824
3860
|
}
|
|
3825
3861
|
if (status === TOOL_STOP_SYMBOL) {
|
|
3826
3862
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3827
3863
|
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`);
|
|
3828
|
-
self.params.onAfterToolCalls &&
|
|
3829
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3830
3864
|
}
|
|
3831
3865
|
if (status === CANCEL_OUTPUT_SYMBOL) {
|
|
3832
3866
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3833
3867
|
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`);
|
|
3834
|
-
self.params.onAfterToolCalls &&
|
|
3835
|
-
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
3836
3868
|
}
|
|
3837
3869
|
if (status === TOOL_ERROR_SYMBOL) {
|
|
3838
3870
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3839
3871
|
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`);
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3872
|
+
try {
|
|
3873
|
+
const result = await self._resurrectModel(mode, `Function call failed with error: name=${tool.function.name} arguments=${JSON.stringify(tool.function.arguments)}`);
|
|
3874
|
+
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3875
|
+
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${result}`);
|
|
3876
|
+
await self._emitOutput(mode, result, outputEpoch);
|
|
3877
|
+
}
|
|
3878
|
+
catch (recoveryError) {
|
|
3879
|
+
// Nobody awaits the status chain: a throwing recovery
|
|
3880
|
+
// (_emitOutput throws by contract when validation fails twice)
|
|
3881
|
+
// would reject it unhandled. Resolve the waiter directly instead.
|
|
3882
|
+
console.error(`agent-swarm tool error recovery failed functionName=${tool.function.name} error=${functoolsKit.getErrorMessage(recoveryError)}`);
|
|
3883
|
+
await errorSubject.next([
|
|
3884
|
+
self.params.clientId,
|
|
3885
|
+
recoveryError,
|
|
3886
|
+
]);
|
|
3887
|
+
if (outputEpoch === self._outputEpoch) {
|
|
3888
|
+
await self._outputSubject.next(createPlaceholder());
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3844
3891
|
}
|
|
3845
3892
|
return status;
|
|
3846
3893
|
});
|
|
3847
3894
|
}
|
|
3848
|
-
lastToolStatusRef
|
|
3895
|
+
lastToolStatusRef
|
|
3896
|
+
.catch((error) => {
|
|
3897
|
+
// The chain is fire-and-forget; a rejected link (e.g. a throwing
|
|
3898
|
+
// params.map/bus adapter) must not surface as an unhandled rejection.
|
|
3899
|
+
console.error(`agent-swarm tool status chain error agentName=${self.params.agentName} clientId=${self.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
3900
|
+
return null;
|
|
3901
|
+
})
|
|
3902
|
+
.finally(() => {
|
|
3849
3903
|
self._activeToolChains -= 1;
|
|
3850
3904
|
self.params.onAfterToolCalls &&
|
|
3851
3905
|
self.params.onAfterToolCalls(self.params.clientId, self.params.agentName, toolCalls);
|
|
@@ -3857,22 +3911,16 @@ const EXECUTE_FN = async (incoming, mode, self) => {
|
|
|
3857
3911
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3858
3912
|
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute no tool calls detected`);
|
|
3859
3913
|
}
|
|
3860
|
-
const result = await self.params.transform(message.content, self.params.clientId, self.params.agentName);
|
|
3861
3914
|
await self.params.history.push({
|
|
3862
3915
|
...message,
|
|
3863
3916
|
agentName: self.params.agentName,
|
|
3864
3917
|
});
|
|
3865
|
-
let validation = null;
|
|
3866
|
-
if ((validation = await self.params.validate(result))) {
|
|
3867
|
-
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3868
|
-
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute invalid tool call detected: ${validation}`, { result });
|
|
3869
|
-
const result1 = await self._resurrectModel(mode, `Invalid model output: ${result}`);
|
|
3870
|
-
await self._emitOutput(mode, result1, outputEpoch);
|
|
3871
|
-
return;
|
|
3872
|
-
}
|
|
3873
3918
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3874
|
-
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${
|
|
3875
|
-
|
|
3919
|
+
self.params.logger.debug(`ClientAgent agentName=${self.params.agentName} clientId=${self.params.clientId} execute end result=${message.content}`);
|
|
3920
|
+
// _emitOutput owns transform + validate (with resurrect on failure), so the
|
|
3921
|
+
// raw content goes straight through — transforming/validating here as well
|
|
3922
|
+
// ran every transform twice per answer and broke non-idempotent transforms.
|
|
3923
|
+
await self._emitOutput(mode, message.content, outputEpoch);
|
|
3876
3924
|
};
|
|
3877
3925
|
/**
|
|
3878
3926
|
* Represents a client-side agent in the swarm system, implementing the IAgent interface.
|
|
@@ -3965,9 +4013,30 @@ class ClientAgent {
|
|
|
3965
4013
|
*/
|
|
3966
4014
|
this.execute = functoolsKit.queued(async (incoming, mode) => {
|
|
3967
4015
|
this._activeExecutions += 1;
|
|
4016
|
+
const outputEpoch = this._outputEpoch;
|
|
3968
4017
|
try {
|
|
3969
4018
|
return await EXECUTE_FN(incoming, mode, this);
|
|
3970
4019
|
}
|
|
4020
|
+
catch (error) {
|
|
4021
|
+
// EXECUTE_FN is fired without await from ClientSession.execute and
|
|
4022
|
+
// queued() rejects internally: an unguarded rejection (e.g. a throwing
|
|
4023
|
+
// completion in getCompletion) crashes the host process and leaves the
|
|
4024
|
+
// pending waitForOutput hanging forever. Recover with the resque flow.
|
|
4025
|
+
console.error(`agent-swarm execute error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4026
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4027
|
+
try {
|
|
4028
|
+
const result = await this._resurrectModel(mode, `Execution failed: ${functoolsKit.getErrorMessage(error)}`);
|
|
4029
|
+
await this._emitOutput(mode, result, outputEpoch);
|
|
4030
|
+
}
|
|
4031
|
+
catch (recoveryError) {
|
|
4032
|
+
// Even the recovery failed (e.g. recomplete strategy hitting the same
|
|
4033
|
+
// broken completion): resolve the waiter directly so nothing hangs.
|
|
4034
|
+
console.error(`agent-swarm execute recovery error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(recoveryError)}`);
|
|
4035
|
+
if (outputEpoch === this._outputEpoch) {
|
|
4036
|
+
await this._outputSubject.next(createPlaceholder());
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
3971
4040
|
finally {
|
|
3972
4041
|
this._activeExecutions -= 1;
|
|
3973
4042
|
}
|
|
@@ -3976,7 +4045,19 @@ class ClientAgent {
|
|
|
3976
4045
|
* Runs a stateless completion for the incoming message, queued to prevent overlapping executions.
|
|
3977
4046
|
* Implements IAgent.run, delegating to RUN_FN with queuing via functools-kit’s queued decorator.
|
|
3978
4047
|
*/
|
|
3979
|
-
this.run = functoolsKit.queued(async (incoming) =>
|
|
4048
|
+
this.run = functoolsKit.queued(async (incoming) => {
|
|
4049
|
+
try {
|
|
4050
|
+
return await RUN_FN(incoming, this);
|
|
4051
|
+
}
|
|
4052
|
+
catch (error) {
|
|
4053
|
+
// queued() rejects internally, so a throwing completion would raise an
|
|
4054
|
+
// unhandled rejection besides failing the caller: degrade to an empty
|
|
4055
|
+
// stateless result and surface the error through errorSubject.
|
|
4056
|
+
console.error(`agent-swarm run error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4057
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4058
|
+
return "";
|
|
4059
|
+
}
|
|
4060
|
+
});
|
|
3980
4061
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
3981
4062
|
this.params.logger.debug(`ClientAgent agentName=${this.params.agentName} clientId=${this.params.clientId} CTOR`, {
|
|
3982
4063
|
params,
|
|
@@ -3993,11 +4074,15 @@ class ClientAgent {
|
|
|
3993
4074
|
const seen = new Set();
|
|
3994
4075
|
const agentToolList = [];
|
|
3995
4076
|
if (this.params.tools) {
|
|
3996
|
-
|
|
4077
|
+
// Resolve availability/dynamic schemas concurrently, but keep the RESULT
|
|
4078
|
+
// in declaration order: pushing from within the concurrent map would order
|
|
4079
|
+
// tools by resolution time, making the tool list, the seen-based dedup and
|
|
4080
|
+
// the single-commit-action filter all non-deterministic across runs.
|
|
4081
|
+
const resolvedTools = await Promise.all(this.params.tools.map(async (tool) => {
|
|
3997
4082
|
const { toolName, function: upperFn, isAvailable = TOOL_AVAILABLE_DEFAULT, ...other } = tool;
|
|
3998
4083
|
try {
|
|
3999
4084
|
if (await functoolsKit.not(isAvailable(this.params.clientId, this.params.agentName, toolName))) {
|
|
4000
|
-
return;
|
|
4085
|
+
return null;
|
|
4001
4086
|
}
|
|
4002
4087
|
}
|
|
4003
4088
|
catch (error) {
|
|
@@ -4005,7 +4090,7 @@ class ClientAgent {
|
|
|
4005
4090
|
// hang the pending waitForOutput: treat it as "not available".
|
|
4006
4091
|
console.error(`agent-swarm isAvailable error toolName=${toolName} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4007
4092
|
await errorSubject.next([this.params.clientId, error]);
|
|
4008
|
-
return;
|
|
4093
|
+
return null;
|
|
4009
4094
|
}
|
|
4010
4095
|
let fn;
|
|
4011
4096
|
try {
|
|
@@ -4019,14 +4104,19 @@ class ClientAgent {
|
|
|
4019
4104
|
// the execution — the tool is skipped for this completion.
|
|
4020
4105
|
console.error(`agent-swarm dynamic tool function error toolName=${toolName} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4021
4106
|
await errorSubject.next([this.params.clientId, error]);
|
|
4022
|
-
return;
|
|
4107
|
+
return null;
|
|
4023
4108
|
}
|
|
4024
|
-
|
|
4109
|
+
return {
|
|
4025
4110
|
...other,
|
|
4026
4111
|
toolName,
|
|
4027
4112
|
function: fn,
|
|
4028
|
-
}
|
|
4113
|
+
};
|
|
4029
4114
|
}));
|
|
4115
|
+
for (const tool of resolvedTools) {
|
|
4116
|
+
if (tool) {
|
|
4117
|
+
agentToolList.push(tool);
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4030
4120
|
}
|
|
4031
4121
|
let mcpToolList = [];
|
|
4032
4122
|
try {
|
|
@@ -4175,6 +4265,10 @@ class ClientAgent {
|
|
|
4175
4265
|
}
|
|
4176
4266
|
this.params.onOutput &&
|
|
4177
4267
|
this.params.onOutput(this.params.clientId, this.params.agentName, result);
|
|
4268
|
+
// Keep the callback surface symmetric with the resurrect branch above:
|
|
4269
|
+
// the emitted output is an assistant message on both paths.
|
|
4270
|
+
this.params.onAssistantMessage &&
|
|
4271
|
+
this.params.onAssistantMessage(this.params.clientId, this.params.agentName, result);
|
|
4178
4272
|
await this._outputSubject.next(result);
|
|
4179
4273
|
await this.params.bus.emit(this.params.clientId, {
|
|
4180
4274
|
type: "emit-output",
|
|
@@ -4229,7 +4323,21 @@ class ClientAgent {
|
|
|
4229
4323
|
await this._resqueSubject.next(MODEL_RESQUE_SYMBOL);
|
|
4230
4324
|
return placeholder;
|
|
4231
4325
|
}
|
|
4232
|
-
|
|
4326
|
+
let rawMessage;
|
|
4327
|
+
try {
|
|
4328
|
+
rawMessage = await this.getCompletion(mode, []);
|
|
4329
|
+
}
|
|
4330
|
+
catch (error) {
|
|
4331
|
+
// _resurrectModel IS the recovery path: if the resque completion itself
|
|
4332
|
+
// rejects (e.g. the provider is down — often the very reason we are
|
|
4333
|
+
// here), rethrowing would surface as an unhandled rejection from the
|
|
4334
|
+
// fire-and-forget callers (createToolCall, tool status chain). Degrade
|
|
4335
|
+
// to the placeholder instead.
|
|
4336
|
+
console.error(`agent-swarm _resurrectModel completion error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4337
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4338
|
+
await this._resqueSubject.next(MODEL_RESQUE_SYMBOL);
|
|
4339
|
+
return placeholder;
|
|
4340
|
+
}
|
|
4233
4341
|
if (rawMessage.tool_calls?.length) {
|
|
4234
4342
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4235
4343
|
this.params.logger.debug(`ClientAgent agentName=${this.params.agentName} clientId=${this.params.clientId} _resurrectModel should not emit tool_calls`);
|
|
@@ -4718,6 +4826,21 @@ class OperatorSignal {
|
|
|
4718
4826
|
sendMessage(message, next) {
|
|
4719
4827
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4720
4828
|
this.params.logger.debug(`OperatorSignal agentName=${this.params.agentName} clientId=${this.params.clientId} sendMessage`, { message });
|
|
4829
|
+
// Dispose the previous signal before opening a new one: overwriting the
|
|
4830
|
+
// ref leaks the pending signal, and its late answer would resolve the
|
|
4831
|
+
// NEXT waitForOutput with a reply to the previous question.
|
|
4832
|
+
if (this._disposeRef) {
|
|
4833
|
+
const disposeRef = this._disposeRef;
|
|
4834
|
+
this._disposeRef = null;
|
|
4835
|
+
try {
|
|
4836
|
+
disposeRef();
|
|
4837
|
+
}
|
|
4838
|
+
catch (error) {
|
|
4839
|
+
// A throwing user dispose must not prevent the next signal from
|
|
4840
|
+
// opening — the previous one is torn down on a best-effort basis.
|
|
4841
|
+
console.error(`agent-swarm operator signal dispose error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4721
4844
|
this._disposeRef = this._signalFactory(message, next);
|
|
4722
4845
|
}
|
|
4723
4846
|
/**
|
|
@@ -4729,7 +4852,14 @@ class OperatorSignal {
|
|
|
4729
4852
|
if (this._disposeRef) {
|
|
4730
4853
|
const disposeRef = this._disposeRef;
|
|
4731
4854
|
this._disposeRef = null;
|
|
4732
|
-
|
|
4855
|
+
try {
|
|
4856
|
+
await disposeRef();
|
|
4857
|
+
}
|
|
4858
|
+
catch (error) {
|
|
4859
|
+
// dispose is awaited from waitForOutput's timeout path: a throwing
|
|
4860
|
+
// user dispose would reject waitForOutput and hang the swarm race.
|
|
4861
|
+
console.error(`agent-swarm operator signal dispose error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4862
|
+
}
|
|
4733
4863
|
}
|
|
4734
4864
|
}
|
|
4735
4865
|
}
|
|
@@ -4772,7 +4902,17 @@ class ClientOperator {
|
|
|
4772
4902
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4773
4903
|
this.params.logger.debug(`ClientOperator agentName=${this.params.agentName} clientId=${this.params.clientId} execute tool mode forwarded to operator`);
|
|
4774
4904
|
}
|
|
4775
|
-
|
|
4905
|
+
try {
|
|
4906
|
+
this._operatorSignal.sendMessage(input, this._outgoingSubject.next);
|
|
4907
|
+
}
|
|
4908
|
+
catch (error) {
|
|
4909
|
+
// execute is fired without await from ClientSession.execute: a throwing
|
|
4910
|
+
// operator connector would surface as an unhandled rejection and crash
|
|
4911
|
+
// the host process. Surface the error instead; the pending waitForOutput
|
|
4912
|
+
// resolves through the operator timeout.
|
|
4913
|
+
console.error(`agent-swarm operator connector error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
4914
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
4915
|
+
}
|
|
4776
4916
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
4777
4917
|
this.params.logger.debug(`ClientOperator agentName=${this.params.agentName} clientId=${this.params.clientId} execute end`, { input, mode });
|
|
4778
4918
|
}
|
|
@@ -5850,6 +5990,8 @@ class ClientHistory {
|
|
|
5850
5990
|
// the error through errorSubject so the caller's complete() rejects.
|
|
5851
5991
|
console.error(`agent-swarm history push error agentName=${this.params.agentName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
5852
5992
|
await errorSubject.next([this.params.clientId, error]);
|
|
5993
|
+
// The record was dropped: do not announce a successful push on the bus.
|
|
5994
|
+
return;
|
|
5853
5995
|
}
|
|
5854
5996
|
await this.params.bus.emit(this.params.clientId, {
|
|
5855
5997
|
type: "push",
|
|
@@ -6308,6 +6450,7 @@ class NoopAgent {
|
|
|
6308
6450
|
const message = `called commitCancelOutput on agent which not in the swarm clientId=${this.clientId} agentName=${this.agentName} swarmName=${this.swarmName}`;
|
|
6309
6451
|
this.logger.log(message);
|
|
6310
6452
|
console.error(message);
|
|
6453
|
+
return await this.defaultAgent.commitCancelOutput();
|
|
6311
6454
|
}
|
|
6312
6455
|
/**
|
|
6313
6456
|
* Logs an attempt to run the missing agent and delegates to the default agent's run method.
|
|
@@ -6504,7 +6647,10 @@ class ClientSwarm {
|
|
|
6504
6647
|
setBusy(isBusy) {
|
|
6505
6648
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
6506
6649
|
this.params.logger.debug(`ClientSwarm swarmName=${this.params.swarmName} clientId=${this.params.clientId} setBusy`, { isBusy });
|
|
6507
|
-
|
|
6650
|
+
// Clamp at zero: an unmatched setBusy(false) (reachable through the public
|
|
6651
|
+
// SwarmPublicService.setBusy) would drive the counter negative, and a
|
|
6652
|
+
// negative counter reads as "busy" forever — deadlocking the session lock.
|
|
6653
|
+
this._isBusy = Math.max(0, this._isBusy + (isBusy ? 1 : -1));
|
|
6508
6654
|
}
|
|
6509
6655
|
/**
|
|
6510
6656
|
* Getter for the busy state of the swarm.
|
|
@@ -6735,7 +6881,14 @@ class ClientSwarm {
|
|
|
6735
6881
|
});
|
|
6736
6882
|
if (!result) {
|
|
6737
6883
|
console.error(`agent-swarm ClientSwarm getAgent current agent is not in the swarm agentName=${agentName} clientId=${this.params.clientId} swarmName=${this.params.swarmName}`);
|
|
6738
|
-
|
|
6884
|
+
const defaultAgent = this.params.agentMap[this.params.defaultAgent];
|
|
6885
|
+
if (!defaultAgent) {
|
|
6886
|
+
// NoopAgent delegates every call to the default agent: with that one
|
|
6887
|
+
// missing too the delegation would die with a bare TypeError instead
|
|
6888
|
+
// of an actionable message.
|
|
6889
|
+
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}`);
|
|
6890
|
+
}
|
|
6891
|
+
return new NoopAgent(this.params.clientId, this.params.swarmName, agentName, defaultAgent, this.params.logger);
|
|
6739
6892
|
}
|
|
6740
6893
|
return result;
|
|
6741
6894
|
}
|
|
@@ -10882,15 +11035,49 @@ class ClientStorage {
|
|
|
10882
11035
|
*/
|
|
10883
11036
|
this._itemMap = new Map();
|
|
10884
11037
|
/**
|
|
10885
|
-
*
|
|
10886
|
-
*
|
|
11038
|
+
* Queued core of dispatch. The wrapped function must never reject: queued()
|
|
11039
|
+
* chains every call on the previous promise, so a rejection inside the queue
|
|
11040
|
+
* (e.g. a throwing createIndex/createEmbedding/setData) would reject every
|
|
11041
|
+
* already-queued dispatch with this foreign error WITHOUT running it — a
|
|
11042
|
+
* concurrent upsert/remove/clear would be silently dropped. Errors are boxed
|
|
11043
|
+
* here and rethrown outside the queue, so only the failing caller observes them.
|
|
10887
11044
|
*/
|
|
10888
|
-
this.
|
|
11045
|
+
this._dispatchQueue = functoolsKit.queued(async (action, payload) => {
|
|
11046
|
+
try {
|
|
11047
|
+
await DISPATCH_FN$1(action, this, payload);
|
|
11048
|
+
return null;
|
|
11049
|
+
}
|
|
11050
|
+
catch (error) {
|
|
11051
|
+
return { error };
|
|
11052
|
+
}
|
|
11053
|
+
});
|
|
11054
|
+
/**
|
|
11055
|
+
* Dispatches a storage action (upsert, remove, or clear) through the serialized
|
|
11056
|
+
* queue, delegating to DISPATCH_FN. Ensures sequential execution of storage
|
|
11057
|
+
* operations, supporting thread-safe updates from ClientAgent or tools.
|
|
11058
|
+
*/
|
|
11059
|
+
this.dispatch = async (action, payload) => {
|
|
11060
|
+
const result = await this._dispatchQueue(action, payload);
|
|
11061
|
+
if (result) {
|
|
11062
|
+
throw result.error;
|
|
11063
|
+
}
|
|
11064
|
+
};
|
|
10889
11065
|
/**
|
|
10890
11066
|
* Creates embeddings for the given item, memoized by item ID to avoid redundant calculations via CREATE_EMBEDDING_FN.
|
|
10891
11067
|
* Caches results for efficiency, cleared on upsert/remove to ensure freshness, supporting EmbeddingSchemaService.
|
|
10892
11068
|
*/
|
|
10893
|
-
this._createEmbedding = functoolsKit.memoize(([{ id }]) => id, async (item) =>
|
|
11069
|
+
this._createEmbedding = functoolsKit.memoize(([{ id }]) => id, async (item) => {
|
|
11070
|
+
try {
|
|
11071
|
+
return await CREATE_EMBEDDING_FN(item, this);
|
|
11072
|
+
}
|
|
11073
|
+
catch (error) {
|
|
11074
|
+
// Never cache a rejected embedding: the memoized promise would keep
|
|
11075
|
+
// this item permanently broken (every take re-throwing the same
|
|
11076
|
+
// error) even after the embedding provider recovers.
|
|
11077
|
+
this._createEmbedding.clear(item.id);
|
|
11078
|
+
throw error;
|
|
11079
|
+
}
|
|
11080
|
+
});
|
|
10894
11081
|
/**
|
|
10895
11082
|
* Waits for the initialization of the storage, loading initial data and creating embeddings via WAIT_FOR_INIT_FN.
|
|
10896
11083
|
* Ensures initialization happens only once using singleshot, supporting StorageConnectionService’s lifecycle.
|
|
@@ -11770,6 +11957,13 @@ const WAIT_FOR_INIT_FN = async (self) => {
|
|
|
11770
11957
|
* SwarmConnectionService (swarm-level state), and BusService (event emission).
|
|
11771
11958
|
*/
|
|
11772
11959
|
class ClientState {
|
|
11960
|
+
/**
|
|
11961
|
+
* True only while the caller runs inside the async context of an active
|
|
11962
|
+
* dispatch on this instance (i.e. a reentrant call from within a dispatchFn).
|
|
11963
|
+
*/
|
|
11964
|
+
get _inDispatch() {
|
|
11965
|
+
return this._dispatchContext.getStore() === true;
|
|
11966
|
+
}
|
|
11773
11967
|
/**
|
|
11774
11968
|
* Constructs a ClientState instance with the provided parameters.
|
|
11775
11969
|
* Invokes the onInit callback if provided and logs construction if debugging is enabled.
|
|
@@ -11778,29 +11972,50 @@ class ClientState {
|
|
|
11778
11972
|
this.params = params;
|
|
11779
11973
|
this.stateChanged = new functoolsKit.Subject();
|
|
11780
11974
|
/**
|
|
11781
|
-
*
|
|
11782
|
-
* getState checks it to serve reentrant reads (getState
|
|
11783
|
-
* dispatchFn) without re-entering the queue, which would
|
|
11975
|
+
* Marks the async execution context of a running dispatch (read/write).
|
|
11976
|
+
* getState checks it to serve reentrant reads (getState called from INSIDE a
|
|
11977
|
+
* setState dispatchFn/middleware) without re-entering the queue, which would
|
|
11978
|
+
* deadlock. Scoping this per async-context — instead of a plain instance flag —
|
|
11979
|
+
* ensures an UNRELATED concurrent getState still queues and observes writes in
|
|
11980
|
+
* order, rather than reading a stale field while some other write is in flight.
|
|
11784
11981
|
*/
|
|
11785
|
-
this.
|
|
11982
|
+
this._dispatchContext = new async_hooks.AsyncLocalStorage();
|
|
11786
11983
|
/**
|
|
11787
11984
|
* The current state data, initialized as null and set during waitForInit.
|
|
11788
11985
|
* Updated by setState and clearState, persisted via params.setState if provided.
|
|
11789
11986
|
*/
|
|
11790
11987
|
this._state = null;
|
|
11791
11988
|
/**
|
|
11792
|
-
* Queued
|
|
11793
|
-
*
|
|
11989
|
+
* Queued core of dispatch. The wrapped function must never reject: queued()
|
|
11990
|
+
* chains every call on the previous promise, so a rejection inside the queue
|
|
11991
|
+
* would reject every already-queued dispatch with this foreign error WITHOUT
|
|
11992
|
+
* running it — a concurrent setState would be silently dropped. Errors (e.g.
|
|
11993
|
+
* a throwing user dispatchFn or middleware) are boxed here and rethrown
|
|
11994
|
+
* outside the queue, so only the caller whose operation failed observes them.
|
|
11794
11995
|
*/
|
|
11795
|
-
this.
|
|
11796
|
-
this._inDispatch = true;
|
|
11996
|
+
this._dispatchQueue = functoolsKit.queued(async (action, payload) => {
|
|
11797
11997
|
try {
|
|
11798
|
-
return
|
|
11998
|
+
return {
|
|
11999
|
+
ok: true,
|
|
12000
|
+
state: await this._dispatchContext.run(true, async () => await DISPATCH_FN(action, this, payload)),
|
|
12001
|
+
};
|
|
11799
12002
|
}
|
|
11800
|
-
|
|
11801
|
-
|
|
12003
|
+
catch (error) {
|
|
12004
|
+
return { ok: false, error };
|
|
11802
12005
|
}
|
|
11803
12006
|
});
|
|
12007
|
+
/**
|
|
12008
|
+
* Dispatches a read or write of the state through the serialized queue,
|
|
12009
|
+
* delegating to DISPATCH_FN. Ensures thread-safe state operations, supporting
|
|
12010
|
+
* concurrent access from ClientAgent or tools.
|
|
12011
|
+
*/
|
|
12012
|
+
this.dispatch = async (action, payload) => {
|
|
12013
|
+
const result = await this._dispatchQueue(action, payload);
|
|
12014
|
+
if ("error" in result) {
|
|
12015
|
+
throw result.error;
|
|
12016
|
+
}
|
|
12017
|
+
return result.state;
|
|
12018
|
+
};
|
|
11804
12019
|
/**
|
|
11805
12020
|
* Waits for the state to initialize via WAIT_FOR_INIT_FN, ensuring it’s only called once using singleshot.
|
|
11806
12021
|
* Loads the initial state into _state, supporting StateConnectionService’s lifecycle management.
|
|
@@ -11830,8 +12045,18 @@ class ClientState {
|
|
|
11830
12045
|
});
|
|
11831
12046
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
11832
12047
|
this.params.logger.debug(`ClientState stateName=${this.params.stateName} clientId=${this.params.clientId} shared=${this.params.shared} setState output`, { pendingState: this._state });
|
|
11833
|
-
this.params.setState
|
|
11834
|
-
|
|
12048
|
+
if (this.params.setState) {
|
|
12049
|
+
try {
|
|
12050
|
+
await this.params.setState(this._state, this.params.clientId, this.params.stateName);
|
|
12051
|
+
}
|
|
12052
|
+
catch (error) {
|
|
12053
|
+
// Persistence was fired without await before: a rejecting adapter
|
|
12054
|
+
// raised an unhandled rejection. Keep the in-memory state and surface
|
|
12055
|
+
// the error to the caller through errorSubject instead.
|
|
12056
|
+
console.error(`agent-swarm state persist error stateName=${this.params.stateName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
12057
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
12058
|
+
}
|
|
12059
|
+
}
|
|
11835
12060
|
if (this.params.callbacks?.onWrite) {
|
|
11836
12061
|
this.params.callbacks.onWrite(this._state, this.params.clientId, this.params.stateName);
|
|
11837
12062
|
}
|
|
@@ -11863,8 +12088,17 @@ class ClientState {
|
|
|
11863
12088
|
});
|
|
11864
12089
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
11865
12090
|
this.params.logger.debug(`ClientState stateName=${this.params.stateName} clientId=${this.params.clientId} shared=${this.params.shared} clearState output`, { pendingState: this._state });
|
|
11866
|
-
this.params.setState
|
|
11867
|
-
|
|
12091
|
+
if (this.params.setState) {
|
|
12092
|
+
try {
|
|
12093
|
+
await this.params.setState(this._state, this.params.clientId, this.params.stateName);
|
|
12094
|
+
}
|
|
12095
|
+
catch (error) {
|
|
12096
|
+
// See setState above: a rejecting persistence adapter must not raise
|
|
12097
|
+
// an unhandled rejection.
|
|
12098
|
+
console.error(`agent-swarm state persist error stateName=${this.params.stateName} clientId=${this.params.clientId} error=${functoolsKit.getErrorMessage(error)}`);
|
|
12099
|
+
await errorSubject.next([this.params.clientId, error]);
|
|
12100
|
+
}
|
|
12101
|
+
}
|
|
11868
12102
|
if (this.params.callbacks?.onWrite) {
|
|
11869
12103
|
this.params.callbacks.onWrite(this._state, this.params.clientId, this.params.stateName);
|
|
11870
12104
|
}
|
|
@@ -14929,7 +15163,6 @@ class PolicyPublicService {
|
|
|
14929
15163
|
}
|
|
14930
15164
|
}
|
|
14931
15165
|
|
|
14932
|
-
const BAN_NEED_FETCH = Symbol("ban-need-fetch");
|
|
14933
15166
|
/**
|
|
14934
15167
|
* Class representing a client policy in the swarm system, implementing the IPolicy interface.
|
|
14935
15168
|
* Manages client bans, input/output validation, and restrictions, with lazy-loaded ban lists and event emission via BusService.
|
|
@@ -14938,6 +15171,32 @@ const BAN_NEED_FETCH = Symbol("ban-need-fetch");
|
|
|
14938
15171
|
* Supports auto-banning on validation failure and customizable ban messages, ensuring swarm security and compliance.
|
|
14939
15172
|
*/
|
|
14940
15173
|
class ClientPolicy {
|
|
15174
|
+
/**
|
|
15175
|
+
* Returns the ban set of the given swarm, fetching it on first access.
|
|
15176
|
+
* Re-checks the map after the await: a ban committed through _banQueue while
|
|
15177
|
+
* the fetch was in flight must not be overwritten by the stale fetch result.
|
|
15178
|
+
*/
|
|
15179
|
+
async _getBanSet(swarmName) {
|
|
15180
|
+
const existing = this._banSetBySwarm.get(swarmName);
|
|
15181
|
+
if (existing) {
|
|
15182
|
+
return existing;
|
|
15183
|
+
}
|
|
15184
|
+
const fetched = new Set(await this.params.getBannedClients(this.params.policyName, swarmName));
|
|
15185
|
+
if (!this._banSetBySwarm.has(swarmName)) {
|
|
15186
|
+
this._banSetBySwarm.set(swarmName, fetched);
|
|
15187
|
+
}
|
|
15188
|
+
return this._banSetBySwarm.get(swarmName);
|
|
15189
|
+
}
|
|
15190
|
+
/**
|
|
15191
|
+
* Runs a ban-set mutation through _banQueue, rethrowing its boxed error
|
|
15192
|
+
* outside the queue so only the failing caller observes it.
|
|
15193
|
+
*/
|
|
15194
|
+
async _runBanQueue(fn) {
|
|
15195
|
+
const result = await this._banQueue(fn);
|
|
15196
|
+
if (result) {
|
|
15197
|
+
throw result.error;
|
|
15198
|
+
}
|
|
15199
|
+
}
|
|
14941
15200
|
/**
|
|
14942
15201
|
* Constructs a ClientPolicy instance with the provided parameters.
|
|
14943
15202
|
* Invokes the onInit callback if defined and logs construction if debugging is enabled.
|
|
@@ -14945,11 +15204,13 @@ class ClientPolicy {
|
|
|
14945
15204
|
constructor(params) {
|
|
14946
15205
|
this.params = params;
|
|
14947
15206
|
/**
|
|
14948
|
-
*
|
|
14949
|
-
*
|
|
14950
|
-
*
|
|
15207
|
+
* Ban sets keyed by swarm name, lazily populated via params.getBannedClients.
|
|
15208
|
+
* A ClientPolicy instance is memoized per policyName and shared by every swarm
|
|
15209
|
+
* that lists the policy, while bans are persisted per (policy, swarm) — a
|
|
15210
|
+
* single shared set would leak bans of one swarm into another and persist
|
|
15211
|
+
* the mixed set into the wrong store.
|
|
14951
15212
|
*/
|
|
14952
|
-
this.
|
|
15213
|
+
this._banSetBySwarm = new Map();
|
|
14953
15214
|
/**
|
|
14954
15215
|
* Serializes the read-modify-write of _banSet shared by banClient/unbanClient.
|
|
14955
15216
|
* Without it two concurrent bans of different clients both read the same ban
|
|
@@ -14957,8 +15218,23 @@ class ClientPolicy {
|
|
|
14957
15218
|
* the earlier one — one ban is silently lost in memory and in the persisted
|
|
14958
15219
|
* store. queued() runs the mutations one at a time so each observes the result
|
|
14959
15220
|
* of the previous one.
|
|
14960
|
-
|
|
14961
|
-
|
|
15221
|
+
*
|
|
15222
|
+
* The wrapped function must never reject: queued() chains every call on the
|
|
15223
|
+
* previous promise, so a rejection inside the queue (e.g. a throwing
|
|
15224
|
+
* setBannedClients adapter) would reject every already-queued ban/unban with
|
|
15225
|
+
* this foreign error WITHOUT running it — a concurrent ban of another client
|
|
15226
|
+
* would be silently lost. Errors are boxed here and rethrown by the caller
|
|
15227
|
+
* outside the queue.
|
|
15228
|
+
*/
|
|
15229
|
+
this._banQueue = functoolsKit.queued(async (fn) => {
|
|
15230
|
+
try {
|
|
15231
|
+
await fn();
|
|
15232
|
+
return null;
|
|
15233
|
+
}
|
|
15234
|
+
catch (error) {
|
|
15235
|
+
return { error };
|
|
15236
|
+
}
|
|
15237
|
+
});
|
|
14962
15238
|
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
|
|
14963
15239
|
this.params.logger.debug(`ClientPolicy policyName=${this.params.policyName} CTOR`, {
|
|
14964
15240
|
params,
|
|
@@ -14977,10 +15253,8 @@ class ClientPolicy {
|
|
|
14977
15253
|
clientId,
|
|
14978
15254
|
swarmName,
|
|
14979
15255
|
});
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
}
|
|
14983
|
-
return this._banSet.has(clientId);
|
|
15256
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15257
|
+
return banSet.has(clientId);
|
|
14984
15258
|
}
|
|
14985
15259
|
/**
|
|
14986
15260
|
* Retrieves the ban message for a client, using a custom getBanMessage function if provided or falling back to params.banMessage.
|
|
@@ -15029,10 +15303,8 @@ class ClientPolicy {
|
|
|
15029
15303
|
},
|
|
15030
15304
|
clientId,
|
|
15031
15305
|
});
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
}
|
|
15035
|
-
if (this._banSet.has(clientId)) {
|
|
15306
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15307
|
+
if (banSet.has(clientId)) {
|
|
15036
15308
|
return false;
|
|
15037
15309
|
}
|
|
15038
15310
|
if (!this.params.validateInput) {
|
|
@@ -15074,10 +15346,8 @@ class ClientPolicy {
|
|
|
15074
15346
|
},
|
|
15075
15347
|
clientId,
|
|
15076
15348
|
});
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
}
|
|
15080
|
-
if (this._banSet.has(clientId)) {
|
|
15349
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15350
|
+
if (banSet.has(clientId)) {
|
|
15081
15351
|
return false;
|
|
15082
15352
|
}
|
|
15083
15353
|
if (!this.params.validateOutput) {
|
|
@@ -15102,31 +15372,32 @@ class ClientPolicy {
|
|
|
15102
15372
|
clientId,
|
|
15103
15373
|
swarmName,
|
|
15104
15374
|
});
|
|
15105
|
-
|
|
15106
|
-
this.
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
input: {},
|
|
15112
|
-
output: {},
|
|
15113
|
-
context: {
|
|
15114
|
-
policyName: this.params.policyName,
|
|
15115
|
-
swarmName,
|
|
15116
|
-
},
|
|
15117
|
-
clientId,
|
|
15118
|
-
});
|
|
15119
|
-
await this._banQueue(async () => {
|
|
15120
|
-
if (this._banSet === BAN_NEED_FETCH) {
|
|
15121
|
-
this._banSet = new Set(await this.params.getBannedClients(this.params.policyName, swarmName));
|
|
15122
|
-
}
|
|
15123
|
-
if (this._banSet.has(clientId)) {
|
|
15375
|
+
await this._runBanQueue(async () => {
|
|
15376
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15377
|
+
if (banSet.has(clientId)) {
|
|
15378
|
+
// Already banned: skip the mutation AND the notifications, matching
|
|
15379
|
+
// the documented "skips if already banned" contract — otherwise every
|
|
15380
|
+
// redundant ban re-fires onBanClient and the bus event.
|
|
15124
15381
|
return;
|
|
15125
15382
|
}
|
|
15126
|
-
this.
|
|
15383
|
+
this._banSetBySwarm.set(swarmName, new Set(banSet).add(clientId));
|
|
15127
15384
|
if (this.params.setBannedClients) {
|
|
15128
|
-
await this.params.setBannedClients([...this.
|
|
15385
|
+
await this.params.setBannedClients([...this._banSetBySwarm.get(swarmName)], this.params.policyName, swarmName);
|
|
15386
|
+
}
|
|
15387
|
+
if (this.params.callbacks?.onBanClient) {
|
|
15388
|
+
this.params.callbacks.onBanClient(clientId, swarmName, this.params.policyName);
|
|
15129
15389
|
}
|
|
15390
|
+
await this.params.bus.emit(clientId, {
|
|
15391
|
+
type: "ban-client",
|
|
15392
|
+
source: "policy-bus",
|
|
15393
|
+
input: {},
|
|
15394
|
+
output: {},
|
|
15395
|
+
context: {
|
|
15396
|
+
policyName: this.params.policyName,
|
|
15397
|
+
swarmName,
|
|
15398
|
+
},
|
|
15399
|
+
clientId,
|
|
15400
|
+
});
|
|
15130
15401
|
});
|
|
15131
15402
|
}
|
|
15132
15403
|
/**
|
|
@@ -15140,35 +15411,35 @@ class ClientPolicy {
|
|
|
15140
15411
|
clientId,
|
|
15141
15412
|
swarmName,
|
|
15142
15413
|
});
|
|
15143
|
-
|
|
15144
|
-
this.
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
source: "policy-bus",
|
|
15149
|
-
input: {},
|
|
15150
|
-
output: {},
|
|
15151
|
-
context: {
|
|
15152
|
-
policyName: this.params.policyName,
|
|
15153
|
-
swarmName,
|
|
15154
|
-
},
|
|
15155
|
-
clientId,
|
|
15156
|
-
});
|
|
15157
|
-
await this._banQueue(async () => {
|
|
15158
|
-
if (this._banSet === BAN_NEED_FETCH) {
|
|
15159
|
-
this._banSet = new Set(await this.params.getBannedClients(this.params.policyName, swarmName));
|
|
15160
|
-
}
|
|
15161
|
-
if (!this._banSet.has(clientId)) {
|
|
15414
|
+
await this._runBanQueue(async () => {
|
|
15415
|
+
const banSet = await this._getBanSet(swarmName);
|
|
15416
|
+
if (!banSet.has(clientId)) {
|
|
15417
|
+
// Not banned: skip the mutation and the notifications, matching the
|
|
15418
|
+
// documented "skips if not banned" contract.
|
|
15162
15419
|
return;
|
|
15163
15420
|
}
|
|
15164
15421
|
{
|
|
15165
|
-
const
|
|
15166
|
-
|
|
15167
|
-
this.
|
|
15422
|
+
const nextBanSet = new Set(banSet);
|
|
15423
|
+
nextBanSet.delete(clientId);
|
|
15424
|
+
this._banSetBySwarm.set(swarmName, nextBanSet);
|
|
15168
15425
|
}
|
|
15169
15426
|
if (this.params.setBannedClients) {
|
|
15170
|
-
await this.params.setBannedClients([...this.
|
|
15427
|
+
await this.params.setBannedClients([...this._banSetBySwarm.get(swarmName)], this.params.policyName, swarmName);
|
|
15428
|
+
}
|
|
15429
|
+
if (this.params.callbacks?.onUnbanClient) {
|
|
15430
|
+
this.params.callbacks.onUnbanClient(clientId, swarmName, this.params.policyName);
|
|
15171
15431
|
}
|
|
15432
|
+
await this.params.bus.emit(clientId, {
|
|
15433
|
+
type: "unban-client",
|
|
15434
|
+
source: "policy-bus",
|
|
15435
|
+
input: {},
|
|
15436
|
+
output: {},
|
|
15437
|
+
context: {
|
|
15438
|
+
policyName: this.params.policyName,
|
|
15439
|
+
swarmName,
|
|
15440
|
+
},
|
|
15441
|
+
clientId,
|
|
15442
|
+
});
|
|
15172
15443
|
});
|
|
15173
15444
|
}
|
|
15174
15445
|
}
|