@agentprojectcontext/apx 1.61.0 → 1.63.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/package.json +1 -1
- package/skills/apx/SKILL.md +5 -0
- package/src/core/agent/super-agent.js +7 -1
- package/src/core/agent/tools/handlers/call-runtime.js +185 -76
- package/src/core/channels/telegram/dispatch.js +24 -1
- package/src/core/channels/telegram/reply.js +79 -2
- package/src/core/integrations/plugins/asana.js +10 -30
- package/src/core/integrations/plugins/github.js +4 -16
- package/src/core/stores/runtime-callbacks.js +107 -0
- package/src/host/daemon/callback-reconciler.js +87 -0
- package/src/host/daemon/index.js +7 -0
- package/src/interfaces/cli/commands/exec.js +29 -2
- package/src/interfaces/cli/index.js +15 -2
- package/src/interfaces/web/dist/assets/{index-DRFIAiiq.js → index-CFcs16SV.js} +169 -152
- package/src/interfaces/web/dist/assets/{index-DRFIAiiq.js.map → index-CFcs16SV.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +3 -6
- package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +39 -26
- package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +7 -8
- package/src/interfaces/web/src/i18n/en.ts +57 -0
- package/src/interfaces/web/src/i18n/es.ts +57 -0
- package/src/interfaces/web/src/lib/api/integrations.ts +6 -10
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +20 -27
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +1 -20
package/package.json
CHANGED
package/skills/apx/SKILL.md
CHANGED
|
@@ -39,6 +39,11 @@ apx <command> --help
|
|
|
39
39
|
## Core commands you'll actually use
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
+
# One-shot super-agent call
|
|
43
|
+
apx exec "prompt" # default 'cli' channel
|
|
44
|
+
apx exec --code "prompt" # 'code' channel: coding system prompt + git/code tools
|
|
45
|
+
apx exec --channel <name> "…" # explicit channel (cli, code, api, …)
|
|
46
|
+
|
|
42
47
|
# Project + daemon
|
|
43
48
|
apx status # daemon health
|
|
44
49
|
apx project list # registered projects
|
|
@@ -60,6 +60,12 @@ export async function runSuperAgent({
|
|
|
60
60
|
// Null disables human-in-the-loop (tools that need confirmation fail
|
|
61
61
|
// immediately instead of waiting for user input).
|
|
62
62
|
requestConfirmation = null,
|
|
63
|
+
// A2A callback sink: when a background tool (call_runtime) finishes out of
|
|
64
|
+
// band, it feeds the result back into the super-agent via this function so
|
|
65
|
+
// the agent — not a raw dump — relays it to the user. Channel-specific
|
|
66
|
+
// (telegram wires it to a follow-up streamed turn). Null → tools fall back to
|
|
67
|
+
// a direct channel send. See call-runtime.js.
|
|
68
|
+
backgroundResultSink = null,
|
|
63
69
|
// When true, suppress the static "Available skills" slug-dump hint block
|
|
64
70
|
// because a per-turn skill inspector already injected the right context.
|
|
65
71
|
// Set by the daemon's super-agent endpoint when config.skills.inspector is on.
|
|
@@ -131,7 +137,7 @@ export async function runSuperAgent({
|
|
|
131
137
|
overrideModel,
|
|
132
138
|
toolSchemas,
|
|
133
139
|
makeToolHandlers,
|
|
134
|
-
toolHandlerCtx: { projects, plugins, registries, globalConfig, channel, toolSession, requestConfirmation },
|
|
140
|
+
toolHandlerCtx: { projects, plugins, registries, globalConfig, channel, channelMeta, toolSession, requestConfirmation, backgroundResultSink },
|
|
135
141
|
onEvent,
|
|
136
142
|
signal,
|
|
137
143
|
onToken,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createRuntimeSession,
|
|
9
9
|
extractRuntimeResult as extractApfResult,
|
|
10
10
|
} from "#core/stores/runtime-sessions.js";
|
|
11
|
+
import { writePendingCallback, deletePendingCallback } from "#core/stores/runtime-callbacks.js";
|
|
11
12
|
import { buildRuntimeBridgeHint as buildApfHint } from "#core/agent/runtime-bridge.js";
|
|
12
13
|
import { detectAll } from "#core/runtimes/detect.js";
|
|
13
14
|
import {
|
|
@@ -167,15 +168,38 @@ export default {
|
|
|
167
168
|
type: "string",
|
|
168
169
|
description: "Optional prior session id (claude/codex/apx) — APX prepends that session's title + last prompt to the prompt so the runtime has context.",
|
|
169
170
|
},
|
|
170
|
-
timeout_s: { type: "integer", description: "seconds before SIGTERM; default
|
|
171
|
+
timeout_s: { type: "integer", description: "seconds before SIGTERM. Foreground default 300; background runs default to 3600 (1h)." },
|
|
172
|
+
background: {
|
|
173
|
+
type: "boolean",
|
|
174
|
+
description: "Run detached instead of blocking this turn. On Telegram this is the DEFAULT (runtimes like claude-code can take many minutes to an hour; blocking would freeze the super-agent). Returns immediately with status:\"launched\"; when the runtime finishes, its result is delivered to this same chat as an automatic message. Set false ONLY when you genuinely need the runtime's output within THIS turn for an immediate follow-up (short tasks).",
|
|
175
|
+
},
|
|
171
176
|
},
|
|
172
177
|
required: ["runtime", "prompt"],
|
|
173
178
|
},
|
|
174
179
|
},
|
|
175
180
|
},
|
|
176
|
-
makeHandler: ({ projects, requirePermission }) => async ({ project, agent: slug, runtime, prompt, resume_session_id = null, timeout_s =
|
|
181
|
+
makeHandler: ({ projects, requirePermission, plugins, channel, channelMeta, backgroundResultSink = null }) => async ({ project, agent: slug, runtime, prompt, resume_session_id = null, timeout_s = null, background = null, confirmed = false }) => {
|
|
177
182
|
await requirePermission("call_runtime", { dangerous: true, confirmed, args: { runtime } });
|
|
178
183
|
|
|
184
|
+
// Async delivery sink: on Telegram we can push the runtime's result back to
|
|
185
|
+
// the originating chat when it finishes, so the call doesn't have to block
|
|
186
|
+
// the turn. channelMeta.chatId + the telegram plugin are what make that
|
|
187
|
+
// possible; without them there's nowhere to deliver a late result, so we
|
|
188
|
+
// stay synchronous (web/desktop/exec keep their current behavior).
|
|
189
|
+
const chatId = channelMeta?.chatId ?? null;
|
|
190
|
+
const tgChannelName = channelMeta?.channelName ?? null;
|
|
191
|
+
const telegramPlugin = channel === "telegram" && chatId != null ? plugins?.get?.("telegram") : null;
|
|
192
|
+
// We can report a late result either by re-entering the super-agent (A2A,
|
|
193
|
+
// preferred — the agent relays in its own voice) or by a direct channel
|
|
194
|
+
// send (fallback). Either makes background mode viable.
|
|
195
|
+
const canCallback = !!backgroundResultSink || !!telegramPlugin;
|
|
196
|
+
// Background by default when we can call back; the model opts out with
|
|
197
|
+
// background:false when it needs the output inside this same turn.
|
|
198
|
+
const runInBackground = canCallback && background !== false;
|
|
199
|
+
// Long runtimes (claude-code sessions) can run for an hour; a detached run
|
|
200
|
+
// must not be SIGTERM'd at the 5-min foreground default.
|
|
201
|
+
const effectiveTimeoutS = Number(timeout_s) || (runInBackground ? 3600 : 300);
|
|
202
|
+
|
|
179
203
|
const p = slug ? resolveProjectForAgent(projects, project, slug) : resolveProject(projects, project);
|
|
180
204
|
const agent = slug ? readAgents(p.path).find((a) => a.slug === slug) : null;
|
|
181
205
|
if (slug && !agent) {
|
|
@@ -226,106 +250,191 @@ export default {
|
|
|
226
250
|
project: p.path,
|
|
227
251
|
resume_session_id: resume_session_id || null,
|
|
228
252
|
resume_resolved: resume.meta ? `${resume.meta.engine}:${resume.meta.id}` : null,
|
|
229
|
-
timeout_s,
|
|
253
|
+
timeout_s: effectiveTimeoutS,
|
|
254
|
+
background: runInBackground,
|
|
230
255
|
});
|
|
231
256
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
257
|
+
// Run the runtime to completion and finalize (close the session record, log
|
|
258
|
+
// the transcript, shape the result object). NEVER throws — a thrown spawn is
|
|
259
|
+
// caught and returned as an error object, so the background path can deliver
|
|
260
|
+
// it as a callback instead of crashing an un-awaited promise.
|
|
261
|
+
const runToCompletion = async () => {
|
|
262
|
+
try {
|
|
263
|
+
const r = await rt.run({
|
|
264
|
+
system: buildRuntimeSystem(p, agent, runtime, session.id, "super_agent_tool"),
|
|
265
|
+
prompt: effectivePrompt,
|
|
266
|
+
cwd: p.path,
|
|
267
|
+
timeoutMs: effectiveTimeoutS * 1000,
|
|
268
|
+
});
|
|
239
269
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
const failure = runtimeLooksLikeFailure(r);
|
|
271
|
+
const result = extractApfResult(r.output) || (r.output || "").slice(0, 200);
|
|
272
|
+
closeRuntimeSession({
|
|
273
|
+
filePath: session.path,
|
|
274
|
+
externalSessionPath: r.externalSessionPath || null,
|
|
275
|
+
exitCode: failure.failed && r.exitCode === 0 ? -1 : r.exitCode,
|
|
276
|
+
result: failure.failed ? `failed: ${failure.reason}` : result,
|
|
277
|
+
});
|
|
248
278
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
p.logMessage({
|
|
280
|
+
agent_slug: actor,
|
|
281
|
+
channel: "runtime",
|
|
282
|
+
direction: "in",
|
|
283
|
+
author: "user",
|
|
284
|
+
body: effectivePrompt,
|
|
285
|
+
meta: { runtime, invoked_by: "super_agent_tool", apc_session: session.id, resume_session_id: resume_session_id || null },
|
|
286
|
+
});
|
|
287
|
+
p.logMessage({
|
|
288
|
+
agent_slug: actor,
|
|
289
|
+
channel: "runtime",
|
|
290
|
+
direction: "out",
|
|
291
|
+
type: "agent",
|
|
292
|
+
actor_id: agent?.slug || runtime,
|
|
293
|
+
actor_kind: agent?.slug ? "agent" : "engine",
|
|
294
|
+
author: agent?.slug || runtime,
|
|
295
|
+
body: r.output || "",
|
|
296
|
+
meta: {
|
|
297
|
+
runtime,
|
|
298
|
+
exit_code: r.exitCode,
|
|
299
|
+
external_session_path: r.externalSessionPath || null,
|
|
300
|
+
session_id: r.sessionId || null,
|
|
301
|
+
apc_session: session.id,
|
|
302
|
+
invoked_by: "super_agent_tool",
|
|
303
|
+
failed: failure.failed || false,
|
|
304
|
+
failure_reason: failure.failed ? failure.reason : null,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (failure.failed) {
|
|
309
|
+
log.error(`${runtime} run failed: ${failure.reason}`, {
|
|
310
|
+
apc_session: session.id,
|
|
311
|
+
exit_code: r.exitCode,
|
|
312
|
+
stderr: String(r.stderr || "").slice(0, 500),
|
|
313
|
+
external_session_path: r.externalSessionPath || null,
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
error: `runtime "${runtime}" did not complete successfully: ${failure.reason}`,
|
|
317
|
+
runtime,
|
|
318
|
+
agent: agent?.slug || null,
|
|
319
|
+
apc_session: session.id,
|
|
320
|
+
exit_code: r.exitCode,
|
|
321
|
+
stderr: (r.stderr || "").slice(0, 2000),
|
|
322
|
+
output: (r.output || "").slice(0, 2000),
|
|
323
|
+
external_session_path: r.externalSessionPath || null,
|
|
324
|
+
session_id: r.sessionId || null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
277
327
|
|
|
278
|
-
|
|
279
|
-
log.error(`${runtime} run failed: ${failure.reason}`, {
|
|
328
|
+
log.info(`${runtime} run ok`, {
|
|
280
329
|
apc_session: session.id,
|
|
281
330
|
exit_code: r.exitCode,
|
|
282
|
-
stderr: String(r.stderr || "").slice(0, 500),
|
|
283
331
|
external_session_path: r.externalSessionPath || null,
|
|
332
|
+
session_id: r.sessionId || null,
|
|
333
|
+
output_bytes: (r.output || "").length,
|
|
284
334
|
});
|
|
335
|
+
|
|
285
336
|
return {
|
|
286
|
-
error: `runtime "${runtime}" did not complete successfully: ${failure.reason}`,
|
|
287
337
|
runtime,
|
|
288
338
|
agent: agent?.slug || null,
|
|
289
339
|
apc_session: session.id,
|
|
290
340
|
exit_code: r.exitCode,
|
|
341
|
+
result,
|
|
342
|
+
output: (r.output || "").slice(0, 4000),
|
|
291
343
|
stderr: (r.stderr || "").slice(0, 2000),
|
|
292
|
-
|
|
344
|
+
truncated: (r.output || "").length > 4000,
|
|
293
345
|
external_session_path: r.externalSessionPath || null,
|
|
294
346
|
session_id: r.sessionId || null,
|
|
295
347
|
};
|
|
348
|
+
} catch (e) {
|
|
349
|
+
log.error(`${runtime} run threw: ${e.message}`, { apc_session: session.id });
|
|
350
|
+
try {
|
|
351
|
+
closeRuntimeSession({
|
|
352
|
+
filePath: session.path,
|
|
353
|
+
exitCode: -1,
|
|
354
|
+
result: `error: ${e.message.slice(0, 200)}`,
|
|
355
|
+
});
|
|
356
|
+
} catch {}
|
|
357
|
+
return { error: `runtime "${runtime}" threw: ${e.message}`, runtime, agent: agent?.slug || null, apc_session: session.id };
|
|
296
358
|
}
|
|
359
|
+
};
|
|
297
360
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
361
|
+
// Deliver a finished background run. Preferred path (A2A): feed the result
|
|
362
|
+
// back into the super-agent so it relays in its own voice / chains the next
|
|
363
|
+
// step — an internal agent-to-agent hand-off. Fallback: a direct channel
|
|
364
|
+
// send of the raw result. Best-effort: never throws (nothing awaits it).
|
|
365
|
+
const who = `${runtime}${agent ? ` (agente ${agent.slug})` : ""}`;
|
|
366
|
+
const deliverCallback = async (res) => {
|
|
367
|
+
// Take ownership of delivery: drop the durable IOU so the reconciler in
|
|
368
|
+
// another/next daemon can't also deliver this one. Fallbacks below still
|
|
369
|
+
// run in-process, so a delete here doesn't risk losing the callback.
|
|
370
|
+
deletePendingCallback(session.id);
|
|
371
|
+
const body = res.error ? "" : String(res.result || res.output || "").trim();
|
|
372
|
+
|
|
373
|
+
if (backgroundResultSink) {
|
|
374
|
+
// A2A report phrased for Roby (not the end user). Roby decides how to
|
|
375
|
+
// relay it and whether a next step is needed.
|
|
376
|
+
const report = res.error
|
|
377
|
+
? `[callback A2A] La tarea que delegaste a ${who} (sesión ${session.id}) FALLÓ: ${res.error}. ` +
|
|
378
|
+
`Avisale al usuario en tu voz y proponé cómo seguir.`
|
|
379
|
+
: `[callback A2A] Terminó la tarea que delegaste a ${who} (sesión ${session.id}). ` +
|
|
380
|
+
`Resultado del agente:\n${body.slice(0, 6000)}\n\n` +
|
|
381
|
+
`Contale al usuario el resultado en tu voz, breve y claro. No vuelvas a delegar salvo que falte un paso siguiente.`;
|
|
382
|
+
try {
|
|
383
|
+
await backgroundResultSink(report);
|
|
384
|
+
log.info(`${runtime} callback relayed via A2A sink`, { apc_session: session.id });
|
|
385
|
+
return;
|
|
386
|
+
} catch (e) {
|
|
387
|
+
log.error(`${runtime} A2A sink failed, falling back to direct send: ${e.message}`, { apc_session: session.id });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!telegramPlugin) return;
|
|
392
|
+
const head = res.error
|
|
393
|
+
? `⚠️ La sesión de ${who} terminó con error (sesión \`${session.id}\`): ${res.error}`
|
|
394
|
+
: `✅ Terminó la sesión de ${who} (\`${session.id}\`).`;
|
|
395
|
+
const text = body ? `${head}\n\n${body.slice(0, 3500)}` : head;
|
|
396
|
+
try {
|
|
397
|
+
await telegramPlugin.send({ channel: tgChannelName, chat_id: chatId, text });
|
|
398
|
+
log.info(`${runtime} callback delivered (direct)`, { apc_session: session.id, chat_id: chatId });
|
|
399
|
+
} catch (e) {
|
|
400
|
+
log.error(`${runtime} callback send failed: ${e.message}`, { apc_session: session.id });
|
|
401
|
+
}
|
|
402
|
+
};
|
|
305
403
|
|
|
404
|
+
if (runInBackground) {
|
|
405
|
+
// Durable IOU: if this daemon dies before the run finishes (crash, pull,
|
|
406
|
+
// or a task that restarts the daemon), the reconciler on the next daemon
|
|
407
|
+
// delivers the result from the session record. The in-process path below
|
|
408
|
+
// deletes it the moment it takes over. Only telegram delivery is wired.
|
|
409
|
+
if (chatId != null) {
|
|
410
|
+
writePendingCallback({
|
|
411
|
+
session_id: session.id,
|
|
412
|
+
session_path: session.path,
|
|
413
|
+
channel: "telegram",
|
|
414
|
+
chat_id: chatId,
|
|
415
|
+
tg_channel: tgChannelName,
|
|
416
|
+
runtime,
|
|
417
|
+
agent: agent?.slug || null,
|
|
418
|
+
who,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
// Fire-and-forget: don't await into the turn. runToCompletion never
|
|
422
|
+
// rejects, so this promise is safe un-awaited; the result is pushed to the
|
|
423
|
+
// chat when the runtime exits.
|
|
424
|
+
runToCompletion().then(deliverCallback);
|
|
306
425
|
return {
|
|
307
426
|
runtime,
|
|
308
427
|
agent: agent?.slug || null,
|
|
309
428
|
apc_session: session.id,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
429
|
+
status: "launched",
|
|
430
|
+
background: true,
|
|
431
|
+
note:
|
|
432
|
+
`La sesión de ${runtime} arrancó en segundo plano y puede tardar varios minutos u horas. ` +
|
|
433
|
+
`NO esperes ni vuelvas a llamar a call_runtime para esto: el resultado llegará AUTOMÁTICAMENTE a este chat cuando termine. ` +
|
|
434
|
+
`Respondé al usuario ahora, en una línea, avisándole que la lanzaste y que le vas a avisar acá cuando esté lista.`,
|
|
316
435
|
};
|
|
317
|
-
} catch (e) {
|
|
318
|
-
log.error(`${runtime} run threw: ${e.message}`, {
|
|
319
|
-
apc_session: session.id,
|
|
320
|
-
});
|
|
321
|
-
try {
|
|
322
|
-
closeRuntimeSession({
|
|
323
|
-
filePath: session.path,
|
|
324
|
-
exitCode: -1,
|
|
325
|
-
result: `error: ${e.message.slice(0, 200)}`,
|
|
326
|
-
});
|
|
327
|
-
} catch {}
|
|
328
|
-
throw e;
|
|
329
436
|
}
|
|
437
|
+
|
|
438
|
+
return await runToCompletion();
|
|
330
439
|
},
|
|
331
440
|
};
|
|
@@ -26,7 +26,7 @@ import * as askFlow from "./ask.js";
|
|
|
26
26
|
import { telegramAuthorLabel } from "./helpers.js";
|
|
27
27
|
import { handleIncomingPhoto } from "./inbound/photo.js";
|
|
28
28
|
import { handleIncomingAudio } from "./inbound/audio.js";
|
|
29
|
-
import { buildStreamHandler, runTelegramSuperAgent, telegramErrorText, sendFinalReply } from "./reply.js";
|
|
29
|
+
import { buildStreamHandler, runTelegramSuperAgent, telegramErrorText, sendFinalReply, runFollowupTurn } from "./reply.js";
|
|
30
30
|
import { t, resolveLang } from "#core/i18n/index.js";
|
|
31
31
|
|
|
32
32
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
@@ -285,6 +285,28 @@ export async function handleUpdate(self, u) {
|
|
|
285
285
|
// strip the prefix from the user prompt before sending to the loop.
|
|
286
286
|
const slashed = tryResolveSkillCommand(text, { projectPath: target?.path });
|
|
287
287
|
|
|
288
|
+
// A2A callback sink: when a background call_runtime finishes out of band,
|
|
289
|
+
// it invokes this to feed the sub-agent/runtime result back into a fresh
|
|
290
|
+
// super-agent turn — so Roby relays it in its own voice instead of dumping
|
|
291
|
+
// raw output. Self-referential so a relay turn that delegates again keeps
|
|
292
|
+
// the loop. Only wired when we have a chat to stream back to.
|
|
293
|
+
let backgroundResultSink = null;
|
|
294
|
+
if (chat_id) {
|
|
295
|
+
backgroundResultSink = async (reportText) =>
|
|
296
|
+
runFollowupTurn(self, {
|
|
297
|
+
chat_id,
|
|
298
|
+
reportText,
|
|
299
|
+
target,
|
|
300
|
+
author,
|
|
301
|
+
authorId: msg.from?.id,
|
|
302
|
+
relationshipBlock,
|
|
303
|
+
allowedTools,
|
|
304
|
+
agentDisplay,
|
|
305
|
+
update_id: u.update_id,
|
|
306
|
+
backgroundResultSink,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
288
310
|
try {
|
|
289
311
|
const sa = await runTelegramSuperAgent(self, {
|
|
290
312
|
chat_id,
|
|
@@ -298,6 +320,7 @@ export async function handleUpdate(self, u) {
|
|
|
298
320
|
contextNote: slashed.handled ? slashed.contextNote : "",
|
|
299
321
|
signal: abortCtrl.signal,
|
|
300
322
|
onEvent,
|
|
323
|
+
backgroundResultSink,
|
|
301
324
|
});
|
|
302
325
|
replyText = sa.text;
|
|
303
326
|
replyAuthor = sa.name || agentDisplay;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { runSuperAgent } from "#core/agent/super-agent.js";
|
|
9
9
|
import { TELEGRAM_TOOL_ITERS } from "#core/agent/constants.js";
|
|
10
10
|
import { stripThinking } from "#core/util/thinking.js";
|
|
11
|
-
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
11
|
+
import { appendGlobalMessage, getRecentTelegramTurnsFromFs } from "#core/stores/messages.js";
|
|
12
12
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
13
13
|
import { SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
|
|
14
14
|
import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
|
|
@@ -100,7 +100,7 @@ export function buildStreamHandler(self, { chat_id, update_id, agentDisplay }) {
|
|
|
100
100
|
*/
|
|
101
101
|
export function runTelegramSuperAgent(self, {
|
|
102
102
|
chat_id, prompt, previousMessages, target, author, authorId, relationshipBlock,
|
|
103
|
-
allowedTools, contextNote, signal, onEvent,
|
|
103
|
+
allowedTools, contextNote, signal, onEvent, backgroundResultSink = null,
|
|
104
104
|
}) {
|
|
105
105
|
const confirmAdapter = createTelegramConfirmAdapter({
|
|
106
106
|
token: resolveBotToken(self.channel),
|
|
@@ -130,6 +130,7 @@ export function runTelegramSuperAgent(self, {
|
|
|
130
130
|
signal,
|
|
131
131
|
onEvent,
|
|
132
132
|
requestConfirmation: confirmAdapter.requestConfirmation,
|
|
133
|
+
backgroundResultSink,
|
|
133
134
|
// Autonomy budget: Telegram is the "do the whole task for me" surface, so it
|
|
134
135
|
// gets a real multi-step budget instead of the conversational default (which
|
|
135
136
|
// cut tasks off after ~9 actions to ask "continue?"). Tunable via
|
|
@@ -138,6 +139,82 @@ export function runTelegramSuperAgent(self, {
|
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Run a follow-up super-agent turn triggered internally (not by an inbound
|
|
144
|
+
* message) — the A2A callback path. A background tool (call_runtime) finished
|
|
145
|
+
* out of band; `reportText` is the sub-agent/runtime result phrased as an
|
|
146
|
+
* internal report. We log it as a synthetic inbound so it lands in history,
|
|
147
|
+
* then run a normal streamed turn so Roby relays it to the user in its own
|
|
148
|
+
* voice (and can chain the next step). The same `backgroundResultSink` is
|
|
149
|
+
* forwarded so a relay turn that delegates again keeps the A2A loop intact.
|
|
150
|
+
* Best-effort: never throws (nothing awaits it).
|
|
151
|
+
*/
|
|
152
|
+
export async function runFollowupTurn(self, {
|
|
153
|
+
chat_id, reportText, target, author, authorId, relationshipBlock,
|
|
154
|
+
allowedTools, agentDisplay, update_id, backgroundResultSink = null,
|
|
155
|
+
}) {
|
|
156
|
+
if (!chat_id || !reportText) return;
|
|
157
|
+
try {
|
|
158
|
+
// Synthetic inbound so the report is part of the rolling history. Tagged
|
|
159
|
+
// a2a_callback + a distinct author so it reads as an internal hand-off, not
|
|
160
|
+
// a user turn.
|
|
161
|
+
appendGlobalMessage({
|
|
162
|
+
channel: CHANNELS.TELEGRAM,
|
|
163
|
+
direction: "in",
|
|
164
|
+
type: "user",
|
|
165
|
+
actor_id: "a2a",
|
|
166
|
+
external_id: `a2a-${update_id}-${chat_id}`,
|
|
167
|
+
author: "a2a",
|
|
168
|
+
body: reportText,
|
|
169
|
+
meta: { chat_id, tg_channel: self.channel.name, a2a_callback: true },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const previousMessages = getRecentTelegramTurnsFromFs({ chat_id, keepRecent: 40, max_age_hours: 24 });
|
|
173
|
+
const { onEvent, state } = buildStreamHandler(self, { chat_id, update_id, agentDisplay });
|
|
174
|
+
const stopTyping = self._startTyping(chat_id);
|
|
175
|
+
let replyText;
|
|
176
|
+
let replyAuthor;
|
|
177
|
+
let saUsage = null;
|
|
178
|
+
try {
|
|
179
|
+
const sa = await runTelegramSuperAgent(self, {
|
|
180
|
+
chat_id,
|
|
181
|
+
prompt: reportText,
|
|
182
|
+
previousMessages,
|
|
183
|
+
target,
|
|
184
|
+
author,
|
|
185
|
+
authorId,
|
|
186
|
+
relationshipBlock,
|
|
187
|
+
allowedTools,
|
|
188
|
+
onEvent,
|
|
189
|
+
backgroundResultSink,
|
|
190
|
+
});
|
|
191
|
+
replyText = sa.text;
|
|
192
|
+
replyAuthor = sa.name || agentDisplay;
|
|
193
|
+
saUsage = sa.usage;
|
|
194
|
+
} catch (e) {
|
|
195
|
+
self.log(`telegram[${self.channel.name}] a2a followup failed: ${e.message}`);
|
|
196
|
+
replyText = telegramErrorText(self, e);
|
|
197
|
+
replyAuthor = agentDisplay;
|
|
198
|
+
}
|
|
199
|
+
stopTyping();
|
|
200
|
+
await sendFinalReply(self, {
|
|
201
|
+
chat_id,
|
|
202
|
+
update_id,
|
|
203
|
+
replyText,
|
|
204
|
+
replyAuthor,
|
|
205
|
+
replyActorId: SUPERAGENT_ACTOR_ID,
|
|
206
|
+
replyKind: "superagent",
|
|
207
|
+
saUsage,
|
|
208
|
+
streamedCount: state.streamedCount,
|
|
209
|
+
lastStreamedText: state.lastStreamedText,
|
|
210
|
+
agentDisplay,
|
|
211
|
+
extraMeta: { a2a_relay: true },
|
|
212
|
+
});
|
|
213
|
+
} catch (e) {
|
|
214
|
+
self.log(`telegram[${self.channel.name}] a2a followup crashed: ${e.message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
141
218
|
/** Localized "couldn't reply" text for a failed super-agent turn (model itself
|
|
142
219
|
* failed, so it can't author this — templated, but follows the user's language). */
|
|
143
220
|
export function telegramErrorText(self, e) {
|
|
@@ -143,45 +143,25 @@ export const asanaPlugin = {
|
|
|
143
143
|
],
|
|
144
144
|
|
|
145
145
|
// Declarative UI descriptor consumed by the generic PluginConnect component
|
|
146
|
-
// (see web/components/integrations/PluginConnect.tsx).
|
|
147
|
-
//
|
|
148
|
-
//
|
|
146
|
+
// (see web/components/integrations/PluginConnect.tsx). STRUCTURE ONLY — all
|
|
147
|
+
// display text lives in the web i18n dictionaries under integrations.<slug>.*
|
|
148
|
+
// (keyed by field key). configFields render as inputs; `select` is a
|
|
149
|
+
// post-validate picker sourced from an action; connectedFields are the status
|
|
150
|
+
// keys shown once connected. `help_url`/`help_url_label` are non-translatable
|
|
151
|
+
// data kept here.
|
|
149
152
|
ui: {
|
|
150
153
|
accent: "rose",
|
|
151
154
|
configFields: [
|
|
152
155
|
{
|
|
153
156
|
key: "personal_access_token",
|
|
154
|
-
label: "Personal Access Token",
|
|
155
157
|
type: "password",
|
|
156
158
|
placeholder: "1/1234567890abcdef:...",
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
url: "https://app.asana.com/0/my-apps",
|
|
160
|
-
urlLabel: "app.asana.com/0/my-apps",
|
|
161
|
-
steps: [
|
|
162
|
-
"Abrí app.asana.com/0/my-apps en el navegador.",
|
|
163
|
-
'Bajá hasta la sección "Personal access tokens" (no tus apps OAuth).',
|
|
164
|
-
'Hacé clic en "+ New access token".',
|
|
165
|
-
"Dale un nombre y confirmá.",
|
|
166
|
-
'Copiá el token completo — empieza con "1/..." y tiene un ":" en el medio.',
|
|
167
|
-
"Pegalo en el campo de abajo.",
|
|
168
|
-
],
|
|
169
|
-
},
|
|
159
|
+
help_url: "https://app.asana.com/0/my-apps",
|
|
160
|
+
help_url_label: "app.asana.com/0/my-apps",
|
|
170
161
|
},
|
|
171
162
|
],
|
|
172
|
-
select: {
|
|
173
|
-
|
|
174
|
-
label: "Seleccioná el workspace a usar",
|
|
175
|
-
action: "workspaces",
|
|
176
|
-
listKey: "workspaces",
|
|
177
|
-
valueKey: "gid",
|
|
178
|
-
labelKey: "name",
|
|
179
|
-
},
|
|
180
|
-
connectedFields: [
|
|
181
|
-
{ key: "user_name", label: "Conectado como" },
|
|
182
|
-
{ key: "user_email", label: "Email" },
|
|
183
|
-
{ key: "workspace_name", label: "Workspace" },
|
|
184
|
-
],
|
|
163
|
+
select: { key: "workspace_gid", action: "workspaces", listKey: "workspaces", valueKey: "gid", labelKey: "name" },
|
|
164
|
+
connectedFields: ["user_name", "user_email", "workspace_name"],
|
|
185
165
|
},
|
|
186
166
|
|
|
187
167
|
// Save the PAT and/or the target workspace. Returns a patch to persist.
|
|
@@ -90,31 +90,19 @@ export const githubPlugin = {
|
|
|
90
90
|
{ slug: "github_list_repos", desc: "Listar repositorios accesibles" },
|
|
91
91
|
{ slug: "github_create_issue", desc: "Crear un issue en un repo" },
|
|
92
92
|
],
|
|
93
|
+
// Structure only — display text lives in web i18n (integrations.github.*).
|
|
93
94
|
ui: {
|
|
94
95
|
accent: "slate",
|
|
95
96
|
configFields: [
|
|
96
97
|
{
|
|
97
98
|
key: "token",
|
|
98
|
-
label: "Personal Access Token",
|
|
99
99
|
type: "password",
|
|
100
100
|
placeholder: "ghp_... o github_pat_...",
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
url: "https://github.com/settings/tokens",
|
|
104
|
-
urlLabel: "github.com/settings/tokens",
|
|
105
|
-
steps: [
|
|
106
|
-
"Abrí github.com/settings/tokens.",
|
|
107
|
-
'Generá un token (classic o fine-grained) con scope "repo".',
|
|
108
|
-
"Copiá el token — empieza con ghp_ o github_pat_.",
|
|
109
|
-
"Pegalo en el campo de abajo.",
|
|
110
|
-
],
|
|
111
|
-
},
|
|
101
|
+
help_url: "https://github.com/settings/tokens",
|
|
102
|
+
help_url_label: "github.com/settings/tokens",
|
|
112
103
|
},
|
|
113
104
|
],
|
|
114
|
-
connectedFields: [
|
|
115
|
-
{ key: "user_login", label: "Conectado como" },
|
|
116
|
-
{ key: "user_name", label: "Nombre" },
|
|
117
|
-
],
|
|
105
|
+
connectedFields: ["user_login", "user_name"],
|
|
118
106
|
},
|
|
119
107
|
|
|
120
108
|
configure(record, body = {}) {
|