viveworker 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -11
- package/package.json +11 -2
- package/scripts/a2a-cli.mjs +198 -0
- package/scripts/a2a-executor.mjs +126 -0
- package/scripts/a2a-handler.mjs +455 -0
- package/scripts/a2a-relay-client.mjs +394 -0
- package/scripts/moltbook-api.mjs +11 -6
- package/scripts/moltbook-cli.mjs +92 -22
- package/scripts/moltbook-scout-auto.sh +447 -0
- package/scripts/viveworker-bridge.mjs +272 -8
- package/scripts/viveworker.mjs +11 -0
- package/web/app.css +29 -2
- package/web/app.js +152 -8
- package/web/i18n.js +45 -9
- package/web/sw.js +1 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a2a-handler.mjs — Agent2Agent (A2A) Protocol handler for viveworker bridge.
|
|
3
|
+
*
|
|
4
|
+
* Implements a minimal A2A server (JSON-RPC 2.0) that:
|
|
5
|
+
* - Publishes an Agent Card at /.well-known/agent.json
|
|
6
|
+
* - Accepts tasks via message/send
|
|
7
|
+
* - Returns task status via tasks/get
|
|
8
|
+
* - Cancels tasks via tasks/cancel
|
|
9
|
+
*
|
|
10
|
+
* All tasks require user approval through the PWA before execution.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Agent Card
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export function buildAgentCard(config) {
|
|
20
|
+
const baseUrl = config.a2aPublicUrl || config.publicBaseUrl || `http://localhost:${config.port || 7860}`;
|
|
21
|
+
|
|
22
|
+
// Custom description from A2A_DESCRIPTION env var (set during setup)
|
|
23
|
+
const description = config.a2aDescription ||
|
|
24
|
+
"LAN-connected AI companion that bridges Codex and Claude Desktop. " +
|
|
25
|
+
"Can execute coding tasks, file operations, and code reviews with human approval.";
|
|
26
|
+
|
|
27
|
+
// Custom skills from A2A_SKILLS env var (comma-separated tags → skill objects)
|
|
28
|
+
let skills;
|
|
29
|
+
if (config.a2aSkills) {
|
|
30
|
+
skills = config.a2aSkills.split(",").map((s) => s.trim()).filter(Boolean).map((tag) => ({
|
|
31
|
+
id: tag,
|
|
32
|
+
name: tag,
|
|
33
|
+
description: tag,
|
|
34
|
+
}));
|
|
35
|
+
} else {
|
|
36
|
+
skills = [
|
|
37
|
+
{
|
|
38
|
+
id: "code-task",
|
|
39
|
+
name: "Code Task",
|
|
40
|
+
description: "Execute a coding task via Codex or Claude (with human approval)",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: { instruction: { type: "string" } },
|
|
44
|
+
required: ["instruction"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "code-review",
|
|
49
|
+
name: "Code Review",
|
|
50
|
+
description: "Review code or a pull request (with human approval)",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
target: { type: "string" },
|
|
55
|
+
context: { type: "string" },
|
|
56
|
+
},
|
|
57
|
+
required: ["target"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: "1.0",
|
|
65
|
+
humanReadableId: `viveworker/${config.a2aRelayUserId || "viveworker"}`,
|
|
66
|
+
agentVersion: config.version || "0.3.0",
|
|
67
|
+
name: "viveworker",
|
|
68
|
+
description,
|
|
69
|
+
url: `${baseUrl.replace(/\/$/u, "")}/a2a`,
|
|
70
|
+
provider: { name: "viveworker" },
|
|
71
|
+
capabilities: {
|
|
72
|
+
a2aVersion: "0.2.3",
|
|
73
|
+
streaming: false,
|
|
74
|
+
pushNotifications: false,
|
|
75
|
+
},
|
|
76
|
+
skills,
|
|
77
|
+
authSchemes: [{ scheme: "apiKey", in: "header", name: "X-A2A-Key" }],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Rate limiter (per-IP, in-memory)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const rateBuckets = new Map(); // ip → { count, resetAtMs }
|
|
86
|
+
const RATE_WINDOW_MS = 60_000;
|
|
87
|
+
const RATE_MAX = 10;
|
|
88
|
+
|
|
89
|
+
function checkRateLimit(ip) {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
let bucket = rateBuckets.get(ip);
|
|
92
|
+
if (!bucket || now >= bucket.resetAtMs) {
|
|
93
|
+
bucket = { count: 0, resetAtMs: now + RATE_WINDOW_MS };
|
|
94
|
+
rateBuckets.set(ip, bucket);
|
|
95
|
+
}
|
|
96
|
+
bucket.count += 1;
|
|
97
|
+
return bucket.count <= RATE_MAX;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Sweep stale buckets every 5 minutes
|
|
101
|
+
setInterval(() => {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
for (const [ip, bucket] of rateBuckets) {
|
|
104
|
+
if (now >= bucket.resetAtMs) rateBuckets.delete(ip);
|
|
105
|
+
}
|
|
106
|
+
}, 5 * 60_000).unref?.();
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Constants
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
const MAX_PENDING_TASKS = 5;
|
|
113
|
+
const MAX_BODY_BYTES = 10_240; // 10 KB
|
|
114
|
+
|
|
115
|
+
// A2A task statuses
|
|
116
|
+
const STATUS = {
|
|
117
|
+
SUBMITTED: "submitted",
|
|
118
|
+
WORKING: "working",
|
|
119
|
+
INPUT_REQUIRED: "input-required",
|
|
120
|
+
COMPLETED: "completed",
|
|
121
|
+
FAILED: "failed",
|
|
122
|
+
CANCELED: "canceled",
|
|
123
|
+
REJECTED: "rejected",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Helpers
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
function taskId() {
|
|
131
|
+
return crypto.randomUUID();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function jsonRpcResponse(id, result) {
|
|
135
|
+
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function jsonRpcError(id, code, message, data) {
|
|
139
|
+
const err = { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
|
|
140
|
+
if (data !== undefined) err.error.data = data;
|
|
141
|
+
return err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractTextFromParts(parts) {
|
|
145
|
+
if (!Array.isArray(parts)) return "";
|
|
146
|
+
return parts
|
|
147
|
+
.filter((p) => p && p.type === "text")
|
|
148
|
+
.map((p) => String(p.text || ""))
|
|
149
|
+
.join("\n")
|
|
150
|
+
.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildTaskResponse(task) {
|
|
154
|
+
return {
|
|
155
|
+
id: task.id,
|
|
156
|
+
contextId: task.contextId,
|
|
157
|
+
status: {
|
|
158
|
+
state: task.status,
|
|
159
|
+
...(task.statusMessage ? { message: { role: "agent", parts: [{ type: "text", text: task.statusMessage }] } } : {}),
|
|
160
|
+
},
|
|
161
|
+
...(task.artifacts.length > 0 ? { artifacts: task.artifacts } : {}),
|
|
162
|
+
history: task.messages,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// A2A request handler (called from bridge)
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {object} params
|
|
172
|
+
* @param {object} params.req - Node HTTP IncomingMessage
|
|
173
|
+
* @param {object} params.res - Node HTTP ServerResponse
|
|
174
|
+
* @param {object} params.body - Parsed JSON-RPC request body
|
|
175
|
+
* @param {object} params.config - Bridge config
|
|
176
|
+
* @param {object} params.runtime - Bridge runtime state (Maps)
|
|
177
|
+
* @param {object} params.state - Persistent state
|
|
178
|
+
* @param {Function} params.writeJson - writeJson(res, status, body)
|
|
179
|
+
* @param {Function} params.recordTimelineEntry - recordTimelineEntry({config,runtime,state,entry})
|
|
180
|
+
* @param {Function} params.deliverWebPushItem - deliverWebPushItem({config,state,...})
|
|
181
|
+
* @param {Function} params.saveState - saveState(stateFile, state)
|
|
182
|
+
* @param {Function} params.historyToken - historyToken(stableId) → token string
|
|
183
|
+
* @param {Function} params.cleanText - cleanText(value) → sanitised string
|
|
184
|
+
*/
|
|
185
|
+
export async function handleA2ARequest({
|
|
186
|
+
req,
|
|
187
|
+
res,
|
|
188
|
+
body,
|
|
189
|
+
config,
|
|
190
|
+
runtime,
|
|
191
|
+
state,
|
|
192
|
+
writeJson,
|
|
193
|
+
recordTimelineEntry,
|
|
194
|
+
deliverWebPushItem,
|
|
195
|
+
saveState,
|
|
196
|
+
historyToken,
|
|
197
|
+
cleanText,
|
|
198
|
+
}) {
|
|
199
|
+
// --- Auth ---
|
|
200
|
+
const apiKey = req.headers["x-a2a-key"] || "";
|
|
201
|
+
if (!config.a2aApiKey || apiKey !== config.a2aApiKey) {
|
|
202
|
+
return writeJson(res, 401, jsonRpcError(body?.id, -32000, "Unauthorized: invalid or missing X-A2A-Key"));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Rate limit ---
|
|
206
|
+
const clientIp = req.socket?.remoteAddress || "unknown";
|
|
207
|
+
if (!checkRateLimit(clientIp)) {
|
|
208
|
+
return writeJson(res, 429, jsonRpcError(body?.id, -32000, "Rate limit exceeded (max 10 req/min)"));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- JSON-RPC envelope validation ---
|
|
212
|
+
if (!body || body.jsonrpc !== "2.0" || typeof body.method !== "string") {
|
|
213
|
+
return writeJson(res, 400, jsonRpcError(body?.id, -32600, "Invalid JSON-RPC 2.0 request"));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const method = body.method;
|
|
217
|
+
const params = body.params || {};
|
|
218
|
+
const rpcId = body.id;
|
|
219
|
+
|
|
220
|
+
switch (method) {
|
|
221
|
+
case "message/send":
|
|
222
|
+
return handleMessageSend({ rpcId, params, req, config, runtime, state, res, writeJson, recordTimelineEntry, deliverWebPushItem, saveState, historyToken, cleanText });
|
|
223
|
+
case "tasks/get":
|
|
224
|
+
return handleTasksGet({ rpcId, params, runtime, res, writeJson });
|
|
225
|
+
case "tasks/cancel":
|
|
226
|
+
return handleTasksCancel({ rpcId, params, runtime, res, writeJson });
|
|
227
|
+
default:
|
|
228
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32601, `Method not found: ${method}`));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// message/send
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
async function handleMessageSend({
|
|
237
|
+
rpcId, params, req, config, runtime, state, res, writeJson,
|
|
238
|
+
recordTimelineEntry, deliverWebPushItem, saveState, historyToken, cleanText,
|
|
239
|
+
}) {
|
|
240
|
+
// Validate message
|
|
241
|
+
const message = params.message;
|
|
242
|
+
if (!message || !Array.isArray(message.parts) || message.parts.length === 0) {
|
|
243
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32602, "Invalid params: message.parts is required"));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const instruction = extractTextFromParts(message.parts);
|
|
247
|
+
if (!instruction) {
|
|
248
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32602, "Invalid params: no text content in message parts"));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check pending task limit
|
|
252
|
+
const pendingCount = [...runtime.a2aTasksByToken.values()].filter(
|
|
253
|
+
(t) => t.status === STATUS.SUBMITTED || t.status === STATUS.WORKING || t.status === STATUS.INPUT_REQUIRED
|
|
254
|
+
).length;
|
|
255
|
+
if (pendingCount >= MAX_PENDING_TASKS) {
|
|
256
|
+
return writeJson(res, 429, jsonRpcError(rpcId, -32000, `Too many pending tasks (max ${MAX_PENDING_TASKS})`));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Create task
|
|
260
|
+
const id = params.taskId || taskId();
|
|
261
|
+
const contextId = params.contextId || taskId();
|
|
262
|
+
const token = historyToken(`a2a_task:${id}`);
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
|
|
265
|
+
const task = {
|
|
266
|
+
id,
|
|
267
|
+
token,
|
|
268
|
+
contextId,
|
|
269
|
+
status: STATUS.SUBMITTED,
|
|
270
|
+
statusMessage: "",
|
|
271
|
+
messages: [message],
|
|
272
|
+
artifacts: [],
|
|
273
|
+
instruction,
|
|
274
|
+
callerInfo: {
|
|
275
|
+
ip: req.socket?.remoteAddress || "",
|
|
276
|
+
userAgent: req.headers["user-agent"] || "",
|
|
277
|
+
},
|
|
278
|
+
createdAtMs: now,
|
|
279
|
+
updatedAtMs: now,
|
|
280
|
+
decisionWaiters: [],
|
|
281
|
+
decision: null,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
runtime.a2aTasksByToken.set(token, task);
|
|
285
|
+
|
|
286
|
+
// Record timeline entry
|
|
287
|
+
try {
|
|
288
|
+
recordTimelineEntry({
|
|
289
|
+
config,
|
|
290
|
+
runtime,
|
|
291
|
+
state,
|
|
292
|
+
entry: {
|
|
293
|
+
stableId: `a2a_task:${id}`,
|
|
294
|
+
token,
|
|
295
|
+
kind: "a2a_task",
|
|
296
|
+
threadId: "a2a",
|
|
297
|
+
threadLabel: "A2A",
|
|
298
|
+
title: `A2A: ${cleanText(instruction).slice(0, 80)}`,
|
|
299
|
+
summary: cleanText(instruction).slice(0, 160),
|
|
300
|
+
messageText: instruction,
|
|
301
|
+
createdAtMs: now,
|
|
302
|
+
readOnly: false,
|
|
303
|
+
provider: "a2a",
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
await saveState(config.stateFile, state);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(`[a2a-timeline-save] ${error.message}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Send web push notification
|
|
312
|
+
try {
|
|
313
|
+
await deliverWebPushItem({
|
|
314
|
+
config,
|
|
315
|
+
state,
|
|
316
|
+
kind: "a2a_task",
|
|
317
|
+
token,
|
|
318
|
+
stableId: `a2a_task:${id}`,
|
|
319
|
+
title: "A2A Task Request",
|
|
320
|
+
body: cleanText(instruction).slice(0, 160),
|
|
321
|
+
buildLocalizedContent: ({ locale }) => {
|
|
322
|
+
const lang = locale?.startsWith("ja") ? "ja" : "en";
|
|
323
|
+
return {
|
|
324
|
+
title: lang === "ja" ? "🤝 外部エージェントからタスク依頼" : "🤝 Incoming A2A task request",
|
|
325
|
+
body: cleanText(instruction).slice(0, 160),
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error(`[a2a-push-error] ${error.message}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Return task in submitted state (caller will poll tasks/get)
|
|
334
|
+
return writeJson(res, 200, jsonRpcResponse(rpcId, buildTaskResponse(task)));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// tasks/get
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
function handleTasksGet({ rpcId, params, runtime, res, writeJson }) {
|
|
342
|
+
const id = params.taskId || params.id;
|
|
343
|
+
if (!id) {
|
|
344
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32602, "Invalid params: taskId is required"));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Find task by id
|
|
348
|
+
let task = null;
|
|
349
|
+
for (const t of runtime.a2aTasksByToken.values()) {
|
|
350
|
+
if (t.id === id) { task = t; break; }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!task) {
|
|
354
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32001, `Task not found: ${id}`));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const result = buildTaskResponse(task);
|
|
358
|
+
if (params.includeHistory === false) {
|
|
359
|
+
delete result.history;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return writeJson(res, 200, jsonRpcResponse(rpcId, result));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// tasks/cancel
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
function handleTasksCancel({ rpcId, params, runtime, res, writeJson }) {
|
|
370
|
+
const id = params.taskId || params.id;
|
|
371
|
+
if (!id) {
|
|
372
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32602, "Invalid params: taskId is required"));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let task = null;
|
|
376
|
+
for (const t of runtime.a2aTasksByToken.values()) {
|
|
377
|
+
if (t.id === id) { task = t; break; }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!task) {
|
|
381
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32001, `Task not found: ${id}`));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Only active tasks can be canceled
|
|
385
|
+
if ([STATUS.COMPLETED, STATUS.FAILED, STATUS.CANCELED, STATUS.REJECTED].includes(task.status)) {
|
|
386
|
+
return writeJson(res, 200, jsonRpcError(rpcId, -32000, `Task already in terminal state: ${task.status}`));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
task.status = STATUS.CANCELED;
|
|
390
|
+
task.statusMessage = "Canceled by caller";
|
|
391
|
+
task.updatedAtMs = Date.now();
|
|
392
|
+
|
|
393
|
+
// Notify any decision waiters
|
|
394
|
+
for (const waiter of task.decisionWaiters.splice(0)) {
|
|
395
|
+
try { waiter({ action: "cancel" }); } catch { /* ignore */ }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return writeJson(res, 200, jsonRpcResponse(rpcId, { success: true }));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Decision handler (called from bridge when user approves/denies via PWA)
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Process user decision on an A2A task.
|
|
407
|
+
* @param {object} task - A2A task object from runtime.a2aTasksByToken
|
|
408
|
+
* @param {object} decision - { action: "approve"|"deny", instruction?: string }
|
|
409
|
+
*/
|
|
410
|
+
export function resolveA2ATaskDecision(task, decision) {
|
|
411
|
+
if (!task) return;
|
|
412
|
+
task.decision = decision;
|
|
413
|
+
task.updatedAtMs = Date.now();
|
|
414
|
+
|
|
415
|
+
if (decision.action === "deny") {
|
|
416
|
+
task.status = STATUS.REJECTED;
|
|
417
|
+
task.statusMessage = "Task rejected by user";
|
|
418
|
+
} else if (decision.action === "approve") {
|
|
419
|
+
task.status = STATUS.WORKING;
|
|
420
|
+
task.statusMessage = "Task approved, executing...";
|
|
421
|
+
if (decision.instruction) {
|
|
422
|
+
task.instruction = decision.instruction;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Notify waiters
|
|
427
|
+
for (const waiter of task.decisionWaiters.splice(0)) {
|
|
428
|
+
try { waiter(decision); } catch { /* ignore */ }
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Mark task as completed with result artifacts.
|
|
434
|
+
*/
|
|
435
|
+
export function completeA2ATask(task, resultText) {
|
|
436
|
+
if (!task) return;
|
|
437
|
+
task.status = STATUS.COMPLETED;
|
|
438
|
+
task.statusMessage = "Task completed";
|
|
439
|
+
task.updatedAtMs = Date.now();
|
|
440
|
+
task.artifacts = [
|
|
441
|
+
{
|
|
442
|
+
parts: [{ type: "text", text: resultText }],
|
|
443
|
+
},
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Mark task as failed.
|
|
449
|
+
*/
|
|
450
|
+
export function failA2ATask(task, errorMessage) {
|
|
451
|
+
if (!task) return;
|
|
452
|
+
task.status = STATUS.FAILED;
|
|
453
|
+
task.statusMessage = errorMessage || "Task failed";
|
|
454
|
+
task.updatedAtMs = Date.now();
|
|
455
|
+
}
|