@slashfi/agents-sdk 0.35.0 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/registry.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { dirname, resolve } from "node:path";
8
- import type { AgentEvent, BaseEvent, CustomEventMap, EventCallback, EventType } from "./events.js";
8
+ import type { AgentEvent, BaseEvent, CallAgentToolCallEvent, CustomEventMap, EventCallback, EventType, ListAgentsResult, ListAgentsToolCallEvent } from "./events.js";
9
9
  import { createEventBus } from "./events.js";
10
10
  import type { SerializedAgentDefinition } from "./serialized.js";
11
11
  import type {
@@ -96,6 +96,21 @@ export interface AgentRegistry {
96
96
  /** Call an agent (execute action) */
97
97
  call(request: CallAgentRequest): Promise<CallAgentResponse>;
98
98
 
99
+ /**
100
+ * List agents with hook support.
101
+ * Emits `tools/call/list_agents` event so hosts can inject additional agents
102
+ * (e.g., from remote registries or consumer config) before the callback runs.
103
+ *
104
+ * @param params - Query/pagination params from the MCP tool call
105
+ * @param callback - Processes the (possibly augmented) agent list into the final result.
106
+ * Receives the merged agent list; responsible for visibility, BM25, pagination.
107
+ * @returns The ListAgentsResult from either the callback or an intercepting listener
108
+ */
109
+ listAgents(
110
+ params: { query?: string; limit?: number; cursor?: string },
111
+ callback: (agents: AgentDefinition[]) => Promise<ListAgentsResult>,
112
+ ): Promise<ListAgentsResult>;
113
+
99
114
  /** Register an event listener (global scope — fires for all agents) */
100
115
  on<T extends EventType>(eventType: T, callback: EventCallback<T>): void;
101
116
 
@@ -183,6 +198,18 @@ export function agentFromSerialized(
183
198
  };
184
199
  }
185
200
 
201
+ /**
202
+ * Deduplicate agents by path. Later entries (from additionalAgents) override
203
+ * earlier ones from the base set.
204
+ */
205
+ function dedupeAgents(agents: AgentDefinition[]): AgentDefinition[] {
206
+ const seen = new Map<string, AgentDefinition>();
207
+ for (const agent of agents) {
208
+ seen.set(agent.path, agent);
209
+ }
210
+ return Array.from(seen.values());
211
+ }
212
+
186
213
  export function createAgentRegistry(
187
214
  options: AgentRegistryOptions = {},
188
215
  ): AgentRegistry {
@@ -484,6 +511,46 @@ export function createAgentRegistry(
484
511
  return Array.from(agents.keys());
485
512
  },
486
513
 
514
+ async listAgents(
515
+ params: { query?: string; limit?: number; cursor?: string },
516
+ callback: (agents: AgentDefinition[]) => Promise<ListAgentsResult>,
517
+ ): Promise<ListAgentsResult> {
518
+ const baseAgents = Array.from(agents.values());
519
+ let intercepted: ListAgentsResult | undefined;
520
+ let nextCalled = false;
521
+ let nextResult: ListAgentsResult | undefined;
522
+
523
+ const nextFn = async (additionalAgents?: AgentDefinition[]) => {
524
+ nextCalled = true;
525
+ const merged = additionalAgents
526
+ ? dedupeAgents([...baseAgents, ...additionalAgents])
527
+ : baseAgents;
528
+ nextResult = await callback(merged);
529
+ return nextResult;
530
+ };
531
+ const resolveFn = (result: ListAgentsResult) => {
532
+ intercepted = result;
533
+ };
534
+
535
+ await eventBus.emit({
536
+ type: "tools/call/list_agents",
537
+ agentPath: "*",
538
+ timestamp: Date.now(),
539
+ baseAgents,
540
+ query: params.query,
541
+ limit: params.limit,
542
+ cursor: params.cursor,
543
+ next: nextFn,
544
+ resolve: resolveFn,
545
+ } satisfies ListAgentsToolCallEvent);
546
+
547
+ if (intercepted) return intercepted;
548
+ if (nextCalled) return nextResult!;
549
+
550
+ // No listener engaged — run default with base agents
551
+ return callback(baseAgents);
552
+ },
553
+
487
554
  on<T extends EventType>(eventType: T, callback: EventCallback<T>): void {
488
555
  eventBus.on(eventType, callback);
489
556
  },
@@ -500,26 +567,32 @@ export function createAgentRegistry(
500
567
  },
501
568
 
502
569
  async call(request: CallAgentRequest): Promise<CallAgentResponse> {
503
- // Emit call event — listeners can next()/resolve() to control flow
570
+ // Emit tools/call/call_agent event — listeners can next()/resolve() to control flow
504
571
  let intercepted: CallAgentResponse | undefined;
505
572
  let nextCalled = false;
506
573
  let nextResult: CallAgentResponse | undefined;
574
+
575
+ const nextFn = async (overrideRequest?: CallAgentRequest) => {
576
+ nextCalled = true;
577
+ nextResult = await callInternal(overrideRequest ?? request);
578
+ return nextResult;
579
+ };
580
+ const resolveFn = (response: CallAgentResponse) => {
581
+ intercepted = response;
582
+ };
583
+
584
+ // Emit the new namespaced event
507
585
  await eventBus.emit({
508
- type: "call",
586
+ type: "tools/call/call_agent",
509
587
  agentPath: request.path,
510
588
  timestamp: Date.now(),
511
589
  request,
512
- async next(overrideRequest?: CallAgentRequest) {
513
- nextCalled = true;
514
- nextResult = await callInternal(overrideRequest ?? request);
515
- return nextResult;
516
- },
517
- resolve(response: CallAgentResponse) {
518
- intercepted = response;
519
- },
520
- });
590
+ next: nextFn,
591
+ resolve: resolveFn,
592
+ } satisfies CallAgentToolCallEvent);
521
593
  if (intercepted) return intercepted;
522
594
  if (nextCalled) return nextResult!;
595
+
523
596
  // No listener engaged — run default
524
597
  return callInternal(request);
525
598
  },
@@ -721,6 +794,15 @@ export function createAgentRegistry(
721
794
  return {
722
795
  success: true,
723
796
  tools: toolSchemas,
797
+ description: agent.config?.description,
798
+ security: agent.config?.security
799
+ ? { type: agent.config.security.type }
800
+ : undefined,
801
+ resources: agent.config?.resources?.map((r) => ({
802
+ uri: r.uri,
803
+ name: r.name,
804
+ mimeType: r.mimeType,
805
+ })),
724
806
  } as CallAgentDescribeToolsResponse;
725
807
  }
726
808
 
package/src/server.ts CHANGED
@@ -670,101 +670,108 @@ export function createAgentServer(
670
670
  limit: listLimit,
671
671
  cursor: listCursor,
672
672
  } = listAgentsValidate.parse(args);
673
- const agents = registry.list();
674
- let visible = agents.filter((agent) => canSeeAgent(agent, auth));
675
-
676
- // Decode cursor if provided
677
- const after = listCursor
678
- ? (JSON.parse(Buffer.from(listCursor, "base64url").toString()) as {
679
- path: string;
680
- score?: number;
681
- })
682
- : undefined;
683
-
684
- const pageSize = listLimit ?? 20;
685
- let page: typeof visible;
686
- let nextCursor: string | undefined;
687
-
688
- if (listQuery) {
689
- // BM25 search — ranked by score desc, path asc for tie-breaking
690
- const docs = visible.map((agent, i) => ({
691
- id: String(i),
692
- text: [
693
- agent.path,
694
- agent.config?.name ?? "",
695
- agent.config?.description ?? "",
696
- ...agent.tools
697
- .filter((t) => canSeeTool(t, auth))
698
- .map((t) => `${t.name} ${t.description}`),
699
- ].join(" "),
700
- }));
701
- const index = createBM25Index(docs);
702
- const ranked = index.search(listQuery);
703
-
704
- // Build scored list
705
- type ScoredAgent = (typeof visible)[number] & { _score: number };
706
- let scored: ScoredAgent[] = ranked.map((r) => ({
707
- ...visible[Number(r.id)],
708
- _score: r.score,
709
- }));
710
673
 
711
- // Apply cursor: skip past the after position
712
- if (after?.score !== undefined) {
713
- scored = scored.filter(
714
- (a) =>
715
- a._score < after.score! ||
716
- (a._score === after.score! && a.path > after.path),
717
- );
718
- }
674
+ const result = await registry.listAgents(
675
+ { query: listQuery, limit: listLimit, cursor: listCursor },
676
+ async (allAgents) => {
677
+ let visible = allAgents.filter((agent) => canSeeAgent(agent, auth));
678
+
679
+ // Decode cursor if provided
680
+ const after = listCursor
681
+ ? (JSON.parse(Buffer.from(listCursor, "base64url").toString()) as {
682
+ path: string;
683
+ score?: number;
684
+ })
685
+ : undefined;
686
+
687
+ const pageSize = listLimit ?? 20;
688
+ let page: typeof visible;
689
+ let nextCursor: string | undefined;
690
+
691
+ if (listQuery) {
692
+ // BM25 search — ranked by score desc, path asc for tie-breaking
693
+ const docs = visible.map((agent, i) => ({
694
+ id: String(i),
695
+ text: [
696
+ agent.path,
697
+ agent.config?.name ?? "",
698
+ agent.config?.description ?? "",
699
+ ...agent.tools
700
+ .filter((t) => canSeeTool(t, auth))
701
+ .map((t) => `${t.name} ${t.description}`),
702
+ ].join(" "),
703
+ }));
704
+ const index = createBM25Index(docs);
705
+ const ranked = index.search(listQuery);
706
+
707
+ // Build scored list
708
+ type ScoredAgent = (typeof visible)[number] & { _score: number };
709
+ let scored: ScoredAgent[] = ranked.map((r) => ({
710
+ ...visible[Number(r.id)],
711
+ _score: r.score,
712
+ }));
713
+
714
+ // Apply cursor: skip past the after position
715
+ if (after?.score !== undefined) {
716
+ scored = scored.filter(
717
+ (a) =>
718
+ a._score < after.score! ||
719
+ (a._score === after.score! && a.path > after.path),
720
+ );
721
+ }
719
722
 
720
- page = scored.slice(0, pageSize);
721
- if (scored.length > pageSize) {
722
- const last = scored[pageSize - 1] as ScoredAgent;
723
- nextCursor = Buffer.from(
724
- JSON.stringify({ path: last.path, score: last._score }),
725
- ).toString("base64url");
726
- }
727
- } else {
728
- // Alphabetical listing — sorted by path
729
- visible.sort((a, b) => a.path.localeCompare(b.path));
723
+ page = scored.slice(0, pageSize);
724
+ if (scored.length > pageSize) {
725
+ const last = scored[pageSize - 1] as ScoredAgent;
726
+ nextCursor = Buffer.from(
727
+ JSON.stringify({ path: last.path, score: last._score }),
728
+ ).toString("base64url");
729
+ }
730
+ } else {
731
+ // Alphabetical listing — sorted by path
732
+ visible.sort((a, b) => a.path.localeCompare(b.path));
730
733
 
731
- // Apply cursor: skip past afterPath
732
- if (after) {
733
- visible = visible.filter((a) => a.path > after.path);
734
- }
734
+ // Apply cursor: skip past afterPath
735
+ if (after) {
736
+ visible = visible.filter((a) => a.path > after.path);
737
+ }
735
738
 
736
- page = visible.slice(0, pageSize);
737
- if (visible.length > pageSize) {
738
- const last = page[page.length - 1];
739
- nextCursor = Buffer.from(
740
- JSON.stringify({ path: last.path }),
741
- ).toString("base64url");
742
- }
743
- }
739
+ page = visible.slice(0, pageSize);
740
+ if (visible.length > pageSize) {
741
+ const last = page[page.length - 1];
742
+ nextCursor = Buffer.from(
743
+ JSON.stringify({ path: last.path }),
744
+ ).toString("base64url");
745
+ }
746
+ }
744
747
 
745
- return mcpResult({
746
- success: true,
747
- total: agents.filter((a) => canSeeAgent(a, auth)).length,
748
- nextCursor,
749
- agents: page.map((agent) => ({
750
- path: agent.path,
751
- name: agent.config?.name,
752
- description: agent.config?.description,
753
- supportedActions: agent.config?.supportedActions,
754
- integration: agent.config?.integration || null,
755
- security: agent.config?.security
756
- ? { type: agent.config.security.type }
757
- : undefined,
758
- resources: agent.config?.resources?.map((r) => ({
759
- uri: r.uri,
760
- name: r.name,
761
- mimeType: r.mimeType,
762
- })),
763
- tools: agent.tools
764
- .filter((t) => canSeeTool(t, auth))
765
- .map((t) => t.name),
766
- })),
767
- });
748
+ return {
749
+ success: true as const,
750
+ total: allAgents.filter((a) => canSeeAgent(a, auth)).length,
751
+ nextCursor,
752
+ agents: page.map((agent) => ({
753
+ path: agent.path,
754
+ name: agent.config?.name,
755
+ description: agent.config?.description,
756
+ supportedActions: agent.config?.supportedActions,
757
+ integration: agent.config?.integration || null,
758
+ security: agent.config?.security
759
+ ? { type: agent.config.security.type }
760
+ : undefined,
761
+ resources: agent.config?.resources?.map((r) => ({
762
+ uri: r.uri,
763
+ name: r.name,
764
+ mimeType: r.mimeType,
765
+ })),
766
+ tools: agent.tools
767
+ .filter((t) => canSeeTool(t, auth))
768
+ .map((t) => t.name),
769
+ })),
770
+ };
771
+ },
772
+ );
773
+
774
+ return mcpResult(result);
768
775
  }
769
776
 
770
777
  default:
package/src/types.ts CHANGED
@@ -750,6 +750,12 @@ export interface CallAgentExecuteToolResponse {
750
750
  export interface CallAgentDescribeToolsResponse {
751
751
  success: true;
752
752
  tools: ToolSchema[];
753
+ /** Agent description */
754
+ description?: string;
755
+ /** Security scheme (if any) */
756
+ security?: SecuritySchemeSummary;
757
+ /** Available resources (e.g., AUTH.md) */
758
+ resources?: Array<{ uri: string; name?: string; mimeType?: string }>;
753
759
  }
754
760
 
755
761
  /** A resolved agent ref with its discovered tools */