@agentprojectcontext/apx 1.73.1 → 1.74.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/src/core/channels/telegram/ask-callbacks.js +4 -0
- package/src/core/channels/telegram/dispatch.js +13 -3
- package/src/core/channels/telegram/reply.js +19 -7
- package/src/core/net/ipv4-first.js +32 -0
- package/src/core/stores/messages.js +19 -2
- package/src/host/daemon/api/super-agent.js +35 -5
- package/src/host/daemon/api/voice.js +20 -1
- package/src/host/daemon/index.js +2 -0
- package/src/host/daemon/plugins/desktop/index.js +13 -2
- package/src/interfaces/cli/commands/exec.js +81 -3
- package/src/interfaces/web/dist/assets/index-BxYXJEtf.js +819 -0
- package/src/interfaces/web/dist/assets/index-BxYXJEtf.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-COrRuBp1.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +327 -324
- package/src/interfaces/web/src/components/chat/ContextBar.tsx +83 -28
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +23 -1
- package/src/interfaces/web/src/hooks/useChat.ts +37 -5
- package/src/interfaces/web/src/i18n/en.ts +3 -0
- package/src/interfaces/web/src/i18n/es.ts +3 -0
- package/src/interfaces/web/src/lib/api/agents.ts +2 -2
- package/src/interfaces/web/src/screens/base/LogsTab.tsx +19 -0
- package/src/interfaces/web/src/types/daemon.ts +11 -0
- package/src/interfaces/web/dist/assets/index-CUeIhw7z.css +0 -1
- package/src/interfaces/web/dist/assets/index-CqlRznhf.js +0 -819
- package/src/interfaces/web/dist/assets/index-CqlRznhf.js.map +0 -1
package/package.json
CHANGED
|
@@ -181,6 +181,7 @@ export async function runResumedTurn(self, ctx) {
|
|
|
181
181
|
let replyText;
|
|
182
182
|
let replyAuthor;
|
|
183
183
|
let saUsage = null;
|
|
184
|
+
let saModel = null;
|
|
184
185
|
try {
|
|
185
186
|
const sa = await runTelegramSuperAgent(self, {
|
|
186
187
|
chat_id,
|
|
@@ -216,10 +217,12 @@ export async function runResumedTurn(self, ctx) {
|
|
|
216
217
|
replyText = sa.text;
|
|
217
218
|
replyAuthor = sa.name || agentDisplay;
|
|
218
219
|
saUsage = sa.usage;
|
|
220
|
+
saModel = sa.model || state.model || null;
|
|
219
221
|
} catch (e) {
|
|
220
222
|
self.log(`telegram[${self.channel.name}] ask resume failed: ${e.message}`);
|
|
221
223
|
replyText = telegramErrorText(self, e);
|
|
222
224
|
replyAuthor = agentDisplay;
|
|
225
|
+
saModel = state.model || null;
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
stopTyping();
|
|
@@ -231,6 +234,7 @@ export async function runResumedTurn(self, ctx) {
|
|
|
231
234
|
replyActorId: SUPERAGENT_ACTOR_ID,
|
|
232
235
|
replyKind: "superagent",
|
|
233
236
|
saUsage,
|
|
237
|
+
saModel,
|
|
234
238
|
streamedCount: state.streamedCount,
|
|
235
239
|
lastStreamedText: state.lastStreamedText,
|
|
236
240
|
agentDisplay,
|
|
@@ -226,6 +226,8 @@ export async function handleUpdate(self, u) {
|
|
|
226
226
|
let replyAuthor;
|
|
227
227
|
let replyActorId = SUPERAGENT_ACTOR_ID; // stable id: super_agent | agent slug
|
|
228
228
|
let replyKind = "superagent"; // actor_kind: superagent | agent
|
|
229
|
+
let replyModel = null; // model that actually produced the reply
|
|
230
|
+
let replyUsage = null; // token accounting for this turn
|
|
229
231
|
const projectCfg = target.config || self.globalConfig;
|
|
230
232
|
// Display name for the super-agent persona on this channel (from identity.json).
|
|
231
233
|
const agentDisplay = resolveAgentName(self.globalConfig);
|
|
@@ -254,6 +256,10 @@ export async function handleUpdate(self, u) {
|
|
|
254
256
|
replyAuthor = agent.slug;
|
|
255
257
|
replyActorId = agent.slug;
|
|
256
258
|
replyKind = "agent";
|
|
259
|
+
// Fully-qualified id from the agent card (provider:model) — callEngine
|
|
260
|
+
// resolves it internally and doesn't hand it back.
|
|
261
|
+
replyModel = agent.fields.Model;
|
|
262
|
+
replyUsage = result.usage || null;
|
|
257
263
|
} catch (e) {
|
|
258
264
|
self.log(`telegram[${self.channel.name}] agent reply failed: ${e.message}`);
|
|
259
265
|
replyText = t("telegram.error_agent", {
|
|
@@ -263,6 +269,8 @@ export async function handleUpdate(self, u) {
|
|
|
263
269
|
replyAuthor = agentDisplay;
|
|
264
270
|
replyActorId = SUPERAGENT_ACTOR_ID;
|
|
265
271
|
replyKind = "superagent";
|
|
272
|
+
replyModel = null;
|
|
273
|
+
replyUsage = null;
|
|
266
274
|
}
|
|
267
275
|
} else {
|
|
268
276
|
self.log(
|
|
@@ -276,7 +284,6 @@ export async function handleUpdate(self, u) {
|
|
|
276
284
|
// calls are logged but never sent (internal). The streamed turn + its final
|
|
277
285
|
// send live in ./reply.js so this dispatcher and the ask-flow resume
|
|
278
286
|
// (_runResumedTurn in the host poller) share ONE reply path — no drift.
|
|
279
|
-
let saUsage = null;
|
|
280
287
|
let streamedCount = 0;
|
|
281
288
|
let lastStreamedText = "";
|
|
282
289
|
if (!replyText && isSuperAgentEnabled(self.globalConfig)) {
|
|
@@ -327,7 +334,8 @@ export async function handleUpdate(self, u) {
|
|
|
327
334
|
replyAuthor = sa.name || agentDisplay;
|
|
328
335
|
replyActorId = SUPERAGENT_ACTOR_ID;
|
|
329
336
|
replyKind = "superagent";
|
|
330
|
-
|
|
337
|
+
replyUsage = sa.usage;
|
|
338
|
+
replyModel = sa.model || state.model || null;
|
|
331
339
|
|
|
332
340
|
// ── ask_questions integration ────────────────────────────────────
|
|
333
341
|
// If the super-agent ended this turn by calling ask_questions, hand off
|
|
@@ -374,6 +382,7 @@ export async function handleUpdate(self, u) {
|
|
|
374
382
|
replyAuthor = agentDisplay;
|
|
375
383
|
replyActorId = SUPERAGENT_ACTOR_ID;
|
|
376
384
|
replyKind = "superagent";
|
|
385
|
+
replyModel = state.model || null;
|
|
377
386
|
}
|
|
378
387
|
}
|
|
379
388
|
|
|
@@ -386,7 +395,8 @@ export async function handleUpdate(self, u) {
|
|
|
386
395
|
replyAuthor,
|
|
387
396
|
replyActorId,
|
|
388
397
|
replyKind,
|
|
389
|
-
saUsage,
|
|
398
|
+
saUsage: replyUsage,
|
|
399
|
+
saModel: replyModel,
|
|
390
400
|
streamedCount,
|
|
391
401
|
lastStreamedText,
|
|
392
402
|
agentDisplay,
|
|
@@ -23,12 +23,19 @@ import { buildTelegramMeta, resolveBotToken } from "./helpers.js";
|
|
|
23
23
|
* (audit trail / other channels — never sent to Telegram). Returns the handler
|
|
24
24
|
* plus a live `state` the caller reads AFTER the run to drive the final send.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* `state.model` tracks the model actually answering RIGHT NOW (it can rotate
|
|
27
|
+
* mid-turn on a fallback), so every record this handler writes is stamped with
|
|
28
|
+
* the model that produced it — not with the one the turn happened to end on.
|
|
29
|
+
*
|
|
30
|
+
* @returns {{ onEvent: Function, state: { streamedCount: number, lastStreamedText: string, model: string } }}
|
|
27
31
|
*/
|
|
28
32
|
export function buildStreamHandler(self, { chat_id, update_id, agentDisplay }) {
|
|
29
|
-
const state = { streamedCount: 0, lastStreamedText: "", sentHeadsUp: false };
|
|
33
|
+
const state = { streamedCount: 0, lastStreamedText: "", sentHeadsUp: false, model: "" };
|
|
30
34
|
const onEvent = async (ev) => {
|
|
31
35
|
try {
|
|
36
|
+
if ((ev.type === "model_start" || ev.type === "model_routed" || ev.type === "final_wrapup") && ev.model) {
|
|
37
|
+
state.model = ev.model;
|
|
38
|
+
}
|
|
32
39
|
if (ev.type === "tool_start" && !state.sentHeadsUp && state.streamedCount === 0) {
|
|
33
40
|
state.sentHeadsUp = true;
|
|
34
41
|
const heads = t("telegram.heads_up", { lang: resolveLang(self.globalConfig) });
|
|
@@ -42,7 +49,7 @@ export function buildStreamHandler(self, { chat_id, update_id, agentDisplay }) {
|
|
|
42
49
|
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
43
50
|
author: agentDisplay,
|
|
44
51
|
body: heads,
|
|
45
|
-
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, heads_up: true },
|
|
52
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, heads_up: true, ...(state.model ? { model: state.model } : {}) },
|
|
46
53
|
});
|
|
47
54
|
return;
|
|
48
55
|
}
|
|
@@ -61,7 +68,7 @@ export function buildStreamHandler(self, { chat_id, update_id, agentDisplay }) {
|
|
|
61
68
|
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
62
69
|
author: agentDisplay,
|
|
63
70
|
body: piece,
|
|
64
|
-
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, streamed: true, iteration: ev.iteration },
|
|
71
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, streamed: true, iteration: ev.iteration, ...(state.model ? { model: state.model } : {}) },
|
|
65
72
|
});
|
|
66
73
|
} else if (ev.type === "tool_result" && ev.trace) {
|
|
67
74
|
// Logged for the audit trail / other channels — NOT sent to Telegram.
|
|
@@ -74,7 +81,7 @@ export function buildStreamHandler(self, { chat_id, update_id, agentDisplay }) {
|
|
|
74
81
|
actor_kind: "tool",
|
|
75
82
|
author: agentDisplay,
|
|
76
83
|
body: `${tr.tool}(${JSON.stringify(tr.args || {}).slice(0, 200)})`,
|
|
77
|
-
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, tool: tr.tool, args: tr.args, result: tr.result, iteration: ev.iteration },
|
|
84
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, tool: tr.tool, args: tr.args, result: tr.result, iteration: ev.iteration, ...(state.model ? { model: state.model } : {}) },
|
|
78
85
|
});
|
|
79
86
|
} else if (ev.type === "engine_failed") {
|
|
80
87
|
// A model in the fallback chain errored; the loop is rotating to the
|
|
@@ -175,6 +182,7 @@ export async function runFollowupTurn(self, {
|
|
|
175
182
|
let replyText;
|
|
176
183
|
let replyAuthor;
|
|
177
184
|
let saUsage = null;
|
|
185
|
+
let saModel = null;
|
|
178
186
|
try {
|
|
179
187
|
const sa = await runTelegramSuperAgent(self, {
|
|
180
188
|
chat_id,
|
|
@@ -191,10 +199,12 @@ export async function runFollowupTurn(self, {
|
|
|
191
199
|
replyText = sa.text;
|
|
192
200
|
replyAuthor = sa.name || agentDisplay;
|
|
193
201
|
saUsage = sa.usage;
|
|
202
|
+
saModel = sa.model || state.model || null;
|
|
194
203
|
} catch (e) {
|
|
195
204
|
self.log(`telegram[${self.channel.name}] a2a followup failed: ${e.message}`);
|
|
196
205
|
replyText = telegramErrorText(self, e);
|
|
197
206
|
replyAuthor = agentDisplay;
|
|
207
|
+
saModel = state.model || null;
|
|
198
208
|
}
|
|
199
209
|
stopTyping();
|
|
200
210
|
await sendFinalReply(self, {
|
|
@@ -205,6 +215,7 @@ export async function runFollowupTurn(self, {
|
|
|
205
215
|
replyActorId: SUPERAGENT_ACTOR_ID,
|
|
206
216
|
replyKind: "superagent",
|
|
207
217
|
saUsage,
|
|
218
|
+
saModel,
|
|
208
219
|
streamedCount: state.streamedCount,
|
|
209
220
|
lastStreamedText: state.lastStreamedText,
|
|
210
221
|
agentDisplay,
|
|
@@ -233,7 +244,7 @@ export function telegramErrorText(self, e) {
|
|
|
233
244
|
*/
|
|
234
245
|
export async function sendFinalReply(self, {
|
|
235
246
|
chat_id, update_id, replyText, replyAuthor, replyActorId, replyKind,
|
|
236
|
-
saUsage = null, streamedCount = 0, lastStreamedText = "", agentDisplay,
|
|
247
|
+
saUsage = null, saModel = null, streamedCount = 0, lastStreamedText = "", agentDisplay,
|
|
237
248
|
extraMeta = {},
|
|
238
249
|
}) {
|
|
239
250
|
const finalClean = replyText ? stripThinking(replyText).trim() : "";
|
|
@@ -255,6 +266,7 @@ export async function sendFinalReply(self, {
|
|
|
255
266
|
const meta = { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, final: true, ...extraMeta };
|
|
256
267
|
if (replyText && stripThinking(replyText) !== replyText) meta.thinking_stripped = true;
|
|
257
268
|
if (saUsage) meta.usage = saUsage;
|
|
269
|
+
if (saModel) meta.model = saModel;
|
|
258
270
|
appendGlobalMessage({
|
|
259
271
|
channel: CHANNELS.TELEGRAM,
|
|
260
272
|
direction: "out",
|
|
@@ -277,7 +289,7 @@ export async function sendFinalReply(self, {
|
|
|
277
289
|
agent_slug: actorId,
|
|
278
290
|
author: replyAuthor || agentDisplay,
|
|
279
291
|
body: `[send_failed] ${toSend}`,
|
|
280
|
-
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, send_error: e.message, ...(saUsage ? { usage: saUsage } : {}) },
|
|
292
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, send_error: e.message, ...(saUsage ? { usage: saUsage } : {}), ...(saModel ? { model: saModel } : {}) },
|
|
281
293
|
});
|
|
282
294
|
}
|
|
283
295
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Prefer IPv4 for all outbound connections made by this process.
|
|
2
|
+
//
|
|
3
|
+
// Some networks — notably machines on Tailscale, or misconfigured dual-stack
|
|
4
|
+
// setups — advertise an IPv6 route that black-holes. A dual-stack host such as
|
|
5
|
+
// api.telegram.org then resolves to an unreachable AAAA address, and Node's
|
|
6
|
+
// fetch/undici stalls on it with ETIMEDOUT instead of falling back to the
|
|
7
|
+
// reachable IPv4 the way `curl` does. The symptom is every daemon outbound
|
|
8
|
+
// `fetch` failing ("fetch failed") — Telegram long-poll, some LLM providers —
|
|
9
|
+
// while the shell and `curl -4` work fine.
|
|
10
|
+
//
|
|
11
|
+
// Forcing ipv4first + disabling Happy-Eyeballs family auto-selection makes
|
|
12
|
+
// `fetch` use the reachable A record. On a healthy IPv6 network this is a no-op
|
|
13
|
+
// preference (IPv4 is still universally reachable); opt out with
|
|
14
|
+
// APX_NET_IPV4_FIRST=0 if you specifically need IPv6-first.
|
|
15
|
+
import dns from "node:dns";
|
|
16
|
+
import net from "node:net";
|
|
17
|
+
|
|
18
|
+
if (process.env.APX_NET_IPV4_FIRST !== "0") {
|
|
19
|
+
// Available on Node ≥17; guarded for older runtimes.
|
|
20
|
+
try {
|
|
21
|
+
dns.setDefaultResultOrder("ipv4first");
|
|
22
|
+
} catch {
|
|
23
|
+
/* older node — ignore */
|
|
24
|
+
}
|
|
25
|
+
// Available on Node ≥18.13; disabling it stops undici from racing an
|
|
26
|
+
// unreachable IPv6 address and timing out before it tries IPv4.
|
|
27
|
+
try {
|
|
28
|
+
net.setDefaultAutoSelectFamily?.(false);
|
|
29
|
+
} catch {
|
|
30
|
+
/* older node — ignore */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -643,12 +643,18 @@ export function listGlobalThreads({ channels, _globalMessagesDir } = {}) {
|
|
|
643
643
|
}
|
|
644
644
|
|
|
645
645
|
// Read one channel+day thread shaped for the web chat viewer:
|
|
646
|
-
// { id, channel, messages: [{ role, content, ts }] } — or null when missing.
|
|
646
|
+
// { id, channel, messages: [{ role, content, ts, … }] } — or null when missing.
|
|
647
647
|
// Tool records are INCLUDED (role:"tool" with structured tool/args/result from
|
|
648
648
|
// meta) so the web viewer can render tool executions the same way the live
|
|
649
649
|
// stream does. They're persisted by every channel (e.g. telegram reply.js) but
|
|
650
650
|
// never sent to the channel itself; dropping them here is what made Telegram
|
|
651
651
|
// tool calls invisible in the web chat even though they were on disk.
|
|
652
|
+
//
|
|
653
|
+
// Assistant rows also carry their ATTRIBUTION — which agent answered
|
|
654
|
+
// (agent/agent_name/actor_kind) and on which model, plus the turn's token
|
|
655
|
+
// usage. All of it is already on disk in `meta`; dropping it here is what made
|
|
656
|
+
// a reloaded thread render "0 tok" with no model, even though the live stream
|
|
657
|
+
// showed both.
|
|
652
658
|
export function readGlobalThread({ channel, date, _globalMessagesDir } = {}) {
|
|
653
659
|
if (!CHANNEL_NAME_RE.test(String(channel || ""))) return null;
|
|
654
660
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ""))) return null;
|
|
@@ -668,10 +674,21 @@ export function readGlobalThread({ channel, date, _globalMessagesDir } = {}) {
|
|
|
668
674
|
result: r.meta?.result,
|
|
669
675
|
};
|
|
670
676
|
}
|
|
677
|
+
if (r.type === "user") {
|
|
678
|
+
return { role: "user", content: r.body || "", ts: r.ts };
|
|
679
|
+
}
|
|
680
|
+
const usage = r.meta?.usage;
|
|
671
681
|
return {
|
|
672
|
-
role:
|
|
682
|
+
role: "assistant",
|
|
673
683
|
content: r.body || "",
|
|
674
684
|
ts: r.ts,
|
|
685
|
+
// Stable id of who answered (super_agent | project-agent slug) plus the
|
|
686
|
+
// display name that was shown on the channel.
|
|
687
|
+
...(r.agent_slug || r.actor_id ? { agent: r.agent_slug || r.actor_id } : {}),
|
|
688
|
+
...(r.author ? { agent_name: r.author } : {}),
|
|
689
|
+
...(r.actor_kind ? { actor_kind: r.actor_kind } : {}),
|
|
690
|
+
...(r.meta?.model ? { model: r.meta.model } : {}),
|
|
691
|
+
...(usage && typeof usage === "object" ? { usage } : {}),
|
|
675
692
|
};
|
|
676
693
|
});
|
|
677
694
|
return { id: date, channel, messages };
|
|
@@ -44,12 +44,21 @@ function logInspectorDecision(trace, { trace_id, channel } = {}) {
|
|
|
44
44
|
// the human surfaces (web big chat + sidebar) — not generic "api"/automation
|
|
45
45
|
// callers. Best-effort: a logging failure never breaks the reply.
|
|
46
46
|
const WEB_LOGGED_CHANNELS = new Set([CHANNELS.WEB, CHANNELS.WEB_SIDEBAR]);
|
|
47
|
-
function logWebTurn(channel, { prompt, replyText }) {
|
|
47
|
+
function logWebTurn(channel, { prompt, replyText, name, model, usage }) {
|
|
48
48
|
if (!WEB_LOGGED_CHANNELS.has(channel)) return;
|
|
49
49
|
try {
|
|
50
50
|
appendGlobalMessage({ channel, direction: "in", type: "user", author: "user", body: prompt });
|
|
51
51
|
if (replyText) {
|
|
52
|
-
|
|
52
|
+
// Attribution rides along on the record: which model answered and what the
|
|
53
|
+
// turn cost. Without it a reloaded thread renders "0 tok" and no model.
|
|
54
|
+
appendGlobalMessage({
|
|
55
|
+
channel,
|
|
56
|
+
direction: "out",
|
|
57
|
+
type: "agent",
|
|
58
|
+
author: name || undefined,
|
|
59
|
+
body: replyText,
|
|
60
|
+
meta: { ...(model ? { model } : {}), ...(usage ? { usage } : {}) },
|
|
61
|
+
});
|
|
53
62
|
}
|
|
54
63
|
} catch {
|
|
55
64
|
/* best-effort */
|
|
@@ -146,7 +155,12 @@ export function register(app, { projects, registries, plugins, project, config }
|
|
|
146
155
|
|
|
147
156
|
// Web/TUI channels receive a "confirmation_required" SSE event and respond
|
|
148
157
|
// via POST /super-agent/confirm/:correlationId (see api/confirm.js).
|
|
149
|
-
|
|
158
|
+
// Non-interactive callers (e.g. `apx exec`, which streams only to render a
|
|
159
|
+
// progress indicator) send `confirm: false` to opt out: they have no way to
|
|
160
|
+
// answer the round-trip, so they fall back to the default permission policy —
|
|
161
|
+
// exactly matching the blocking POST /super-agent/chat endpoint's semantics.
|
|
162
|
+
const requestConfirmation =
|
|
163
|
+
req.body?.confirm === false ? undefined : createWebConfirmAdapter({ onEvent });
|
|
150
164
|
|
|
151
165
|
try {
|
|
152
166
|
const saResult = await runSuperAgent({
|
|
@@ -168,13 +182,22 @@ export function register(app, { projects, registries, plugins, project, config }
|
|
|
168
182
|
skipSkillsHint: inspectorOn,
|
|
169
183
|
});
|
|
170
184
|
projects.rebuild(p.id);
|
|
171
|
-
logWebTurn(ctx.channel, {
|
|
185
|
+
logWebTurn(ctx.channel, {
|
|
186
|
+
prompt,
|
|
187
|
+
replyText: saResult.text,
|
|
188
|
+
name: saResult.name,
|
|
189
|
+
model: saResult.model,
|
|
190
|
+
usage: saResult.usage,
|
|
191
|
+
});
|
|
172
192
|
send({
|
|
173
193
|
type: "final",
|
|
174
194
|
result: {
|
|
175
195
|
text: saResult.text,
|
|
176
196
|
usage: saResult.usage,
|
|
197
|
+
// `name` is the agent persona; `model` is the engine that answered
|
|
198
|
+
// (it can differ from the configured one after a routing fallback).
|
|
177
199
|
name: saResult.name,
|
|
200
|
+
model: saResult.model,
|
|
178
201
|
trace: saResult.trace,
|
|
179
202
|
},
|
|
180
203
|
});
|
|
@@ -270,11 +293,18 @@ export function register(app, { projects, registries, plugins, project, config }
|
|
|
270
293
|
skipSkillsHint: inspectorOn,
|
|
271
294
|
});
|
|
272
295
|
projects.rebuild(p.id);
|
|
273
|
-
logWebTurn(ctx.channel, {
|
|
296
|
+
logWebTurn(ctx.channel, {
|
|
297
|
+
prompt,
|
|
298
|
+
replyText: saResult.text,
|
|
299
|
+
name: saResult.name,
|
|
300
|
+
model: saResult.model,
|
|
301
|
+
usage: saResult.usage,
|
|
302
|
+
});
|
|
274
303
|
res.json({
|
|
275
304
|
text: saResult.text,
|
|
276
305
|
usage: saResult.usage,
|
|
277
306
|
name: saResult.name,
|
|
307
|
+
model: saResult.model,
|
|
278
308
|
trace: saResult.trace,
|
|
279
309
|
});
|
|
280
310
|
} catch (e) {
|
|
@@ -104,6 +104,10 @@ export function register(app, { projects, plugins, registries }) {
|
|
|
104
104
|
const channel = body.channel || "voice";
|
|
105
105
|
let suggestions = [];
|
|
106
106
|
let toolsUsed = [];
|
|
107
|
+
// Attribution for the persisted turn (see the appendGlobalMessage below).
|
|
108
|
+
let replyModel = null;
|
|
109
|
+
let replyUsage = null;
|
|
110
|
+
let replyName = null;
|
|
107
111
|
|
|
108
112
|
const channelCtx = buildVoiceChannelContext(channel, {
|
|
109
113
|
projectId: body.projectId,
|
|
@@ -125,6 +129,9 @@ export function register(app, { projects, plugins, registries }) {
|
|
|
125
129
|
previousMessages,
|
|
126
130
|
});
|
|
127
131
|
const raw = (result?.text || "").trim();
|
|
132
|
+
replyModel = result?.model || null;
|
|
133
|
+
replyUsage = result?.usage || null;
|
|
134
|
+
replyName = result?.name || null;
|
|
128
135
|
// Surface the tools the agent actually executed this turn so
|
|
129
136
|
// the overlay can show "lo que APX hizo" instead of echoing
|
|
130
137
|
// the transcript. Dedup by name (a list_tasks before+after
|
|
@@ -188,7 +195,19 @@ export function register(app, { projects, plugins, registries }) {
|
|
|
188
195
|
const logCh = channelCtx.channel || channel;
|
|
189
196
|
if (logCh && logCh !== "api") {
|
|
190
197
|
appendGlobalMessage({ channel: logCh, direction: "in", type: "user", author: "user", body: userText });
|
|
191
|
-
if (replyText)
|
|
198
|
+
if (replyText) {
|
|
199
|
+
appendGlobalMessage({
|
|
200
|
+
channel: logCh,
|
|
201
|
+
direction: "out",
|
|
202
|
+
type: "agent",
|
|
203
|
+
author: replyName || undefined,
|
|
204
|
+
body: replyText,
|
|
205
|
+
meta: {
|
|
206
|
+
...(replyModel ? { model: replyModel } : {}),
|
|
207
|
+
...(replyUsage ? { usage: replyUsage } : {}),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
192
211
|
}
|
|
193
212
|
} catch { /* best-effort */ }
|
|
194
213
|
|
package/src/host/daemon/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// APX daemon entry point. Boots config + projects + Express + plugins.
|
|
3
|
+
// Must run before any outbound fetch — see the module header for why.
|
|
4
|
+
import "#core/net/ipv4-first.js";
|
|
3
5
|
import fs from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -199,9 +199,20 @@ async function _handleMessage({ ws, text, previousMessages }, { projects, config
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
// Persist assistant response
|
|
202
|
+
// Persist assistant response, stamped with the model that answered and the
|
|
203
|
+
// turn's token usage so the web thread viewer can attribute it.
|
|
203
204
|
try {
|
|
204
|
-
await appendGlobalMessage({
|
|
205
|
+
await appendGlobalMessage({
|
|
206
|
+
channel: CHANNEL,
|
|
207
|
+
direction: "out",
|
|
208
|
+
type: "agent",
|
|
209
|
+
author: result?.name || undefined,
|
|
210
|
+
body: finalText,
|
|
211
|
+
meta: {
|
|
212
|
+
...(result?.model ? { model: result.model } : {}),
|
|
213
|
+
...(result?.usage ? { usage: result.usage } : {}),
|
|
214
|
+
},
|
|
215
|
+
});
|
|
205
216
|
} catch {}
|
|
206
217
|
|
|
207
218
|
} catch (e) {
|
|
@@ -58,6 +58,69 @@ export function resolveExecChannel(args) {
|
|
|
58
58
|
return CHANNELS.CLI;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Braille spinner frames for the live "pensando…" indicator.
|
|
62
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
63
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A single-line progress indicator drawn on stderr while the super-agent turn
|
|
67
|
+
* runs, so `apx exec` never looks frozen. Renders only on an interactive stderr
|
|
68
|
+
* (TTY); when piped/redirected it is a no-op so stdout stays clean for scripts.
|
|
69
|
+
* Set APX_NO_SPINNER=1 to disable.
|
|
70
|
+
*/
|
|
71
|
+
function createExecStatus() {
|
|
72
|
+
const active =
|
|
73
|
+
process.stderr.isTTY && process.env.APX_NO_SPINNER !== "1" && !process.env.NO_COLOR;
|
|
74
|
+
let label = "pensando…";
|
|
75
|
+
let frame = 0;
|
|
76
|
+
let timer = null;
|
|
77
|
+
const t0 = Date.now();
|
|
78
|
+
|
|
79
|
+
const draw = () => {
|
|
80
|
+
const secs = Math.floor((Date.now() - t0) / 1000);
|
|
81
|
+
process.stderr.write(`\r\x1b[2K${dim(SPINNER[frame])} ${label} ${dim(secs + "s")}`);
|
|
82
|
+
frame = (frame + 1) % SPINNER.length;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
start() {
|
|
87
|
+
if (!active || timer) return;
|
|
88
|
+
draw();
|
|
89
|
+
timer = setInterval(draw, 90);
|
|
90
|
+
timer.unref?.();
|
|
91
|
+
},
|
|
92
|
+
set(next) {
|
|
93
|
+
if (next) label = next;
|
|
94
|
+
},
|
|
95
|
+
clear() {
|
|
96
|
+
if (timer) {
|
|
97
|
+
clearInterval(timer);
|
|
98
|
+
timer = null;
|
|
99
|
+
}
|
|
100
|
+
if (active) process.stderr.write("\r\x1b[2K");
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Map a stream event to the label shown next to the spinner. */
|
|
106
|
+
function labelForEvent(event) {
|
|
107
|
+
switch (event?.type) {
|
|
108
|
+
case "skill_inspector":
|
|
109
|
+
return "cargando skills…";
|
|
110
|
+
case "model_routed":
|
|
111
|
+
case "model_start": {
|
|
112
|
+
const m = event.model || event.to || event.engine;
|
|
113
|
+
return m ? `pensando… ${dim(m)}` : "pensando…";
|
|
114
|
+
}
|
|
115
|
+
case "tool_start":
|
|
116
|
+
return `${event.trace?.tool || event.tool || "herramienta"}…`;
|
|
117
|
+
case "tool_result":
|
|
118
|
+
return "pensando…";
|
|
119
|
+
default:
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
61
124
|
async function readPromptFromStdin() {
|
|
62
125
|
const fs = await import("node:fs");
|
|
63
126
|
if (process.stdin.isTTY) return "";
|
|
@@ -99,11 +162,26 @@ export async function cmdExec(args) {
|
|
|
99
162
|
if (args.flags["max-tokens"]) body.maxTokens = parseInt(args.flags["max-tokens"], 10);
|
|
100
163
|
|
|
101
164
|
if (useSuperAgent) {
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
// Stream the turn so we can render a live progress indicator instead of a
|
|
166
|
+
// silent hang. `confirm: false` opts out of the interactive confirmation
|
|
167
|
+
// round-trip (which the CLI can't answer), keeping the same semantics as the
|
|
168
|
+
// blocking POST /super-agent/chat endpoint.
|
|
169
|
+
const status = createExecStatus();
|
|
170
|
+
status.start();
|
|
171
|
+
let result;
|
|
172
|
+
try {
|
|
173
|
+
result = await http.streamPost(
|
|
174
|
+
`/projects/${pid}/super-agent/chat/stream`,
|
|
175
|
+
{ ...body, confirm: false },
|
|
176
|
+
(event) => status.set(labelForEvent(event))
|
|
177
|
+
);
|
|
178
|
+
} finally {
|
|
179
|
+
status.clear();
|
|
180
|
+
}
|
|
181
|
+
process.stdout.write((result?.text ?? "") + "\n");
|
|
104
182
|
if (process.stderr.isTTY || args.flags.verbose) {
|
|
105
183
|
process.stderr.write(
|
|
106
|
-
`\n— ${result
|
|
184
|
+
`\n— ${result?.name || "super-agent"} | model=${result?.trace ? "tools" : "engine"} | in=${result?.usage?.input_tokens || "?"} out=${result?.usage?.output_tokens || "?"}${result?.model ? ` | ${result.model}` : ""}\n`
|
|
107
185
|
);
|
|
108
186
|
}
|
|
109
187
|
return;
|