@tpsdev-ai/flair 0.2.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 (64) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +246 -0
  3. package/SECURITY.md +116 -0
  4. package/config.yaml +16 -0
  5. package/dist/resources/A2AAdapter.js +474 -0
  6. package/dist/resources/Agent.js +9 -0
  7. package/dist/resources/AgentCard.js +45 -0
  8. package/dist/resources/AgentSeed.js +111 -0
  9. package/dist/resources/IngestEvents.js +149 -0
  10. package/dist/resources/Integration.js +13 -0
  11. package/dist/resources/IssueTokens.js +19 -0
  12. package/dist/resources/Memory.js +122 -0
  13. package/dist/resources/MemoryBootstrap.js +263 -0
  14. package/dist/resources/MemoryConsolidate.js +105 -0
  15. package/dist/resources/MemoryFeed.js +41 -0
  16. package/dist/resources/MemoryReflect.js +105 -0
  17. package/dist/resources/OrgEvent.js +43 -0
  18. package/dist/resources/OrgEventCatchup.js +65 -0
  19. package/dist/resources/OrgEventMaintenance.js +29 -0
  20. package/dist/resources/SemanticSearch.js +147 -0
  21. package/dist/resources/SkillScan.js +101 -0
  22. package/dist/resources/Soul.js +9 -0
  23. package/dist/resources/SoulFeed.js +12 -0
  24. package/dist/resources/WorkspaceLatest.js +45 -0
  25. package/dist/resources/WorkspaceState.js +76 -0
  26. package/dist/resources/auth-middleware.js +470 -0
  27. package/dist/resources/embeddings-provider.js +127 -0
  28. package/dist/resources/embeddings.js +42 -0
  29. package/dist/resources/health.js +6 -0
  30. package/dist/resources/memory-feed-lib.js +15 -0
  31. package/dist/resources/table-helpers.js +35 -0
  32. package/package.json +62 -0
  33. package/resources/A2AAdapter.ts +510 -0
  34. package/resources/Agent.ts +10 -0
  35. package/resources/AgentCard.ts +65 -0
  36. package/resources/AgentSeed.ts +119 -0
  37. package/resources/IngestEvents.ts +189 -0
  38. package/resources/Integration.ts +14 -0
  39. package/resources/IssueTokens.ts +29 -0
  40. package/resources/Memory.ts +138 -0
  41. package/resources/MemoryBootstrap.ts +283 -0
  42. package/resources/MemoryConsolidate.ts +121 -0
  43. package/resources/MemoryFeed.ts +48 -0
  44. package/resources/MemoryReflect.ts +122 -0
  45. package/resources/OrgEvent.ts +63 -0
  46. package/resources/OrgEventCatchup.ts +89 -0
  47. package/resources/OrgEventMaintenance.ts +37 -0
  48. package/resources/SemanticSearch.ts +157 -0
  49. package/resources/SkillScan.ts +146 -0
  50. package/resources/Soul.ts +10 -0
  51. package/resources/SoulFeed.ts +15 -0
  52. package/resources/WorkspaceLatest.ts +66 -0
  53. package/resources/WorkspaceState.ts +102 -0
  54. package/resources/auth-middleware.ts +502 -0
  55. package/resources/embeddings-provider.ts +144 -0
  56. package/resources/embeddings.ts +28 -0
  57. package/resources/health.ts +7 -0
  58. package/resources/memory-feed-lib.ts +22 -0
  59. package/resources/table-helpers.ts +46 -0
  60. package/schemas/agent.graphql +22 -0
  61. package/schemas/event.graphql +12 -0
  62. package/schemas/memory.graphql +50 -0
  63. package/schemas/schema.graphql +41 -0
  64. package/schemas/workspace.graphql +14 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Fallback hash-based embedding (used when sidecar is unavailable).
3
+ * Real embeddings come from the embed-server sidecar (harper-fabric-embeddings).
4
+ */
5
+ const DIMS = 512;
6
+ function h1(s) { let h = 5381; for (let i = 0; i < s.length; i++)
7
+ h = ((h << 5) + h + s.charCodeAt(i)) >>> 0; return h % DIMS; }
8
+ function h2(s) { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) {
9
+ h ^= s.charCodeAt(i);
10
+ h = Math.imul(h, 0x01000193) >>> 0;
11
+ } return h % DIMS; }
12
+ export function fallbackEmbed(text) {
13
+ const tokens = text.toLowerCase().replace(/[^a-z0-9\s-]/g, ' ').split(/\s+/).filter(t => t.length > 1);
14
+ const clean = text.toLowerCase().replace(/\s+/g, ' ');
15
+ const vec = new Float64Array(DIMS);
16
+ for (const t of tokens) {
17
+ vec[h1(t)] += 2;
18
+ vec[h2(t)] += 1;
19
+ }
20
+ for (let i = 0; i < tokens.length - 1; i++)
21
+ vec[h1(tokens[i] + '_' + tokens[i + 1])] += 1.5;
22
+ for (let i = 0; i <= clean.length - 3; i++)
23
+ vec[h1(clean.slice(i, i + 3))] += 0.5;
24
+ for (let i = 0; i < DIMS; i++)
25
+ if (vec[i] > 0)
26
+ vec[i] = 1 + Math.log(vec[i]);
27
+ let norm = 0;
28
+ for (let i = 0; i < DIMS; i++)
29
+ norm += vec[i] * vec[i];
30
+ norm = Math.sqrt(norm);
31
+ if (norm > 0)
32
+ for (let i = 0; i < DIMS; i++)
33
+ vec[i] /= norm;
34
+ return Array.from(vec);
35
+ }
36
+ export function cosineSimilarity(a, b) {
37
+ let dot = 0;
38
+ const len = Math.min(a.length, b.length);
39
+ for (let i = 0; i < len; i++)
40
+ dot += a[i] * b[i];
41
+ return dot;
42
+ }
@@ -0,0 +1,6 @@
1
+ import { Resource } from "@harperfast/harper";
2
+ export class Health extends Resource {
3
+ async get() {
4
+ return { ok: true };
5
+ }
6
+ }
@@ -0,0 +1,15 @@
1
+ import { createHash } from "node:crypto";
2
+ export function computeContentHash(agentId, content) {
3
+ return createHash("sha256")
4
+ .update(`${agentId}${content}`)
5
+ .digest("hex")
6
+ .slice(0, 16);
7
+ }
8
+ export async function findExistingMemoryByContentHash(records, agentId, contentHash) {
9
+ for await (const record of records) {
10
+ if (record?.agentId === agentId && record?.contentHash === contentHash) {
11
+ return record;
12
+ }
13
+ }
14
+ return null;
15
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Safe read-modify-write helper for Harper tables.
3
+ *
4
+ * Harper's `put()` is FULL RECORD REPLACEMENT. If you pass a partial
5
+ * object, all missing fields (including embeddings!) are permanently
6
+ * deleted. This helper ensures you always read the full record first.
7
+ *
8
+ * Usage:
9
+ * import { patchRecord } from "./table-helpers.js";
10
+ * await patchRecord(tables.Memory, id, { lastReflected: now });
11
+ */
12
+ export async function patchRecord(table, id, patch) {
13
+ const existing = await table.get(id);
14
+ if (!existing)
15
+ throw new Error(`Record ${id} not found`);
16
+ await table.put({ ...existing, ...patch });
17
+ }
18
+ /**
19
+ * Fire-and-forget variant — swallows errors silently.
20
+ * Use for best-effort metadata updates (lastReflected, lastRetrieved, etc.)
21
+ * where a failure should never break the calling request.
22
+ */
23
+ export function patchRecordSilent(table, id, patch) {
24
+ patchRecord(table, id, patch).catch(() => { });
25
+ }
26
+ // ── RULE ──────────────────────────────────────────────────────────────────────
27
+ // Never call `tables.X.put(partial)` directly anywhere in Flair resources.
28
+ // Harper put() = FULL RECORD REPLACEMENT. Missing fields are deleted permanently.
29
+ // Always use patchRecord() or patchRecordSilent().
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // ── RULE ──────────────────────────────────────────────────────────────────────
32
+ // Never call `tables.X.put(partial)` directly anywhere in Flair resources.
33
+ // Harper put() = FULL RECORD REPLACEMENT. Missing fields are deleted permanently.
34
+ // Always use patchRecord() or patchRecordSilent().
35
+ // ─────────────────────────────────────────────────────────────────────────────
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@tpsdev-ai/flair",
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/tpsdev-ai/flair.git"
10
+ },
11
+ "homepage": "https://github.com/tpsdev-ai/flair",
12
+ "bugs": {
13
+ "url": "https://github.com/tpsdev-ai/flair/issues"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "agent",
18
+ "identity",
19
+ "memory",
20
+ "semantic-search",
21
+ "embeddings",
22
+ "ed25519",
23
+ "harper",
24
+ "llm",
25
+ "openclaw"
26
+ ],
27
+ "bin": {
28
+ "flair": "dist/cli.js"
29
+ },
30
+ "files": [
31
+ "dist/",
32
+ "resources/",
33
+ "schemas/",
34
+ "config.yaml",
35
+ "LICENSE",
36
+ "README.md",
37
+ "SECURITY.md"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json --noCheck",
41
+ "build:cli": "tsc -p tsconfig.cli.json --noCheck",
42
+ "test": "bun test"
43
+ },
44
+ "engines": {
45
+ "node": ">=22"
46
+ },
47
+ "dependencies": {
48
+ "@harperfast/harper": "5.0.0-beta.1",
49
+ "@node-llama-cpp/mac-arm64-metal": "^3.17.1",
50
+ "commander": "14.0.3",
51
+ "harper-fabric-embeddings": "^0.2.0",
52
+ "tweetnacl": "1.0.3"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "24.11.0",
56
+ "bun-types": "^1.3.10",
57
+ "typescript": "5.9.3"
58
+ },
59
+ "trustedDependencies": [
60
+ "harper-fabric-embeddings"
61
+ ]
62
+ }
@@ -0,0 +1,510 @@
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 {}
@@ -0,0 +1,10 @@
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
+ }