@tpsdev-ai/flair 0.3.17 → 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.
Files changed (39) hide show
  1. package/README.md +49 -33
  2. package/config.yaml +4 -2
  3. package/dist/cli.js +455 -20
  4. package/dist/resources/Memory.js +52 -3
  5. package/dist/resources/MemoryBootstrap.js +7 -1
  6. package/dist/resources/SemanticSearch.js +129 -56
  7. package/dist/resources/auth-middleware.js +37 -10
  8. package/dist/resources/content-safety.js +62 -0
  9. package/dist/resources/embeddings-provider.js +86 -7
  10. package/dist/resources/rate-limiter.js +118 -0
  11. package/package.json +2 -3
  12. package/resources/A2AAdapter.ts +0 -510
  13. package/resources/Agent.ts +0 -10
  14. package/resources/AgentCard.ts +0 -65
  15. package/resources/AgentSeed.ts +0 -119
  16. package/resources/IngestEvents.ts +0 -189
  17. package/resources/Integration.ts +0 -14
  18. package/resources/IssueTokens.ts +0 -29
  19. package/resources/Memory.ts +0 -151
  20. package/resources/MemoryBootstrap.ts +0 -323
  21. package/resources/MemoryConsolidate.ts +0 -121
  22. package/resources/MemoryFeed.ts +0 -48
  23. package/resources/MemoryMaintenance.ts +0 -95
  24. package/resources/MemoryReflect.ts +0 -122
  25. package/resources/OrgEvent.ts +0 -63
  26. package/resources/OrgEventCatchup.ts +0 -89
  27. package/resources/OrgEventMaintenance.ts +0 -37
  28. package/resources/SemanticSearch.ts +0 -197
  29. package/resources/SkillScan.ts +0 -146
  30. package/resources/Soul.ts +0 -10
  31. package/resources/SoulFeed.ts +0 -15
  32. package/resources/WorkspaceLatest.ts +0 -66
  33. package/resources/WorkspaceState.ts +0 -102
  34. package/resources/auth-middleware.ts +0 -501
  35. package/resources/embeddings-provider.ts +0 -61
  36. package/resources/embeddings.ts +0 -28
  37. package/resources/health.ts +0 -7
  38. package/resources/memory-feed-lib.ts +0 -22
  39. package/resources/table-helpers.ts +0 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.3.17",
3
+ "version": "0.4.0",
4
4
  "description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -29,7 +29,6 @@
29
29
  },
30
30
  "files": [
31
31
  "dist/",
32
- "resources/",
33
32
  "schemas/",
34
33
  "config.yaml",
35
34
  "LICENSE",
@@ -49,7 +48,7 @@
49
48
  "node": ">=22"
50
49
  },
51
50
  "dependencies": {
52
- "@harperfast/harper": "5.0.0-beta.4",
51
+ "@harperfast/harper": "5.0.0-beta.7",
53
52
  "commander": "14.0.3",
54
53
  "harper-fabric-embeddings": "^0.2.2",
55
54
  "tweetnacl": "1.0.3"
@@ -1,510 +0,0 @@
1
- import { Resource, databases } from "@harperfast/harper";
2
- import { access, readFile, readdir } from "node:fs/promises";
3
- import { constants } from "node:fs";
4
- import { basename, extname, join } from "node:path";
5
-
6
- type JsonRpcRequest = {
7
- jsonrpc: string;
8
- id?: string | number | null;
9
- method: string;
10
- params?: Record<string, any>;
11
- };
12
-
13
- type BeadsIssue = {
14
- id: string;
15
- title?: string;
16
- description?: string;
17
- notes?: string;
18
- status?: string;
19
- assignee?: string;
20
- updated_at?: string;
21
- created_at?: string;
22
- [key: string]: any;
23
- };
24
-
25
- const BEADS_ROOT = join(process.env.HOME || "/root", "ops", ".beads");
26
- const BEADS_ISSUES_DIR = join(BEADS_ROOT, "issues");
27
- const BEADS_ISSUES_JSONL = join(BEADS_ROOT, "issues.jsonl");
28
-
29
- function rpcResult(id: string | number | null | undefined, result: any) {
30
- return { jsonrpc: "2.0", id: id ?? null, result };
31
- }
32
-
33
- function rpcError(id: string | number | null | undefined, code: number, message: string, data?: any) {
34
- return {
35
- jsonrpc: "2.0",
36
- id: id ?? null,
37
- error: data === undefined ? { code, message } : { code, message, data },
38
- };
39
- }
40
-
41
- function cleanText(value: unknown): string {
42
- return String(value ?? "").trim();
43
- }
44
-
45
- function truncate(value: string, max: number): string {
46
- return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
47
- }
48
-
49
- function firstTextPart(message: any): string {
50
- const parts = Array.isArray(message?.parts) ? message.parts : [];
51
- for (const part of parts) {
52
- const text = cleanText(part?.text);
53
- if (text) return text;
54
- }
55
- return "";
56
- }
57
-
58
- function stripQuotes(value: string): string {
59
- if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
60
- return value.slice(1, -1);
61
- }
62
- return value;
63
- }
64
-
65
- function parseSimpleYamlIssue(raw: string, fallbackId: string): BeadsIssue {
66
- const issue: BeadsIssue = { id: fallbackId };
67
- const lines = raw.split(/\r?\n/);
68
- let listKey: string | null = null;
69
-
70
- for (const line of lines) {
71
- if (!line.trim() || line.trimStart().startsWith("#")) continue;
72
-
73
- const itemMatch = line.match(/^\s*-\s*(.+)\s*$/);
74
- if (itemMatch && listKey) {
75
- const current = issue[listKey];
76
- if (!Array.isArray(current)) issue[listKey] = [];
77
- issue[listKey].push(stripQuotes(itemMatch[1].trim()));
78
- continue;
79
- }
80
-
81
- if (/^\s/.test(line)) continue;
82
-
83
- const fieldMatch = line.match(/^([A-Za-z0-9_]+):\s*(.*)\s*$/);
84
- if (!fieldMatch) {
85
- listKey = null;
86
- continue;
87
- }
88
-
89
- const [, key, rawValue] = fieldMatch;
90
- if (rawValue === "") {
91
- issue[key] = issue[key] ?? "";
92
- listKey = key;
93
- continue;
94
- }
95
-
96
- if (rawValue === "[]" || rawValue === "[ ]") {
97
- issue[key] = [];
98
- listKey = null;
99
- continue;
100
- }
101
-
102
- if (rawValue === "|" || rawValue === ">") {
103
- issue[key] = "";
104
- listKey = null;
105
- continue;
106
- }
107
-
108
- issue[key] = stripQuotes(rawValue.trim());
109
- listKey = null;
110
- }
111
-
112
- if (!issue.id) issue.id = fallbackId;
113
- return issue;
114
- }
115
-
116
- async function pathExists(path: string): Promise<boolean> {
117
- try {
118
- await access(path, constants.F_OK);
119
- return true;
120
- } catch {
121
- return false;
122
- }
123
- }
124
-
125
- async function readIssuesFromYamlDir(): Promise<BeadsIssue[]> {
126
- if (!(await pathExists(BEADS_ISSUES_DIR))) return [];
127
-
128
- const entries = await readdir(BEADS_ISSUES_DIR, { withFileTypes: true });
129
- const out: BeadsIssue[] = [];
130
- for (const entry of entries) {
131
- if (!entry.isFile()) continue;
132
- const ext = extname(entry.name).toLowerCase();
133
- if (ext !== ".yaml" && ext !== ".yml") continue;
134
-
135
- const fullPath = join(BEADS_ISSUES_DIR, entry.name);
136
- const raw = await readFile(fullPath, "utf8");
137
- const fallbackId = basename(entry.name, ext);
138
- out.push(parseSimpleYamlIssue(raw, fallbackId));
139
- }
140
- return out;
141
- }
142
-
143
- async function readIssuesFromJsonl(): Promise<BeadsIssue[]> {
144
- if (!(await pathExists(BEADS_ISSUES_JSONL))) return [];
145
- const raw = await readFile(BEADS_ISSUES_JSONL, "utf8");
146
- const out: BeadsIssue[] = [];
147
- for (const line of raw.split(/\r?\n/)) {
148
- if (!line.trim()) continue;
149
- try {
150
- const parsed = JSON.parse(line);
151
- if (parsed?.id) out.push(parsed);
152
- } catch {
153
- // Ignore malformed lines.
154
- }
155
- }
156
- return out;
157
- }
158
-
159
- async function readIssue(taskId: string): Promise<BeadsIssue | null> {
160
- const yamlPath = join(BEADS_ISSUES_DIR, `${taskId}.yaml`);
161
- const ymlPath = join(BEADS_ISSUES_DIR, `${taskId}.yml`);
162
- for (const candidate of [yamlPath, ymlPath]) {
163
- if (await pathExists(candidate)) {
164
- const raw = await readFile(candidate, "utf8");
165
- return parseSimpleYamlIssue(raw, taskId);
166
- }
167
- }
168
-
169
- const jsonlIssues = await readIssuesFromJsonl();
170
- return jsonlIssues.find((issue) => issue.id === taskId) ?? null;
171
- }
172
-
173
- function mapBeadsStatusToA2A(statusRaw: unknown): string {
174
- const status = cleanText(statusRaw).toLowerCase();
175
- if (status === "ready" || status === "in_progress" || status === "open" || status === "active" || status === "todo") {
176
- return "working";
177
- }
178
- if (status === "done" || status === "closed" || status === "complete" || status === "completed") {
179
- return "completed";
180
- }
181
- if (status === "blocked") {
182
- return "input-required";
183
- }
184
- if (status === "cancelled" || status === "canceled") {
185
- return "canceled";
186
- }
187
- return "working";
188
- }
189
-
190
- function taskView(issue: BeadsIssue): any {
191
- return {
192
- id: issue.id,
193
- title: issue.title ?? "",
194
- status: mapBeadsStatusToA2A(issue.status),
195
- assignee: issue.assignee ?? null,
196
- updatedAt: issue.updated_at ?? issue.created_at ?? null,
197
- };
198
- }
199
-
200
-
201
- async function taskHistory(taskId: string): Promise<any[]> {
202
- const history: any[] = [];
203
- const refId = `bd://${taskId}`;
204
- for await (const event of (databases as any).flair.OrgEvent.search()) {
205
- if (event?.refId !== refId) continue;
206
- const summary = cleanText(event.summary);
207
- if (!summary) continue;
208
- history.push({
209
- createdAt: event.createdAt ?? "",
210
- role: "agent",
211
- parts: [{ text: summary }],
212
- });
213
- }
214
- history.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
215
- return history.map(({ role, parts }) => ({ role, parts }));
216
- }
217
-
218
- function artifactsFromIssue(issue: BeadsIssue): any[] {
219
- const artifacts: any[] = [];
220
- const notes = cleanText(issue.notes);
221
- if (notes) {
222
- artifacts.push({ name: "notes", parts: [{ text: notes }] });
223
- }
224
- return artifacts;
225
- }
226
-
227
- async function publishOrgEvent(event: any): Promise<void> {
228
- await (databases as any).flair.OrgEvent.put({
229
- id: event.id ?? `a2a-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
230
- authorId: event.authorId ?? "a2a",
231
- kind: event.kind,
232
- scope: event.scope ?? null,
233
- summary: event.summary,
234
- detail: event.detail ?? "",
235
- targetIds: event.targetIds ?? [],
236
- refId: event.refId ?? null,
237
- createdAt: event.createdAt ?? new Date().toISOString(),
238
- });
239
- }
240
-
241
- function parseJsonSafe(value: unknown): any | null {
242
- if (typeof value !== "string" || !value.trim()) return null;
243
- try {
244
- return JSON.parse(value);
245
- } catch {
246
- return null;
247
- }
248
- }
249
-
250
- function normalizeA2AStatus(statusRaw: unknown): string | null {
251
- const status = cleanText(statusRaw).toLowerCase();
252
- if (!status) return null;
253
- if (status === "done" || status === "closed" || status === "complete" || status === "completed") return "completed";
254
- if (status === "failed" || status === "error") return "failed";
255
- if (status === "cancelled" || status === "canceled") return "canceled";
256
- if (status === "working" || status === "in_progress" || status === "open" || status === "active" || status === "todo") {
257
- return "working";
258
- }
259
- return null;
260
- }
261
-
262
- function inferStatusFromText(textRaw: unknown): string | null {
263
- const text = cleanText(textRaw).toLowerCase();
264
- if (!text) return null;
265
- if (text.includes("completed") || text.includes("complete") || text.includes("done")) return "completed";
266
- if (text.includes("failed") || text.includes("error")) return "failed";
267
- if (text.includes("cancelled") || text.includes("canceled")) return "canceled";
268
- if (text.includes("working") || text.includes("started") || text.includes("in progress")) return "working";
269
- return null;
270
- }
271
-
272
- function taskIdFromEvent(event: any): string | null {
273
- const refId = cleanText(event?.refId);
274
- if (refId.startsWith("bd://")) {
275
- const taskId = cleanText(refId.slice("bd://".length));
276
- if (taskId) return taskId;
277
- }
278
- const detail = parseJsonSafe(event?.detail);
279
- const fromDetail = cleanText(detail?.taskId ?? detail?.id ?? detail?.task?.id);
280
- if (fromDetail) return fromDetail;
281
- return null;
282
- }
283
-
284
- function statusFromOrgEvent(event: any): string | null {
285
- const detail = parseJsonSafe(event?.detail);
286
- return (
287
- normalizeA2AStatus(detail?.status) ??
288
- normalizeA2AStatus(detail?.task?.status) ??
289
- normalizeA2AStatus(event?.status) ??
290
- normalizeA2AStatus(event?.kind) ??
291
- inferStatusFromText(event?.summary) ??
292
- null
293
- );
294
- }
295
-
296
- export class A2AAdapter extends Resource {
297
- async get() {
298
- const host = process.env.FLAIR_PUBLIC_URL || "http://localhost:9926";
299
- return new Response(JSON.stringify({
300
- name: "TPS Agent Team",
301
- description: "TPS — agent OS for humans and AI agents. Coordinates via Flair.",
302
- url: `${host}/a2a`,
303
- version: "0.1.0",
304
- capabilities: {
305
- streaming: true,
306
- pushNotifications: false,
307
- },
308
- defaultInputModes: ["text"],
309
- defaultOutputModes: ["text"],
310
- skills: [
311
- {
312
- id: "task-management",
313
- name: "Task Management",
314
- description: "Create, list, and track tasks via Beads issue tracker",
315
- },
316
- {
317
- id: "agent-coordination",
318
- name: "Agent Coordination",
319
- description: "Send messages to agents and coordinate work via OrgEvents",
320
- },
321
- ],
322
- }, null, 2), {
323
- status: 200,
324
- headers: { "Content-Type": "application/json" },
325
- });
326
- }
327
-
328
- async post(content: any, _context?: any) {
329
- const body: JsonRpcRequest = content as JsonRpcRequest;
330
-
331
- if (!body || typeof body !== "object") {
332
- return rpcError(null, -32600, "Invalid Request");
333
- }
334
- if (body.jsonrpc !== "2.0" || typeof body.method !== "string") {
335
- return rpcError(body.id, -32600, "Invalid Request");
336
- }
337
-
338
- const id = body.id ?? null;
339
- const params = body.params ?? {};
340
-
341
- try {
342
- if (body.method === "message/stream") {
343
- const agentId = cleanText(params.agentId);
344
- if (!agentId) {
345
- return rpcError(id, -32602, "Invalid params: agentId is required");
346
- }
347
-
348
- const taskIdHint = cleanText(params.taskId) || null;
349
- const encoder = new TextEncoder();
350
- const startedAt = Date.now();
351
- const timeoutMs = 5 * 60 * 1000;
352
- let lastSeen = new Date(startedAt).toISOString();
353
- let closed = false;
354
- const seenEventIds = new Set<string>();
355
- const seenEventQueue: string[] = [];
356
-
357
- const stream = new ReadableStream<Uint8Array>({
358
- start(controller) {
359
- const writeEvent = (eventName: string, payload: any) => {
360
- if (closed) return;
361
- const frame = `event: ${eventName}\ndata: ${JSON.stringify(payload)}\n\n`;
362
- controller.enqueue(encoder.encode(frame));
363
- };
364
-
365
- const closeStream = () => {
366
- if (closed) return;
367
- closed = true;
368
- clearInterval(pollTimer);
369
- clearTimeout(timeoutTimer);
370
- controller.close();
371
- };
372
-
373
- const poll = async () => {
374
- if (closed) return;
375
- if (Date.now() - startedAt >= timeoutMs) { closeStream(); return; }
376
-
377
- const catchupUrl =
378
- `http://localhost:9926/OrgEventCatchup/${encodeURIComponent(agentId)}?since=${lastSeen}`;
379
-
380
- let events: any[] = [];
381
- try {
382
- const response = await fetch(catchupUrl);
383
- if (!response.ok) return;
384
- const data = await response.json();
385
- if (Array.isArray(data)) events = data;
386
- else if (Array.isArray(data?.events)) events = data.events;
387
- } catch { return; }
388
-
389
- for (const event of events) {
390
- const eventId = cleanText(event?.id) || `${cleanText(event?.createdAt)}:${cleanText(event?.summary)}`;
391
- if (eventId && seenEventIds.has(eventId)) continue;
392
- if (eventId) {
393
- seenEventIds.add(eventId);
394
- seenEventQueue.push(eventId);
395
- if (seenEventQueue.length > 500) {
396
- const removed = seenEventQueue.shift();
397
- if (removed) seenEventIds.delete(removed);
398
- }
399
- }
400
- const createdAt = cleanText(event?.createdAt);
401
- if (createdAt && createdAt > lastSeen) lastSeen = createdAt;
402
-
403
- const status = statusFromOrgEvent(event);
404
- if (!status) continue;
405
-
406
- writeEvent("task.status", rpcResult(id, {
407
- type: "task",
408
- task: { id: taskIdFromEvent(event) ?? taskIdHint, status },
409
- }));
410
-
411
- if (status === "completed" || status === "failed" || status === "canceled") {
412
- closeStream(); return;
413
- }
414
- }
415
- };
416
-
417
- const pollTimer = setInterval(() => { void poll(); }, 2000);
418
- const timeoutTimer = setTimeout(() => { closeStream(); }, timeoutMs);
419
- void poll();
420
- },
421
- cancel() { closed = true; },
422
- });
423
-
424
- return new Response(stream, {
425
- headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
426
- });
427
- }
428
-
429
- if (body.method === "message/send") {
430
- const agentId = cleanText(params.agentId);
431
- const message = params.message;
432
- if (!agentId || !message || typeof message !== "object") {
433
- return rpcError(id, -32602, "Invalid params: agentId and message are required");
434
- }
435
-
436
- const agent = await (databases as any).flair.Agent.get(agentId).catch(() => null);
437
- if (!agent) {
438
- return rpcError(id, -32004, "Agent not found", { agentId });
439
- }
440
-
441
- const summary = truncate(firstTextPart(message) || "A2A message received", 200);
442
- await publishOrgEvent({
443
- kind: "a2a.message",
444
- scope: agentId,
445
- summary,
446
- detail: JSON.stringify({ message }),
447
- targetIds: [agentId],
448
- });
449
-
450
- return rpcResult(id, {
451
- type: "message",
452
- message: {
453
- role: "agent",
454
- parts: [{ text: "Message received. Task created." }],
455
- },
456
- });
457
- }
458
-
459
- if (body.method === "tasks/get") {
460
- const taskId = cleanText(params.taskId);
461
- if (!taskId) return rpcError(id, -32602, "Invalid params: taskId is required");
462
-
463
- const issue = await readIssue(taskId);
464
- if (!issue) return rpcError(id, -32004, "Task not found", { taskId });
465
-
466
- const history = await taskHistory(taskId);
467
- return rpcResult(id, {
468
- type: "task",
469
- task: {
470
- ...taskView(issue),
471
- artifacts: artifactsFromIssue(issue),
472
- history,
473
- },
474
- });
475
- }
476
-
477
- if (body.method === "tasks/list") {
478
- const yamlIssues = await readIssuesFromYamlDir();
479
- const issues = yamlIssues.length > 0 ? yamlIssues : await readIssuesFromJsonl();
480
-
481
- const agentIdFilter = cleanText(params.agentId).toLowerCase();
482
- const statusFilter = cleanText(params.status).toLowerCase();
483
-
484
- const tasks = issues
485
- .filter((issue) => {
486
- if (agentIdFilter) {
487
- const assignee = cleanText(issue.assignee).toLowerCase();
488
- if (assignee !== agentIdFilter) return false;
489
- }
490
- if (statusFilter) {
491
- const mapped = mapBeadsStatusToA2A(issue.status).toLowerCase();
492
- if (mapped !== statusFilter) return false;
493
- }
494
- return true;
495
- })
496
- .map((issue) => taskView(issue))
497
- .sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")));
498
-
499
- return rpcResult(id, { type: "tasks", tasks });
500
- }
501
-
502
- return rpcError(id, -32601, "Method not found");
503
- } catch (error: any) {
504
- return rpcError(id, -32000, "Server error", { detail: error?.message ?? String(error) });
505
- }
506
- }
507
- }
508
-
509
- // Expose exact lowercase endpoint required by A2A clients: POST /a2a
510
- export class a2a extends A2AAdapter {}
@@ -1,10 +0,0 @@
1
- import { databases } from "@harperfast/harper";
2
-
3
- export class Agent extends (databases as any).flair.Agent {
4
- async post(content: any, context: any) {
5
- content.type ||= "agent";
6
- content.createdAt = new Date().toISOString();
7
- content.updatedAt = content.createdAt;
8
- return super.post(content, context);
9
- }
10
- }
@@ -1,65 +0,0 @@
1
- import { Resource, databases } from "@harperfast/harper";
2
-
3
- type SoulLike = {
4
- key?: string;
5
- value?: string;
6
- kind?: string;
7
- content?: string;
8
- };
9
-
10
- function readSoulKind(entry: SoulLike): string {
11
- return String(entry.kind ?? entry.key ?? "").trim().toLowerCase();
12
- }
13
-
14
- function readSoulContent(entry: SoulLike): string {
15
- return String(entry.content ?? entry.value ?? "").trim();
16
- }
17
-
18
- export class AgentCard extends Resource {
19
- async get(pathInfo?: any) {
20
- const agentId =
21
- (typeof pathInfo === "string" ? pathInfo : null) ??
22
- (this as any).getId?.() ??
23
- null;
24
-
25
- if (!agentId) {
26
- return new Response(
27
- JSON.stringify({ error: "agentId required in path: GET /AgentCard/{agentId}" }),
28
- { status: 400, headers: { "Content-Type": "application/json" } },
29
- );
30
- }
31
-
32
- const agent = await (databases as any).flair.Agent.get(agentId).catch(() => null);
33
- if (!agent) {
34
- return new Response(JSON.stringify({ error: "agent_not_found", agentId }), {
35
- status: 404,
36
- headers: { "Content-Type": "application/json" },
37
- });
38
- }
39
-
40
- const souls: SoulLike[] = [];
41
- for await (const row of (databases as any).flair.Soul.search()) {
42
- if (row?.agentId === agentId) souls.push(row);
43
- }
44
-
45
- const descriptionEntry =
46
- souls.find((s) => readSoulKind(s) === "description" && readSoulContent(s)) ??
47
- souls.find((s) => readSoulContent(s));
48
-
49
- const skills = souls
50
- .filter((s) => readSoulKind(s) === "capability")
51
- .map((s) => readSoulContent(s))
52
- .filter(Boolean);
53
-
54
- return {
55
- name: String(agent.name ?? agent.id ?? agentId),
56
- description: descriptionEntry ? readSoulContent(descriptionEntry) : "",
57
- url: String(agent.url ?? ""),
58
- version: String(agent.version ?? "1.0.0"),
59
- capabilities: agent.capabilities && typeof agent.capabilities === "object" ? agent.capabilities : {},
60
- skills,
61
- defaultInputModes: ["text"],
62
- defaultOutputModes: ["text"],
63
- };
64
- }
65
- }