@slashfi/agents-sdk 0.68.0 → 0.69.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.
@@ -1,16 +1,25 @@
1
1
  /**
2
2
  * Ref Materialization — Download agent docs to local filesystem.
3
3
  *
4
- * When `adk ref add` succeeds, materialize the agent's tool schemas,
5
- * resources (skills), and generate TypeScript type definitions locally.
4
+ * When `adk ref add` or `adk sync` runs, materialize the agent's tool schemas,
5
+ * resources (skills), and generate readable markdown docs locally.
6
6
  *
7
- * Files are written under the adk config dir at refs/<name>/.
7
+ * Output format:
8
+ * refs/<name>/
9
+ * agent.json — metadata (name, description, tool list)
10
+ * entrypoint.md — agent overview with tool summary
11
+ * tools/<tool>.tool.md — per-tool docs with parameter tables
12
+ * tools/<tool>.tool.json — raw JSON schemas
13
+ * types/<name>.d.ts — TypeScript type stubs
14
+ * skills/ — resources from the agent
15
+ *
16
+ * The .tool.md files are the primary output — designed for LLMs to read.
8
17
  */
9
18
 
10
19
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
11
20
  import { dirname, join } from "node:path";
12
21
  import type { Adk } from "./config-store.js";
13
- // No init imports needed materialize is self-contained
22
+ import type { JsonSchema } from "./types.js";
14
23
 
15
24
  // ============================================
16
25
  // Types
@@ -19,18 +28,25 @@ import type { Adk } from "./config-store.js";
19
28
  interface ToolSchema {
20
29
  name: string;
21
30
  description?: string;
22
- inputSchema?: Record<string, unknown>;
31
+ inputSchema?: JsonSchema;
23
32
  outputSchema?: Record<string, unknown>;
24
33
  }
25
34
 
26
- interface MaterializeResult {
35
+ export interface MaterializeResult {
27
36
  toolCount: number;
28
37
  skillCount: number;
29
38
  typesGenerated: boolean;
39
+ docsGenerated: boolean;
40
+ }
41
+
42
+ export interface SyncResult {
43
+ refs: Array<{ name: string; result: MaterializeResult; error?: string }>;
44
+ totalTools: number;
45
+ totalSkills: number;
30
46
  }
31
47
 
32
48
  // ============================================
33
- // Helpers
49
+ // File Helpers
34
50
  // ============================================
35
51
 
36
52
  function ensureWrite(path: string, content: string): void {
@@ -41,27 +57,163 @@ function ensureWrite(path: string, content: string): void {
41
57
  writeFileSync(path, content, "utf-8");
42
58
  }
43
59
 
60
+ function toKebabCase(name: string): string {
61
+ return name
62
+ .replace(/[^a-zA-Z0-9]+/g, "-")
63
+ .replace(/^-|-$/g, "")
64
+ .toLowerCase();
65
+ }
66
+
67
+ function pascalCase(s: string): string {
68
+ return s
69
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
70
+ .replace(/^./, (c) => c.toUpperCase());
71
+ }
72
+
73
+ // ============================================
74
+ // JSON Schema → TypeScript type string
75
+ // ============================================
76
+
77
+ function jsonSchemaToTsType(schema: JsonSchema): string {
78
+ if (!schema) return "unknown";
79
+
80
+ const s = schema as any;
81
+
82
+ // const literal
83
+ if (s.const !== undefined) return JSON.stringify(s.const);
84
+
85
+ // enum
86
+ if (schema.enum) return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
87
+
88
+ // oneOf / anyOf → union
89
+ if (s.oneOf) return (s.oneOf as JsonSchema[]).map(jsonSchemaToTsType).join(" | ");
90
+ if (s.anyOf) return (s.anyOf as JsonSchema[]).map(jsonSchemaToTsType).join(" | ");
91
+
92
+ // allOf → intersection
93
+ if (s.allOf) return (s.allOf as JsonSchema[]).map(jsonSchemaToTsType).join(" & ");
94
+
95
+ // $ref
96
+ if (s.$ref) {
97
+ const match = (s.$ref as string).match(/\/([^/]+)$/);
98
+ return match ? match[1] : "unknown";
99
+ }
100
+
101
+ switch (schema.type) {
102
+ case "string":
103
+ return s.format ? `string /* ${s.format} */` : "string";
104
+ case "integer":
105
+ return "number /* integer */";
106
+ case "number":
107
+ return "number";
108
+ case "boolean":
109
+ return "boolean";
110
+ case "null":
111
+ return "null";
112
+ case "array":
113
+ return schema.items ? `${jsonSchemaToTsType(schema.items as JsonSchema)}[]` : "unknown[]";
114
+ case "object": {
115
+ if (schema.properties) {
116
+ const required = new Set((schema.required as string[]) ?? []);
117
+ const props = Object.entries(schema.properties)
118
+ .map(([k, v]) => `${k}${required.has(k) ? "" : "?"}: ${jsonSchemaToTsType(v as JsonSchema)}`)
119
+ .join("; ");
120
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
121
+ const addlType = jsonSchemaToTsType(schema.additionalProperties as JsonSchema);
122
+ return props ? `{ ${props}; [key: string]: ${addlType} }` : `Record<string, ${addlType}>`;
123
+ }
124
+ return `{ ${props} }`;
125
+ }
126
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
127
+ return `Record<string, ${jsonSchemaToTsType(schema.additionalProperties as JsonSchema)}>`;
128
+ }
129
+ return "Record<string, unknown>";
130
+ }
131
+ default:
132
+ if (Array.isArray(schema.type)) {
133
+ return (schema.type as string[]).map((t) => (t === "null" ? "null" : t === "integer" ? "number" : t)).join(" | ");
134
+ }
135
+ return "unknown";
136
+ }
137
+ }
138
+
139
+ // ============================================
140
+ // Markdown Generation
141
+ // ============================================
142
+
143
+ /** Generate a .tool.md file — readable tool docs with parameter tables. */
144
+ function generateToolMd(tool: ToolSchema): string {
145
+ const schema = tool.inputSchema ?? ({ type: "object", properties: {} } as JsonSchema);
146
+ const lines: string[] = [];
147
+
148
+ lines.push(`# ${tool.name}`);
149
+ lines.push("");
150
+ if (tool.description) {
151
+ lines.push(tool.description);
152
+ lines.push("");
153
+ }
154
+
155
+ const props = (schema.properties ?? {}) as Record<string, JsonSchema>;
156
+ const required = new Set((schema.required as string[]) ?? []);
157
+ const paramNames = Object.keys(props);
158
+
159
+ if (paramNames.length > 0) {
160
+ lines.push("## Parameters");
161
+ lines.push("");
162
+ lines.push("| Name | Type | Required | Description |");
163
+ lines.push("|------|------|----------|-------------|");
164
+ for (const name of paramNames) {
165
+ const prop = props[name];
166
+ const type = jsonSchemaToTsType(prop);
167
+ const req = required.has(name) ? "✓" : "";
168
+ const desc = (prop.description ?? "").replace(/\|/g, "\\|");
169
+ lines.push(`| ${name} | \`${type}\` | ${req} | ${desc} |`);
170
+ }
171
+ lines.push("");
172
+ }
173
+
174
+ return lines.join("\n");
175
+ }
176
+
177
+ /** Generate entrypoint.md — agent overview with tool listing. */
178
+ function generateEntrypoint(refName: string, description: string | undefined, tools: ToolSchema[]): string {
179
+ const lines: string[] = [];
180
+ lines.push(`# ${refName}`);
181
+ lines.push("");
182
+ if (description) {
183
+ lines.push(description);
184
+ lines.push("");
185
+ }
186
+ lines.push(`## Available Tools (${tools.length})`);
187
+ lines.push("");
188
+ for (const tool of tools) {
189
+ lines.push(`- **${tool.name}**: ${tool.description ?? "No description"}`);
190
+ }
191
+ lines.push("");
192
+ lines.push(`> Auto-generated by \`adk sync\` at ${new Date().toISOString()}`);
193
+ lines.push("");
194
+ return lines.join("\n");
195
+ }
196
+
44
197
  /** Generate a .d.ts file from tool schemas. */
45
198
  function generateTypes(refName: string, tools: ToolSchema[]): string {
46
199
  const lines: string[] = [
47
- `// Auto-generated by adk ref add`,
200
+ `// Auto-generated by adk sync`,
48
201
  `// Agent: ${refName}`,
49
202
  `// Tools: ${tools.length}`,
50
203
  ``,
51
204
  `export interface ${pascalCase(refName)}Tools {`,
52
205
  ];
53
-
54
206
  for (const tool of tools) {
207
+ const paramsType = tool.inputSchema
208
+ ? jsonSchemaToTsType(tool.inputSchema)
209
+ : "Record<string, unknown>";
55
210
  lines.push(` /** ${tool.description ?? tool.name} */`);
56
211
  lines.push(` ${JSON.stringify(tool.name)}: {`);
57
212
  lines.push(` name: ${JSON.stringify(tool.name)};`);
58
- if (tool.description) {
59
- lines.push(` description: ${JSON.stringify(tool.description)};`);
60
- }
61
- lines.push(` params: Record<string, unknown>;`);
213
+ if (tool.description) lines.push(` description: ${JSON.stringify(tool.description)};`);
214
+ lines.push(` params: ${paramsType};`);
62
215
  lines.push(` };`);
63
216
  }
64
-
65
217
  lines.push(`}`);
66
218
  lines.push(``);
67
219
  lines.push(`export declare const tools: (keyof ${pascalCase(refName)}Tools)[];`);
@@ -69,14 +221,8 @@ function generateTypes(refName: string, tools: ToolSchema[]): string {
69
221
  return lines.join("\n");
70
222
  }
71
223
 
72
- function pascalCase(s: string): string {
73
- return s
74
- .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
75
- .replace(/^./, (c) => c.toUpperCase());
76
- }
77
-
78
224
  // ============================================
79
- // Materialize
225
+ // Materialize a single ref
80
226
  // ============================================
81
227
 
82
228
  export async function materializeRef(
@@ -92,8 +238,9 @@ export async function materializeRef(
92
238
  let toolCount = 0;
93
239
  let skillCount = 0;
94
240
  let typesGenerated = false;
241
+ let docsGenerated = false;
95
242
 
96
- // 1. Fetch and write tool schemas
243
+ // 1. Fetch tool schemas and generate docs
97
244
  try {
98
245
  const info = await adk.ref.inspect(refName, { full: true });
99
246
  if (info?.tools && info.tools.length > 0) {
@@ -104,15 +251,18 @@ export async function materializeRef(
104
251
  outputSchema: t.outputSchema,
105
252
  }));
106
253
 
254
+ // Write .tool.md files (primary output — readable by LLMs)
107
255
  for (const tool of tools) {
108
- const safeName = tool.name.replace(/[^a-zA-Z0-9_-]/g, "_");
109
- ensureWrite(
110
- join(toolsDir, `${safeName}.tool.json`),
111
- JSON.stringify(tool, null, 2),
112
- );
256
+ const safeName = toKebabCase(tool.name);
257
+ ensureWrite(join(toolsDir, `${safeName}.tool.md`), generateToolMd(tool));
258
+ ensureWrite(join(toolsDir, `${safeName}.tool.json`), JSON.stringify(tool, null, 2));
113
259
  }
114
260
  toolCount = tools.length;
115
261
 
262
+ // Write entrypoint.md
263
+ ensureWrite(join(refDir, "entrypoint.md"), generateEntrypoint(refName, info.description, tools));
264
+ docsGenerated = true;
265
+
116
266
  // Write agent.json metadata
117
267
  ensureWrite(
118
268
  join(refDir, "agent.json"),
@@ -126,10 +276,7 @@ export async function materializeRef(
126
276
  );
127
277
 
128
278
  // Generate .d.ts
129
- ensureWrite(
130
- join(typesDir, `${refName}.d.ts`),
131
- generateTypes(refName, tools),
132
- );
279
+ ensureWrite(join(typesDir, `${refName}.d.ts`), generateTypes(refName, tools));
133
280
  typesGenerated = true;
134
281
  }
135
282
  } catch {
@@ -153,6 +300,69 @@ export async function materializeRef(
153
300
  // resources fetch failed — might not be supported
154
301
  }
155
302
 
156
- return { toolCount, skillCount, typesGenerated };
303
+ return { toolCount, skillCount, typesGenerated, docsGenerated };
157
304
  }
158
305
 
306
+ // ============================================
307
+ // Sync all refs from consumer config
308
+ // ============================================
309
+
310
+ const DEFAULT_CONCURRENCY = 3;
311
+
312
+ export async function syncAllRefs(
313
+ adk: Adk,
314
+ configDir: string,
315
+ opts?: { filter?: string; concurrency?: number; onProgress?: (name: string, status: string) => void },
316
+ ): Promise<SyncResult> {
317
+ const config = await adk.readConfig();
318
+ const refs = config.refs ?? [];
319
+
320
+ // Resolve ref names and filter
321
+ const names: string[] = [];
322
+ for (const refEntry of refs) {
323
+ const name = typeof refEntry === "string"
324
+ ? refEntry
325
+ : (refEntry as any).as ?? (refEntry as any).ref ?? (refEntry as any).name;
326
+ if (!name) continue;
327
+ if (opts?.filter && name !== opts.filter) continue;
328
+ names.push(name);
329
+ }
330
+
331
+ const results: SyncResult["refs"] = [];
332
+ let totalTools = 0;
333
+ let totalSkills = 0;
334
+ const concurrency = opts?.concurrency ?? DEFAULT_CONCURRENCY;
335
+
336
+ // Process in batches of `concurrency`
337
+ for (let i = 0; i < names.length; i += concurrency) {
338
+ const batch = names.slice(i, i + concurrency);
339
+ const batchResults = await Promise.allSettled(
340
+ batch.map(async (name) => {
341
+ opts?.onProgress?.(name, "syncing");
342
+ try {
343
+ const result = await materializeRef(adk, name, configDir);
344
+ opts?.onProgress?.(name, `done (${result.toolCount} tools)`);
345
+ return { name, result };
346
+ } catch (err: any) {
347
+ const error = err?.message ?? String(err);
348
+ opts?.onProgress?.(name, `error: ${error}`);
349
+ return {
350
+ name,
351
+ result: { toolCount: 0, skillCount: 0, typesGenerated: false, docsGenerated: false } as MaterializeResult,
352
+ error,
353
+ };
354
+ }
355
+ }),
356
+ );
357
+
358
+ for (const settled of batchResults) {
359
+ if (settled.status === "fulfilled") {
360
+ results.push(settled.value);
361
+ totalTools += settled.value.result.toolCount;
362
+ totalSkills += settled.value.result.skillCount;
363
+ }
364
+ }
365
+ }
366
+
367
+ return { refs: results, totalTools, totalSkills };
368
+ }
@@ -926,12 +926,21 @@ export async function createRegistryConsumer(
926
926
  // Parallel O(1) lookups via describe_tools
927
927
  const results = await Promise.allSettled(
928
928
  targetRegistries.map(async (registry) => {
929
- const data = (await callRegistry(registry, {
929
+ const raw = (await callRegistry(registry, {
930
930
  action: "describe_tools",
931
931
  path: agentPath,
932
932
  tools: undefined,
933
933
  ...(options?.full != null && { full: options.full }),
934
- })) as {
934
+ })) as unknown as Record<string, unknown> | null;
935
+ if (!raw) return null;
936
+
937
+ // Some endpoints (e.g. twin MCP) wrap the response in { success, result },
938
+ // while registries return fields flat. Unwrap if needed.
939
+ const data = (
940
+ !raw.toolSummaries && !raw.tools && raw.result && typeof raw.result === "object"
941
+ ? raw.result
942
+ : raw
943
+ ) as {
935
944
  tools?: unknown[];
936
945
  toolSummaries?: Array<{ name: string; description: string; fullTokens: number }>;
937
946
  description?: string;
@@ -940,8 +949,7 @@ export async function createRegistryConsumer(
940
949
  context?: string;
941
950
  upstream?: string;
942
951
  mode?: string;
943
- } | null;
944
- if (!data) return null;
952
+ };
945
953
  return {
946
954
  path: agentPath,
947
955
  publisher: registry.publisher,