@agentprojectcontext/apx 1.73.1 → 1.74.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/package.json +3 -2
- 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/desktop/autostart.js +31 -14
- package/src/core/mcp/runner.js +6 -1
- package/src/core/net/ipv4-first.js +32 -0
- package/src/core/stores/messages.js +19 -2
- package/src/core/util/index.js +1 -0
- package/src/core/util/path-env.js +73 -0
- package/src/core/voice/transcription.js +28 -3
- 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/host/daemon/whisper-server.js +27 -0
- package/src/interfaces/cli/commands/exec.js +81 -3
- package/src/interfaces/web/dist/assets/index-COrRuBp1.css +1 -0
- package/src/interfaces/web/dist/assets/index-DUXlrW8P.js +819 -0
- package/src/interfaces/web/dist/assets/index-DUXlrW8P.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +407 -378
- 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
|
@@ -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) {
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
WHISPER_LOCAL_PORT,
|
|
16
16
|
DEFAULT_LOCAL,
|
|
17
17
|
getConfig,
|
|
18
|
+
setLocalServerEnsure,
|
|
18
19
|
} from "#core/voice/transcription.js";
|
|
20
|
+
import { envWithPath } from "#core/util/path-env.js";
|
|
19
21
|
import { pythonForWhisper } from "./stt-venv.js";
|
|
20
22
|
|
|
21
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -82,7 +84,21 @@ async function _killOrphanWhisper() {
|
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
// Serializes concurrent ensures. Two voice messages arriving together (or a
|
|
88
|
+
// desktop utterance racing a Telegram note) would otherwise each see a dead
|
|
89
|
+
// port and spawn their own python, and the loser then trips
|
|
90
|
+
// "address already in use".
|
|
91
|
+
let _ensureInFlight = null;
|
|
92
|
+
|
|
85
93
|
export async function ensureWhisperServer(opts) {
|
|
94
|
+
if (_ensureInFlight) return _ensureInFlight;
|
|
95
|
+
_ensureInFlight = _ensureWhisperServer(opts).finally(() => {
|
|
96
|
+
_ensureInFlight = null;
|
|
97
|
+
});
|
|
98
|
+
return _ensureInFlight;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function _ensureWhisperServer(opts) {
|
|
86
102
|
const model = opts.model || DEFAULT_LOCAL.model;
|
|
87
103
|
const backend = opts.backend || "faster";
|
|
88
104
|
|
|
@@ -129,9 +145,15 @@ async function _spawnWhisper(opts, model, backend, retried) {
|
|
|
129
145
|
|
|
130
146
|
// Prefer APX's dedicated venv interpreter (isolated mlx/faster-whisper);
|
|
131
147
|
// fall back to system python3 for the legacy user-site install.
|
|
148
|
+
//
|
|
149
|
+
// envWithPath: both whisper backends shell out to `ffmpeg` to decode
|
|
150
|
+
// .oga/.webm. Booted from launchd the daemon's PATH is /usr/bin:/bin:
|
|
151
|
+
// /usr/sbin:/sbin, so a Homebrew ffmpeg is invisible and every transcription
|
|
152
|
+
// dies with "[Errno 2] No such file or directory: 'ffmpeg'".
|
|
132
153
|
const proc = spawn(pythonForWhisper(), args, {
|
|
133
154
|
stdio: ["ignore", "pipe", "inherit"],
|
|
134
155
|
detached: false,
|
|
156
|
+
env: envWithPath(),
|
|
135
157
|
});
|
|
136
158
|
|
|
137
159
|
_serverProcess = proc;
|
|
@@ -236,3 +258,8 @@ export const WHISPER_PATHS = {
|
|
|
236
258
|
whisper_server: WHISPER_SERVER,
|
|
237
259
|
port: WHISPER_LOCAL_PORT,
|
|
238
260
|
};
|
|
261
|
+
|
|
262
|
+
// Hand core the ability to revive the subprocess on demand. Importing this
|
|
263
|
+
// module (the daemon does, to preload at boot) is what wires it up — core
|
|
264
|
+
// itself never reaches into host/.
|
|
265
|
+
setLocalServerEnsure(ensureWhisperServer);
|
|
@@ -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;
|