@nolag/agents 0.1.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/AgentRoom.d.ts +16 -3
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/correlation.d.ts +1 -1
- package/dist/envelope.d.ts +2 -2
- package/dist/errors.d.ts +11 -0
- package/dist/index.cjs +223 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +222 -51
- package/dist/index.mjs.map +1 -1
- package/dist/patterns/handoff.d.ts +20 -4
- package/dist/patterns/tools.d.ts +5 -0
- package/dist/types.d.ts +34 -0
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -66,6 +66,10 @@ const TOPIC_TOOLS = "tools";
|
|
|
66
66
|
const TOPIC_APPROVAL = "approval";
|
|
67
67
|
/** Default room for agent coordination */
|
|
68
68
|
const DEFAULT_ROOM = "default-workflow";
|
|
69
|
+
/** Agents-protocol version: 2 = directed replies (filter-routed results),
|
|
70
|
+
* NO_HANDLER NACKs, presence protocol advertisement. Absent/1 = legacy
|
|
71
|
+
* broadcast replies (pre-0.2.0 SDKs). */
|
|
72
|
+
const AGENTS_PROTOCOL_VERSION = 2;
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
75
|
* AgentRoom — wraps a RoomContext from @nolag/js-sdk.
|
|
@@ -98,10 +102,13 @@ class AgentRoom extends EventEmitter {
|
|
|
98
102
|
this._presence = presence;
|
|
99
103
|
this._wireTopicListeners();
|
|
100
104
|
this._wirePresenceListeners();
|
|
101
|
-
// Set presence if provided
|
|
105
|
+
// Set presence if provided (with the SDK's protocol version advertised
|
|
106
|
+
// so counterparts can detect incompatible reply semantics)
|
|
102
107
|
if (presence) {
|
|
103
|
-
|
|
104
|
-
this.
|
|
108
|
+
const withProtocol = { protocol: AGENTS_PROTOCOL_VERSION, ...presence };
|
|
109
|
+
this._presence = withProtocol;
|
|
110
|
+
this._log(`setting presence in room ${name}:`, withProtocol);
|
|
111
|
+
this._roomContext.setPresence(withProtocol);
|
|
105
112
|
}
|
|
106
113
|
// Fetch initial presence snapshot
|
|
107
114
|
this._fetchInitialPresence();
|
|
@@ -134,11 +141,12 @@ class AgentRoom extends EventEmitter {
|
|
|
134
141
|
// ============================================================
|
|
135
142
|
// PRESENCE
|
|
136
143
|
// ============================================================
|
|
137
|
-
/** Update this agent's presence data */
|
|
144
|
+
/** Update this agent's presence data (protocol version auto-injected) */
|
|
138
145
|
setPresence(data) {
|
|
139
|
-
|
|
146
|
+
const withProtocol = { protocol: AGENTS_PROTOCOL_VERSION, ...data };
|
|
147
|
+
this._presence = withProtocol;
|
|
140
148
|
this._log(`updating presence in room ${this.name}`);
|
|
141
|
-
this._roomContext.setPresence(
|
|
149
|
+
this._roomContext.setPresence(withProtocol);
|
|
142
150
|
}
|
|
143
151
|
/** Fetch current presence snapshot for this room */
|
|
144
152
|
async fetchPresence() {
|
|
@@ -176,13 +184,20 @@ class AgentRoom extends EventEmitter {
|
|
|
176
184
|
}
|
|
177
185
|
this._publish(TOPIC_TASKS, envelope);
|
|
178
186
|
}
|
|
179
|
-
/** Publish to the results topic */
|
|
187
|
+
/** Publish to the results topic — directed to the dispatcher via filter when replyTo is set */
|
|
180
188
|
publishResult(envelope) {
|
|
181
189
|
// Auto-set completedBy if not set
|
|
182
190
|
if (!envelope.completedBy) {
|
|
183
191
|
envelope.completedBy = this.agentId;
|
|
184
192
|
}
|
|
185
|
-
|
|
193
|
+
if (envelope.replyTo) {
|
|
194
|
+
this._publish(TOPIC_RESULTS, envelope, { filter: envelope.replyTo });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Legacy: no reply address — unfiltered publish (only reaches
|
|
198
|
+
// wildcard subscribers, i.e. pre-0.2.0 SDKs)
|
|
199
|
+
this._publish(TOPIC_RESULTS, envelope);
|
|
200
|
+
}
|
|
186
201
|
}
|
|
187
202
|
/** Publish to the state topic (retained) */
|
|
188
203
|
publishState(data) {
|
|
@@ -204,8 +219,17 @@ class AgentRoom extends EventEmitter {
|
|
|
204
219
|
publishInbox(data) {
|
|
205
220
|
this._publish(TOPIC_INBOX, data);
|
|
206
221
|
}
|
|
207
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Publish a tool message.
|
|
224
|
+
* Requests go to the tools topic (load-balanced one-of-N across server
|
|
225
|
+
* replicas). Responses are directed to the requester on the results topic
|
|
226
|
+
* via filter — never load-balanced, never broadcast.
|
|
227
|
+
*/
|
|
208
228
|
publishTools(data) {
|
|
229
|
+
if (data?.type === "tool_response" && typeof data.replyTo === "string" && data.replyTo) {
|
|
230
|
+
this._publish(TOPIC_RESULTS, data, { filter: data.replyTo });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
209
233
|
this._publish(TOPIC_TOOLS, data);
|
|
210
234
|
}
|
|
211
235
|
/** Publish to the approval topic (retained) */
|
|
@@ -233,6 +257,8 @@ class AgentRoom extends EventEmitter {
|
|
|
233
257
|
capabilities: presence.capabilities || [],
|
|
234
258
|
metadata: presence.metadata,
|
|
235
259
|
connectedAt: actor.joinedAt || Date.now(),
|
|
260
|
+
protocol: typeof presence.protocol === 'number' ? presence.protocol : 1,
|
|
261
|
+
status: actor.status,
|
|
236
262
|
};
|
|
237
263
|
}
|
|
238
264
|
async _fetchInitialPresence() {
|
|
@@ -268,6 +294,7 @@ class AgentRoom extends EventEmitter {
|
|
|
268
294
|
capabilities: data.capabilities || [],
|
|
269
295
|
metadata: data.metadata,
|
|
270
296
|
connectedAt: Date.now(),
|
|
297
|
+
protocol: typeof data.protocol === 'number' ? data.protocol : 1,
|
|
271
298
|
};
|
|
272
299
|
this._agents.set(id, agent);
|
|
273
300
|
this._log(`agent joined room ${this.name}:`, agent.name, agent.capabilities);
|
|
@@ -299,6 +326,7 @@ class AgentRoom extends EventEmitter {
|
|
|
299
326
|
capabilities: data.capabilities || existing?.capabilities || [],
|
|
300
327
|
metadata: data.metadata || existing?.metadata,
|
|
301
328
|
connectedAt: existing?.connectedAt || Date.now(),
|
|
329
|
+
protocol: typeof data.protocol === 'number' ? data.protocol : (existing?.protocol ?? 1),
|
|
302
330
|
};
|
|
303
331
|
this._agents.set(id, agent);
|
|
304
332
|
this.emit('presenceUpdate', id, data);
|
|
@@ -307,23 +335,33 @@ class AgentRoom extends EventEmitter {
|
|
|
307
335
|
});
|
|
308
336
|
}
|
|
309
337
|
_wireTopicListeners() {
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
338
|
+
// Work distribution topics honour the connection-level loadBalance
|
|
339
|
+
// setting, so a pool shares each message one-of-N (no double handling):
|
|
340
|
+
// - tasks: each task goes to exactly one worker in the group
|
|
341
|
+
// - tools: each tool REQUEST goes to exactly one tool-server replica
|
|
342
|
+
this._roomContext.subscribe(TOPIC_TASKS);
|
|
343
|
+
this._roomContext.subscribe(TOPIC_TOOLS);
|
|
344
|
+
// Replies are DIRECTED, not broadcast: the results topic carries task
|
|
345
|
+
// results and tool responses published with `filter: <recipient agentId>`,
|
|
346
|
+
// and each agent subscribes only to its own filter sub-topic. The broker
|
|
347
|
+
// routes each reply straight to the requester — no fan-out waste, and
|
|
348
|
+
// immune to load-balance groups (a broadcast or LB'd reply could land on
|
|
349
|
+
// a group member that doesn't hold the pending correlation, timing out
|
|
350
|
+
// the requester even though the responder did the work).
|
|
351
|
+
this._roomContext.subscribe(TOPIC_RESULTS, {
|
|
352
|
+
loadBalance: false,
|
|
353
|
+
filters: [this.agentId],
|
|
354
|
+
});
|
|
355
|
+
// Broadcast topics must always fan out, even when the connection enables
|
|
356
|
+
// loadBalance for work distribution: state/events are broadcasts by
|
|
357
|
+
// nature; inbox and approval messages are claimed client-side.
|
|
358
|
+
const broadcastTopics = [TOPIC_STATE, TOPIC_EVENTS, TOPIC_INBOX, TOPIC_APPROVAL];
|
|
359
|
+
for (const topic of broadcastTopics) {
|
|
360
|
+
this._roomContext.subscribe(topic, { loadBalance: false });
|
|
322
361
|
}
|
|
323
362
|
// Simple 1:1 mappings
|
|
324
363
|
const simpleMap = [
|
|
325
364
|
{ topic: TOPIC_TASKS, event: "task" },
|
|
326
|
-
{ topic: TOPIC_RESULTS, event: "result" },
|
|
327
365
|
{ topic: TOPIC_STATE, event: "stateChange" },
|
|
328
366
|
{ topic: TOPIC_EVENTS, event: "event" },
|
|
329
367
|
{ topic: TOPIC_INBOX, event: "inbox" },
|
|
@@ -334,6 +372,17 @@ class AgentRoom extends EventEmitter {
|
|
|
334
372
|
this.emit(event, data);
|
|
335
373
|
});
|
|
336
374
|
}
|
|
375
|
+
// Multiplexed: results topic carries task results AND tool responses,
|
|
376
|
+
// both filter-directed to this agent.
|
|
377
|
+
this._roomContext.on(TOPIC_RESULTS, (data) => {
|
|
378
|
+
this._log(`received ${TOPIC_RESULTS} in room ${this.name}`);
|
|
379
|
+
if (data?.type === "tool_response") {
|
|
380
|
+
this.emit("toolResponse", data);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
this.emit("result", data);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
337
386
|
// Multiplexed: approval topic carries requests + responses
|
|
338
387
|
this._roomContext.on(TOPIC_APPROVAL, (data) => {
|
|
339
388
|
this._log(`received ${TOPIC_APPROVAL} in room ${this.name}`);
|
|
@@ -344,7 +393,9 @@ class AgentRoom extends EventEmitter {
|
|
|
344
393
|
this.emit("approvalRequest", data);
|
|
345
394
|
}
|
|
346
395
|
});
|
|
347
|
-
//
|
|
396
|
+
// Tools topic carries requests; tool_response is still accepted here for
|
|
397
|
+
// backward compatibility with responders on older SDK versions (their
|
|
398
|
+
// responses are only reliable when the requester is not load-balanced).
|
|
348
399
|
this._roomContext.on(TOPIC_TOOLS, (data) => {
|
|
349
400
|
this._log(`received ${TOPIC_TOOLS} in room ${this.name}`);
|
|
350
401
|
if (data?.type === "tool_response") {
|
|
@@ -582,6 +633,24 @@ class NoLagAgents extends EventEmitter {
|
|
|
582
633
|
}
|
|
583
634
|
}
|
|
584
635
|
|
|
636
|
+
/**
|
|
637
|
+
* IncompatibleProtocolError — thrown when an operation would deterministically
|
|
638
|
+
* fail because every relevant counterpart runs an older agents-protocol
|
|
639
|
+
* (pre-directed-replies). Failing fast beats burning the correlation timeout.
|
|
640
|
+
*/
|
|
641
|
+
class IncompatibleProtocolError extends Error {
|
|
642
|
+
constructor(operation, agents) {
|
|
643
|
+
const list = agents.map((a) => `${a.name} (protocol ${a.protocol})`).join(", ");
|
|
644
|
+
super(`${operation} cannot succeed: every relevant agent advertises agents-protocol < 2 ` +
|
|
645
|
+
`[${list}]. Protocol >= 2 responders direct replies to the requester; older ones ` +
|
|
646
|
+
`broadcast and their replies never reach this SDK's filtered subscription. ` +
|
|
647
|
+
`Upgrade the responders to @nolag/agents >= 0.2.0 / nolag-agents >= 0.3.0. ` +
|
|
648
|
+
`NOTE: 0.2.x/0.3.0 responders DO have directed replies but do not yet advertise ` +
|
|
649
|
+
`protocol — if your responders run those versions, pass { allowLegacyResponders: true }.`);
|
|
650
|
+
this.name = "IncompatibleProtocolError";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
585
654
|
/**
|
|
586
655
|
* CorrelationManager — maps correlationIds to pending promises with timeout.
|
|
587
656
|
* Used by Handoff and Tools patterns for request/response correlation.
|
|
@@ -594,13 +663,16 @@ class CorrelationManager {
|
|
|
594
663
|
* Register a pending correlation. Returns a promise that resolves
|
|
595
664
|
* when `resolve()` is called with the matching correlationId.
|
|
596
665
|
*/
|
|
597
|
-
register(correlationId, timeoutMs) {
|
|
666
|
+
register(correlationId, timeoutMs, context) {
|
|
598
667
|
return new Promise((resolve, reject) => {
|
|
599
668
|
let timer = null;
|
|
600
669
|
if (timeoutMs && timeoutMs > 0) {
|
|
601
670
|
timer = setTimeout(() => {
|
|
602
671
|
this._pending.delete(correlationId);
|
|
603
|
-
|
|
672
|
+
// Context turns an opaque correlation id into an actionable error —
|
|
673
|
+
// callers supply what they were waiting for and the likely causes.
|
|
674
|
+
const what = context ?? `Correlation ${correlationId}`;
|
|
675
|
+
reject(new Error(`${what} timed out after ${timeoutMs}ms`));
|
|
604
676
|
}, timeoutMs);
|
|
605
677
|
}
|
|
606
678
|
this._pending.set(correlationId, { resolve, reject, timer });
|
|
@@ -658,6 +730,7 @@ class CorrelationManager {
|
|
|
658
730
|
function createTaskEnvelope(capability, payload, options) {
|
|
659
731
|
return {
|
|
660
732
|
type: "task",
|
|
733
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
661
734
|
taskId: generateId(),
|
|
662
735
|
correlationId: generateId(),
|
|
663
736
|
replyTo: options?.replyTo,
|
|
@@ -671,9 +744,10 @@ function createTaskEnvelope(capability, payload, options) {
|
|
|
671
744
|
timeout: options?.timeout,
|
|
672
745
|
};
|
|
673
746
|
}
|
|
674
|
-
function createResultEnvelope(taskId, correlationId, status, payload, error, completedBy) {
|
|
747
|
+
function createResultEnvelope(taskId, correlationId, status, payload, error, completedBy, replyTo) {
|
|
675
748
|
return {
|
|
676
749
|
type: "result",
|
|
750
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
677
751
|
correlationId,
|
|
678
752
|
taskId,
|
|
679
753
|
status,
|
|
@@ -681,11 +755,13 @@ function createResultEnvelope(taskId, correlationId, status, payload, error, com
|
|
|
681
755
|
error,
|
|
682
756
|
completedAt: createTimestamp(),
|
|
683
757
|
completedBy,
|
|
758
|
+
replyTo,
|
|
684
759
|
};
|
|
685
760
|
}
|
|
686
761
|
function createStateEnvelope(key, value, version, updatedBy) {
|
|
687
762
|
return {
|
|
688
763
|
type: "state",
|
|
764
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
689
765
|
key,
|
|
690
766
|
value,
|
|
691
767
|
version,
|
|
@@ -696,6 +772,7 @@ function createStateEnvelope(key, value, version, updatedBy) {
|
|
|
696
772
|
function createEventEnvelope(category, emittedBy, payload, severity = "info") {
|
|
697
773
|
return {
|
|
698
774
|
type: "event",
|
|
775
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
699
776
|
eventId: generateId(),
|
|
700
777
|
severity,
|
|
701
778
|
category,
|
|
@@ -707,6 +784,7 @@ function createEventEnvelope(category, emittedBy, payload, severity = "info") {
|
|
|
707
784
|
function createApprovalRequest(action, context, requestedBy, options) {
|
|
708
785
|
return {
|
|
709
786
|
type: "approval_request",
|
|
787
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
710
788
|
requestId: generateId(),
|
|
711
789
|
correlationId: generateId(),
|
|
712
790
|
action,
|
|
@@ -720,6 +798,7 @@ function createApprovalRequest(action, context, requestedBy, options) {
|
|
|
720
798
|
function createApprovalResponse(requestId, correlationId, decision, respondedBy, reason) {
|
|
721
799
|
return {
|
|
722
800
|
type: "approval_response",
|
|
801
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
723
802
|
requestId,
|
|
724
803
|
correlationId,
|
|
725
804
|
decision,
|
|
@@ -731,6 +810,7 @@ function createApprovalResponse(requestId, correlationId, decision, respondedBy,
|
|
|
731
810
|
function createToolRequest(toolName, args, requestedBy, options) {
|
|
732
811
|
return {
|
|
733
812
|
type: "tool_request",
|
|
813
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
734
814
|
requestId: generateId(),
|
|
735
815
|
correlationId: generateId(),
|
|
736
816
|
replyTo: options?.replyTo,
|
|
@@ -740,9 +820,10 @@ function createToolRequest(toolName, args, requestedBy, options) {
|
|
|
740
820
|
requestedAt: createTimestamp(),
|
|
741
821
|
};
|
|
742
822
|
}
|
|
743
|
-
function createToolResponse(requestId, correlationId, status, result, error, respondedBy) {
|
|
823
|
+
function createToolResponse(requestId, correlationId, status, result, error, respondedBy, replyTo) {
|
|
744
824
|
return {
|
|
745
825
|
type: "tool_response",
|
|
826
|
+
protocol: AGENTS_PROTOCOL_VERSION,
|
|
746
827
|
requestId,
|
|
747
828
|
correlationId,
|
|
748
829
|
status,
|
|
@@ -750,6 +831,7 @@ function createToolResponse(requestId, correlationId, status, result, error, res
|
|
|
750
831
|
error,
|
|
751
832
|
respondedBy,
|
|
752
833
|
respondedAt: createTimestamp(),
|
|
834
|
+
replyTo,
|
|
753
835
|
};
|
|
754
836
|
}
|
|
755
837
|
|
|
@@ -780,6 +862,7 @@ function createToolResponse(requestId, correlationId, status, result, error, res
|
|
|
780
862
|
class Handoff {
|
|
781
863
|
constructor(room) {
|
|
782
864
|
this._correlations = new CorrelationManager();
|
|
865
|
+
this._warnedMixed = false;
|
|
783
866
|
this._room = room;
|
|
784
867
|
// Wire result correlation
|
|
785
868
|
this._room.on("result", (envelope) => {
|
|
@@ -794,11 +877,16 @@ class Handoff {
|
|
|
794
877
|
* agent is connected (unless `allowNoWorkers` is set).
|
|
795
878
|
*/
|
|
796
879
|
async dispatch(capability, payload, options) {
|
|
797
|
-
// Service discovery: check if any agent can handle this capability
|
|
880
|
+
// Service discovery: check if any agent can handle this capability.
|
|
881
|
+
// Persistent Presence: findAgents includes offline persistent agents, which
|
|
882
|
+
// the broker wakes on publish — so they satisfy the gate unless requireOnline.
|
|
798
883
|
if (!options?.allowNoWorkers) {
|
|
799
884
|
const capable = this._room.findAgents(capability);
|
|
800
|
-
|
|
801
|
-
|
|
885
|
+
const usable = options?.requireOnline
|
|
886
|
+
? capable.filter((a) => a.status === undefined || a.status === "online")
|
|
887
|
+
: capable;
|
|
888
|
+
if (usable.length === 0) {
|
|
889
|
+
throw new Error(`No ${options?.requireOnline ? "online " : ""}agent with capability "${capability}" is available. ` +
|
|
802
890
|
`Available capabilities: [${this._room.getAvailableCapabilities().join(', ')}]. ` +
|
|
803
891
|
`Connected agents: ${this._room.getConnectedAgents().length}. ` +
|
|
804
892
|
`Use { allowNoWorkers: true } to dispatch anyway.`);
|
|
@@ -806,32 +894,48 @@ class Handoff {
|
|
|
806
894
|
}
|
|
807
895
|
const envelope = createTaskEnvelope(capability, payload, {
|
|
808
896
|
...options,
|
|
809
|
-
createdBy: this._room.agentId,
|
|
897
|
+
createdBy: options?.createdBy ?? this._room.agentId,
|
|
898
|
+
// Reply address: workers publish the result filter-directed to this
|
|
899
|
+
// room's results subscription
|
|
900
|
+
replyTo: options?.replyTo ?? this._room.agentId,
|
|
810
901
|
});
|
|
811
902
|
this._room.publishTask(envelope);
|
|
812
903
|
if (options?.waitForResult) {
|
|
813
|
-
|
|
904
|
+
// Fail fast when the outcome is deterministic: if capable workers are
|
|
905
|
+
// visible and ALL advertise agents-protocol < 2, their results cannot
|
|
906
|
+
// reach this dispatcher's filtered subscription. Mixed pools proceed
|
|
907
|
+
// with a warning (presence is eventually consistent).
|
|
908
|
+
const capable = this._room.findAgents(capability);
|
|
909
|
+
if (!options?.allowLegacyResponders && capable.length > 0) {
|
|
910
|
+
const modern = capable.filter((a) => a.protocol >= 2);
|
|
911
|
+
if (modern.length === 0) {
|
|
912
|
+
throw new IncompatibleProtocolError(`Task '${capability}' dispatch with waitForResult`, capable.map((a) => ({ name: a.name, protocol: a.protocol })));
|
|
913
|
+
}
|
|
914
|
+
if (modern.length < capable.length && !this._warnedMixed) {
|
|
915
|
+
this._warnedMixed = true;
|
|
916
|
+
console.warn(`[nolag-agents] Capability '${capability}' has workers on agents-protocol < 2: ` +
|
|
917
|
+
capable.filter((a) => a.protocol < 2).map((a) => a.name).join(", ") +
|
|
918
|
+
". Their results may not be delivered — upgrade them.");
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return this._correlations.register(envelope.correlationId, options.timeout, `Task '${capability}' dispatch (${capable.length} capable worker${capable.length === 1 ? "" : "s"} visible). ` +
|
|
922
|
+
`Likely causes: worker crashed mid-task, worker on agents-protocol < 2 ` +
|
|
923
|
+
`(results not directed), or the room is not deliverable`);
|
|
814
924
|
}
|
|
815
925
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
* capabilities will be delivered to the handler. Non-matching tasks
|
|
821
|
-
* are silently ignored.
|
|
822
|
-
*
|
|
823
|
-
* @param capabilities - Array of capabilities this worker handles.
|
|
824
|
-
* Pass `'*'` to receive all tasks.
|
|
825
|
-
* @param handler - Async handler called with the task and a respond function.
|
|
826
|
-
*/
|
|
827
|
-
onTask(capabilities, handler) {
|
|
926
|
+
onTask(capabilitiesOrHandler, maybeHandler) {
|
|
927
|
+
// Single-arg form: onTask(handler) receives all tasks
|
|
928
|
+
const capabilities = typeof capabilitiesOrHandler === "function" ? '*' : capabilitiesOrHandler;
|
|
929
|
+
const handler = typeof capabilitiesOrHandler === "function" ? capabilitiesOrHandler : maybeHandler;
|
|
828
930
|
this._room.on("task", (task) => {
|
|
829
931
|
// Filter by capability unless wildcard
|
|
830
932
|
if (capabilities !== '*' && !capabilities.includes(task.capability)) {
|
|
831
933
|
return;
|
|
832
934
|
}
|
|
833
935
|
const respond = (status, payload, error) => {
|
|
834
|
-
const result = createResultEnvelope(task.taskId, task.correlationId, status, payload, error, this._room.agentId
|
|
936
|
+
const result = createResultEnvelope(task.taskId, task.correlationId, status, payload, error, this._room.agentId,
|
|
937
|
+
// Direct the result to the dispatcher's filter sub-topic
|
|
938
|
+
task.replyTo ?? task.createdBy);
|
|
835
939
|
this._room.publishResult(result);
|
|
836
940
|
};
|
|
837
941
|
handler(task, respond);
|
|
@@ -1030,6 +1134,7 @@ class Tools {
|
|
|
1030
1134
|
constructor(room, agentId) {
|
|
1031
1135
|
this._correlations = new CorrelationManager();
|
|
1032
1136
|
this._handlers = new Map();
|
|
1137
|
+
this._warnedMixed = false;
|
|
1033
1138
|
this._room = room;
|
|
1034
1139
|
this._agentId = agentId;
|
|
1035
1140
|
// Wire response correlation
|
|
@@ -1039,18 +1144,40 @@ class Tools {
|
|
|
1039
1144
|
// Wire request handling
|
|
1040
1145
|
this._room.on("toolRequest", async (envelope) => {
|
|
1041
1146
|
const handler = this._handlers.get(envelope.toolName);
|
|
1042
|
-
|
|
1147
|
+
// Direct the response back to the requester's filter sub-topic
|
|
1148
|
+
const replyTo = envelope.replyTo ?? envelope.requestedBy;
|
|
1149
|
+
if (!handler) {
|
|
1150
|
+
// Tool requests are load-balanced to EVERY group in the room, so
|
|
1151
|
+
// agents legitimately receive requests meant for other tool servers.
|
|
1152
|
+
// Stay silent unless this agent plausibly owns the tool:
|
|
1153
|
+
// - pure requesters (zero handlers) never answer
|
|
1154
|
+
// - servers answer only within their own namespace (the prefix
|
|
1155
|
+
// before the first '.', e.g. 'backend.*', 'chemistry.*') — a
|
|
1156
|
+
// 'backend.*' server NACKing 'chemistry.analyze' would race and
|
|
1157
|
+
// beat the real chemistry server's response
|
|
1158
|
+
if (!this._ownsNamespace(envelope.toolName))
|
|
1159
|
+
return;
|
|
1160
|
+
// A tool SERVER missing a handler in ITS OWN namespace NACKs instead
|
|
1161
|
+
// of silently ignoring — silence means the requester burns its full
|
|
1162
|
+
// timeout. (Requires homogeneous tool sets within a loadBalanceGroup
|
|
1163
|
+
// — see AGENTS-PROTOCOL.md.)
|
|
1164
|
+
const nack = createToolResponse(envelope.requestId, envelope.correlationId, "error", null, {
|
|
1165
|
+
code: "NO_HANDLER",
|
|
1166
|
+
message: `Agent '${this._agentId}' has no handler for tool '${envelope.toolName}'`,
|
|
1167
|
+
}, this._agentId, replyTo);
|
|
1168
|
+
this._room.publishTools(nack);
|
|
1043
1169
|
return;
|
|
1170
|
+
}
|
|
1044
1171
|
try {
|
|
1045
1172
|
const result = await handler(envelope.arguments);
|
|
1046
|
-
const response = createToolResponse(envelope.requestId, envelope.correlationId, "success", result, undefined, this._agentId);
|
|
1173
|
+
const response = createToolResponse(envelope.requestId, envelope.correlationId, "success", result, undefined, this._agentId, replyTo);
|
|
1047
1174
|
this._room.publishTools(response);
|
|
1048
1175
|
}
|
|
1049
1176
|
catch (err) {
|
|
1050
1177
|
const response = createToolResponse(envelope.requestId, envelope.correlationId, "error", null, {
|
|
1051
1178
|
code: "TOOL_ERROR",
|
|
1052
1179
|
message: err instanceof Error ? err.message : String(err),
|
|
1053
|
-
}, this._agentId);
|
|
1180
|
+
}, this._agentId, replyTo);
|
|
1054
1181
|
this._room.publishTools(response);
|
|
1055
1182
|
}
|
|
1056
1183
|
});
|
|
@@ -1061,13 +1188,57 @@ class Tools {
|
|
|
1061
1188
|
register(toolName, handler) {
|
|
1062
1189
|
this._handlers.set(toolName, handler);
|
|
1063
1190
|
}
|
|
1191
|
+
/** True when this agent hosts handlers in the tool's namespace (prefix
|
|
1192
|
+
* before the first '.'); unprefixed tools match any unprefixed handler. */
|
|
1193
|
+
_ownsNamespace(toolName) {
|
|
1194
|
+
if (this._handlers.size === 0)
|
|
1195
|
+
return false;
|
|
1196
|
+
const ns = toolName.includes(".") ? toolName.slice(0, toolName.indexOf(".")) : null;
|
|
1197
|
+
for (const name of this._handlers.keys()) {
|
|
1198
|
+
const handlerNs = name.includes(".") ? name.slice(0, name.indexOf(".")) : null;
|
|
1199
|
+
if (handlerNs === ns)
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1064
1204
|
/**
|
|
1065
1205
|
* Invoke a remote tool and wait for the response.
|
|
1066
1206
|
*/
|
|
1067
1207
|
async invoke(toolName, args, options) {
|
|
1068
|
-
|
|
1208
|
+
// Fail fast when the outcome is deterministic: tool servers are visible
|
|
1209
|
+
// in presence; if some exist and ALL advertise protocol < 2, their
|
|
1210
|
+
// replies cannot reach this requester. Mixed pools proceed with a
|
|
1211
|
+
// warning (presence is eventually consistent — hard-failing on one
|
|
1212
|
+
// stale entry would flake).
|
|
1213
|
+
const servers = this._room
|
|
1214
|
+
.getConnectedAgents()
|
|
1215
|
+
.filter((a) => a.role === "tool-server");
|
|
1216
|
+
if (!options?.allowLegacyResponders && servers.length > 0) {
|
|
1217
|
+
const modern = servers.filter((a) => a.protocol >= 2);
|
|
1218
|
+
if (modern.length === 0) {
|
|
1219
|
+
throw new IncompatibleProtocolError(`Tool '${toolName}' invocation`, servers.map((a) => ({ name: a.name, protocol: a.protocol })));
|
|
1220
|
+
}
|
|
1221
|
+
if (modern.length < servers.length && !this._warnedMixed) {
|
|
1222
|
+
this._warnedMixed = true;
|
|
1223
|
+
console.warn(`[nolag-agents] Room '${this._room.name}' has tool servers on agents-protocol < 2: ` +
|
|
1224
|
+
servers.filter((a) => a.protocol < 2).map((a) => a.name).join(", ") +
|
|
1225
|
+
". Their replies may not be delivered — upgrade them.");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
// replyTo is the room's agentId — the filter sub-topic this room's
|
|
1229
|
+
// results subscription listens on. (this._agentId may differ when a
|
|
1230
|
+
// caller attributes requests to a logical agent; delivery must use the
|
|
1231
|
+
// address that is actually subscribed.)
|
|
1232
|
+
const envelope = createToolRequest(toolName, args, this._agentId, {
|
|
1233
|
+
replyTo: this._room.agentId,
|
|
1234
|
+
});
|
|
1069
1235
|
this._room.publishTools(envelope);
|
|
1070
|
-
|
|
1236
|
+
const serverCount = servers.length;
|
|
1237
|
+
return this._correlations.register(envelope.correlationId, options?.timeout, `Tool '${toolName}' invocation in room '${this._room.name}' ` +
|
|
1238
|
+
`(${serverCount} tool-server${serverCount === 1 ? "" : "s"} visible). ` +
|
|
1239
|
+
`Likely causes: no agent has this tool registered (pre-0.3.0 responders ` +
|
|
1240
|
+
`don't NACK), the responder is offline, or the room is not deliverable ` +
|
|
1241
|
+
`(watch the room 'error' events)`);
|
|
1071
1242
|
}
|
|
1072
1243
|
/** Cancel all pending correlations */
|
|
1073
1244
|
dispose() {
|
|
@@ -1095,5 +1266,5 @@ function tag(prefix, value) {
|
|
|
1095
1266
|
return `${prefix}:${value}`;
|
|
1096
1267
|
}
|
|
1097
1268
|
|
|
1098
|
-
export { AgentRoom, Approve, Blackboard, CorrelationManager, EventEmitter, Handoff, Inbox, NoLagAgents, Observe, TAG_FLAGS, TAG_PREFIX, Tools, createApprovalRequest, createApprovalResponse, createEventEnvelope, createResultEnvelope, createStateEnvelope, createTaskEnvelope, createToolRequest, createToolResponse, tag };
|
|
1269
|
+
export { AGENTS_PROTOCOL_VERSION, AgentRoom, Approve, Blackboard, CorrelationManager, EventEmitter, Handoff, Inbox, IncompatibleProtocolError, NoLagAgents, Observe, TAG_FLAGS, TAG_PREFIX, Tools, createApprovalRequest, createApprovalResponse, createEventEnvelope, createResultEnvelope, createStateEnvelope, createTaskEnvelope, createToolRequest, createToolResponse, tag };
|
|
1099
1270
|
//# sourceMappingURL=index.mjs.map
|