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