@agentprojectcontext/apx 1.33.0 → 1.34.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/apc-context/SKILL.md +2 -5
- package/skills/apx/SKILL.md +49 -61
- package/src/core/agent/a2a/reply.js +48 -0
- package/src/core/agent/build-agent-system.js +4 -3
- package/src/core/agent/channels/voice-context.js +98 -0
- package/src/core/agent/memory.js +2 -1
- package/src/core/agent/prompt-builder.js +2 -1
- package/src/core/agent/prompts/modes/code-build.md +1 -0
- package/src/core/agent/prompts/modes/code-plan.md +1 -0
- package/src/core/agent/prompts/modes/index.js +28 -0
- package/src/core/agent/skills/loader.js +22 -18
- package/src/core/agent/stream/turn-accumulator.js +73 -0
- package/src/core/agent/suggestions.js +37 -0
- package/src/core/agent/tools/handlers/add-project.js +5 -2
- package/src/core/agent/tools/handlers/call-runtime.js +3 -2
- package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
- package/src/core/agent/tools/helpers.js +2 -2
- package/src/core/agent/tools/names.js +138 -0
- package/src/core/agent/tools/registry-bridge.js +6 -14
- package/src/core/agent/tools/registry.js +68 -65
- package/src/core/apc/context-copy.js +27 -0
- package/src/core/apc/notes.js +19 -0
- package/src/core/apc/parser.js +13 -6
- package/src/core/apc/paths.js +87 -0
- package/src/core/apc/scaffold.js +82 -74
- package/src/core/apc/skill-sync.js +13 -1
- package/src/core/channels/telegram/dispatch.js +595 -0
- package/src/core/channels/telegram/helpers.js +130 -0
- package/src/core/config/index.js +3 -2
- package/src/core/config/redact.js +95 -0
- package/src/core/constants/channels.js +2 -0
- package/src/core/constants/code-modes.js +10 -0
- package/src/core/constants/index.js +1 -0
- package/src/core/deck/manifest.js +186 -0
- package/src/core/engines/catalog.js +83 -0
- package/src/core/engines/gemini.js +28 -11
- package/src/core/engines/index.js +11 -1
- package/src/core/{tools → http-tools}/browser.js +0 -1
- package/src/core/{tools → http-tools}/fetch.js +0 -1
- package/src/core/{tools → http-tools}/glob.js +0 -1
- package/src/core/{tools → http-tools}/grep.js +0 -1
- package/src/core/{tools → http-tools}/registry.js +0 -1
- package/src/core/{tools → http-tools}/search.js +0 -1
- package/src/core/i18n/en.js +9 -0
- package/src/core/i18n/es.js +12 -0
- package/src/core/i18n/index.js +54 -0
- package/src/core/i18n/pt.js +9 -0
- package/src/core/identity/telegram.js +2 -1
- package/src/core/mcp/runner.js +272 -14
- package/src/core/mcp/sources.js +3 -2
- package/src/core/routines/index.js +16 -0
- package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
- package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
- package/src/core/runtime-skills/apx/SKILL.md +95 -0
- package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
- package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
- package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
- package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
- package/src/core/stores/code-sessions.js +50 -2
- package/src/core/stores/routine-memory.js +1 -1
- package/src/core/stores/sessions-search.js +121 -0
- package/src/core/stores/sessions.js +38 -0
- package/src/core/vars/index.js +14 -0
- package/src/core/vars/interpolate.js +86 -0
- package/src/core/vars/sources.js +151 -0
- package/src/core/voice/audio-decode.js +38 -0
- package/src/core/voice/transcription.js +225 -0
- package/src/host/daemon/api/admin-config.js +5 -82
- package/src/host/daemon/api/agents.js +5 -5
- package/src/host/daemon/api/code.js +17 -169
- package/src/host/daemon/api/config.js +3 -4
- package/src/host/daemon/api/conversations.js +8 -29
- package/src/host/daemon/api/deck.js +37 -404
- package/src/host/daemon/api/engines.js +1 -50
- package/src/host/daemon/api/exec.js +1 -1
- package/src/host/daemon/api/mcps.js +32 -0
- package/src/host/daemon/api/routines.js +1 -1
- package/src/host/daemon/api/runtimes.js +4 -3
- package/src/host/daemon/api/sessions-search.js +24 -140
- package/src/host/daemon/api/sessions.js +12 -30
- package/src/host/daemon/api/shared.js +2 -1
- package/src/host/daemon/api/telegram.js +1 -11
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/transcribe.js +2 -2
- package/src/host/daemon/api/vars.js +137 -0
- package/src/host/daemon/api/voice.js +13 -290
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/db.js +6 -6
- package/src/host/daemon/deck-exec.js +148 -0
- package/src/host/daemon/index.js +3 -3
- package/src/host/daemon/plugins/telegram/index.js +24 -687
- package/src/host/daemon/routines-scheduler.js +64 -0
- package/src/host/daemon/smoke.js +3 -2
- package/src/host/daemon/whisper-server.js +225 -0
- package/src/interfaces/cli/commands/agent.js +3 -2
- package/src/interfaces/cli/commands/command.js +2 -3
- package/src/interfaces/cli/commands/messages.js +6 -2
- package/src/interfaces/cli/commands/pair.js +5 -4
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/sessions.js +3 -2
- package/src/interfaces/cli/commands/skills.js +36 -55
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +182 -182
- package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
- package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
- package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
- package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
- package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
- package/src/interfaces/web/src/constants/index.ts +1 -1
- package/src/interfaces/web/src/i18n/en.ts +174 -7
- package/src/interfaces/web/src/i18n/es.ts +179 -15
- package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
- package/src/interfaces/web/src/lib/api/vars.ts +38 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
- package/src/interfaces/web/src/types/daemon.ts +5 -0
- package/src/host/daemon/transcription.js +0 -538
- package/src/host/daemon/whisper-transcribe.py +0 -73
- package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
- /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
- /package/src/core/{tools → http-tools}/index.js +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
- /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
- /package/src/{host/daemon → core/stores}/conversations.js +0 -0
- /package/src/{host/daemon → core/util}/thinking.js +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Audio transcription client. Two backends, both pure (no subprocess lifecycle):
|
|
2
|
+
//
|
|
3
|
+
// - LOCAL: HTTP client that talks to the persistent whisper-server.py at
|
|
4
|
+
// localhost:WHISPER_LOCAL_PORT. The server itself is spun up/down by
|
|
5
|
+
// host/daemon/whisper-server.js — this file just assumes it is reachable.
|
|
6
|
+
// - OPENAI: Whisper-1 cloud API. Needs OPENAI_API_KEY or
|
|
7
|
+
// engines.openai.api_key in config.
|
|
8
|
+
//
|
|
9
|
+
// Provider selection in ~/.apx/config.json:
|
|
10
|
+
// "transcription": {
|
|
11
|
+
// "provider": "auto" | "local" | "openai",
|
|
12
|
+
// "local": { model, device, compute_type, language, beam_size, idle_minutes }
|
|
13
|
+
// }
|
|
14
|
+
// "auto" tries local first, falls back to OpenAI if a key is configured.
|
|
15
|
+
//
|
|
16
|
+
// The split rule: anything that boots/teardown a process lives in host/daemon.
|
|
17
|
+
// Anything that sends bytes over HTTP and parses JSON lives here.
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { logInfo, logWarn } from "#core/logging.js";
|
|
21
|
+
|
|
22
|
+
/** Port the host-side whisper-server.py listens on. Single source of truth. */
|
|
23
|
+
export const WHISPER_LOCAL_PORT = 18765;
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_LOCAL = {
|
|
26
|
+
model: "small",
|
|
27
|
+
device: "cpu",
|
|
28
|
+
compute_type: "int8",
|
|
29
|
+
language: "auto",
|
|
30
|
+
beam_size: 5,
|
|
31
|
+
idle_minutes: 10,
|
|
32
|
+
// Long audio (Telegram voice notes > 10 min) can take several minutes on
|
|
33
|
+
// CPU. 20 minutes covers ~60-minute notes on a small int8 model.
|
|
34
|
+
timeout_ms: 20 * 60_000,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the effective transcription language. Priority:
|
|
39
|
+
* explicit local config → config.user.language → "auto" (whisper detects).
|
|
40
|
+
*/
|
|
41
|
+
export function resolveTranscriptionLanguage(localCfg, userLang) {
|
|
42
|
+
if (localCfg.language && localCfg.language !== "auto") return localCfg.language;
|
|
43
|
+
if (userLang) return userLang;
|
|
44
|
+
return "auto";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getConfig() {
|
|
48
|
+
try {
|
|
49
|
+
const { readConfig } = await import("#core/config/index.js");
|
|
50
|
+
const cfg = readConfig() || {};
|
|
51
|
+
const t = cfg.transcription || {};
|
|
52
|
+
const openaiKey = cfg.engines?.openai?.api_key || process.env.OPENAI_API_KEY || "";
|
|
53
|
+
const userLang = cfg.user?.language || "";
|
|
54
|
+
const localBase = { ...DEFAULT_LOCAL, ...(t.local || {}) };
|
|
55
|
+
localBase.language = resolveTranscriptionLanguage(localBase, userLang);
|
|
56
|
+
return {
|
|
57
|
+
provider: t.provider || "auto",
|
|
58
|
+
local: localBase,
|
|
59
|
+
openaiKey,
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return {
|
|
63
|
+
provider: "auto",
|
|
64
|
+
local: { ...DEFAULT_LOCAL },
|
|
65
|
+
openaiKey: process.env.OPENAI_API_KEY || "",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Call the local whisper-server.py over HTTP. Does NOT spawn or check the
|
|
72
|
+
* subprocess — that's host/daemon/whisper-server.js's job. If the server is
|
|
73
|
+
* down, this throws a clear "ECONNREFUSED" the caller can surface.
|
|
74
|
+
*/
|
|
75
|
+
export async function transcribeViaLocalServer(filePath, opts) {
|
|
76
|
+
const language = (opts.language || DEFAULT_LOCAL.language) === "auto"
|
|
77
|
+
? null
|
|
78
|
+
: (opts.language || null);
|
|
79
|
+
|
|
80
|
+
const timeoutMs = Number(opts.timeout_ms) > 0
|
|
81
|
+
? Number(opts.timeout_ms)
|
|
82
|
+
: DEFAULT_LOCAL.timeout_ms;
|
|
83
|
+
|
|
84
|
+
const body = JSON.stringify({
|
|
85
|
+
audio_path: filePath,
|
|
86
|
+
language,
|
|
87
|
+
beam_size: opts.beam_size || DEFAULT_LOCAL.beam_size,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Long transcriptions on CPU sometimes trip undici keep-alive on the
|
|
91
|
+
// outbound socket — retry once on generic "fetch failed".
|
|
92
|
+
const maxAttempts = 2;
|
|
93
|
+
let lastErr = null;
|
|
94
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
95
|
+
const t0 = Date.now();
|
|
96
|
+
try {
|
|
97
|
+
logInfo("whisper", `transcribeViaLocalServer attempt ${attempt}/${maxAttempts}`, {
|
|
98
|
+
file: path.basename(filePath),
|
|
99
|
+
language: language || "auto",
|
|
100
|
+
timeout_ms: timeoutMs,
|
|
101
|
+
});
|
|
102
|
+
const res = await fetch(`http://127.0.0.1:${WHISPER_LOCAL_PORT}/transcribe`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "content-type": "application/json", "connection": "close" },
|
|
105
|
+
body,
|
|
106
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
107
|
+
});
|
|
108
|
+
const json = await res.json();
|
|
109
|
+
if (!json.ok) throw new Error(json.error || "transcription failed");
|
|
110
|
+
logInfo("whisper", `transcribeViaLocalServer ok in ${Date.now() - t0}ms`, {
|
|
111
|
+
chars: (json.text || "").length,
|
|
112
|
+
language: json.language,
|
|
113
|
+
duration: json.duration,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
backend: "local",
|
|
118
|
+
text: json.text || "",
|
|
119
|
+
language: json.language || null,
|
|
120
|
+
language_probability: json.language_probability ?? null,
|
|
121
|
+
duration: json.duration ?? null,
|
|
122
|
+
model: json.model,
|
|
123
|
+
compute_type: json.compute_type,
|
|
124
|
+
};
|
|
125
|
+
} catch (e) {
|
|
126
|
+
lastErr = e;
|
|
127
|
+
const isRetriable = /fetch failed|ECONNRESET|socket hang up|terminated/i.test(e.message || "");
|
|
128
|
+
const dt = Date.now() - t0;
|
|
129
|
+
logWarn("whisper", `transcribeViaLocalServer attempt ${attempt} failed in ${dt}ms`, {
|
|
130
|
+
error: e.message,
|
|
131
|
+
retriable: isRetriable,
|
|
132
|
+
will_retry: isRetriable && attempt < maxAttempts,
|
|
133
|
+
});
|
|
134
|
+
if (!isRetriable || attempt >= maxAttempts) break;
|
|
135
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw lastErr || new Error("transcribeViaLocalServer: unknown failure");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** OpenAI Whisper-1 cloud API. Needs an api_key. */
|
|
142
|
+
export async function transcribeOpenAI(filePath, apiKey) {
|
|
143
|
+
if (!apiKey) throw new Error("openai transcription: no api_key");
|
|
144
|
+
const buf = fs.readFileSync(filePath);
|
|
145
|
+
const ext = path.extname(filePath).slice(1).toLowerCase() || "webm";
|
|
146
|
+
const fileType = ext === "ogg" || ext === "oga" ? "audio/ogg"
|
|
147
|
+
: ext === "mp3" ? "audio/mpeg"
|
|
148
|
+
: ext === "m4a" ? "audio/mp4"
|
|
149
|
+
: ext === "wav" ? "audio/wav"
|
|
150
|
+
: ext === "webm" ? "audio/webm"
|
|
151
|
+
: "application/octet-stream";
|
|
152
|
+
|
|
153
|
+
const form = new FormData();
|
|
154
|
+
form.append("model", "whisper-1");
|
|
155
|
+
form.append("file", new Blob([buf], { type: fileType }), path.basename(filePath));
|
|
156
|
+
|
|
157
|
+
const res = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
160
|
+
body: form,
|
|
161
|
+
signal: AbortSignal.timeout(60_000),
|
|
162
|
+
});
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const errBody = await res.text().catch(() => "");
|
|
165
|
+
throw new Error(`openai whisper ${res.status}: ${errBody.slice(0, 240)}`);
|
|
166
|
+
}
|
|
167
|
+
const json = await res.json();
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
backend: "openai",
|
|
171
|
+
text: json.text || "",
|
|
172
|
+
language: json.language || null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Transcribe a file. Provider chosen by config:
|
|
178
|
+
* - "openai": cloud only
|
|
179
|
+
* - "local": whisper-server only (no fallback)
|
|
180
|
+
* - "auto": local first, OpenAI fallback if api_key present
|
|
181
|
+
*/
|
|
182
|
+
export async function transcribe(filePath, overrides = {}) {
|
|
183
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
184
|
+
throw new Error(`transcribe: file not found: ${filePath}`);
|
|
185
|
+
}
|
|
186
|
+
const cfg = await getConfig();
|
|
187
|
+
const provider = overrides.provider || cfg.provider;
|
|
188
|
+
const localOpts = { ...cfg.local, ...overrides };
|
|
189
|
+
|
|
190
|
+
if (provider === "openai") {
|
|
191
|
+
return transcribeOpenAI(filePath, cfg.openaiKey);
|
|
192
|
+
}
|
|
193
|
+
if (provider === "local") {
|
|
194
|
+
return transcribeViaLocalServer(filePath, localOpts);
|
|
195
|
+
}
|
|
196
|
+
// auto: local first, fall back to openai if a key is configured
|
|
197
|
+
try {
|
|
198
|
+
return await transcribeViaLocalServer(filePath, localOpts);
|
|
199
|
+
} catch (localErr) {
|
|
200
|
+
if (cfg.openaiKey) {
|
|
201
|
+
return transcribeOpenAI(filePath, cfg.openaiKey);
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`local transcription failed: ${localErr.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Transcribe raw audio bytes. Saves to a temp file, transcribes, cleans up.
|
|
209
|
+
* @param {Buffer} buf
|
|
210
|
+
* @param {string} format extension hint ("webm" | "ogg" | "wav" | "mp3")
|
|
211
|
+
*/
|
|
212
|
+
export async function transcribeBuffer(buf, format = "webm", overrides = {}) {
|
|
213
|
+
if (!buf || !buf.length) throw new Error("transcribeBuffer: empty buffer");
|
|
214
|
+
const ext = format.replace(/^\./, "") || "webm";
|
|
215
|
+
const tmpFile = path.join(
|
|
216
|
+
(await import("node:os")).default.tmpdir(),
|
|
217
|
+
`apx-audio-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`
|
|
218
|
+
);
|
|
219
|
+
try {
|
|
220
|
+
fs.writeFileSync(tmpFile, buf);
|
|
221
|
+
return await transcribe(tmpFile, overrides);
|
|
222
|
+
} finally {
|
|
223
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -9,88 +9,11 @@ import { readConfig, writeConfig } from "#core/config/index.js";
|
|
|
9
9
|
import { resolveAgentName } from "#core/identity/index.js";
|
|
10
10
|
import { setDottedKey, unsetDottedKey } from "../project-config.js";
|
|
11
11
|
import { PERMISSION_MODES, DEFAULT_PERMISSION_MODE } from "#core/constants/permissions.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"engines.openrouter.api_key",
|
|
18
|
-
"engines.gemini.api_key",
|
|
19
|
-
"voice.tts.elevenlabs.api_key",
|
|
20
|
-
"voice.tts.openai.api_key",
|
|
21
|
-
"voice.tts.gemini.api_key",
|
|
22
|
-
"memory.embeddings.openai.api_key",
|
|
23
|
-
"memory.embeddings.gemini.api_key",
|
|
24
|
-
"telegram.channels.*.bot_token",
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
function getDotted(obj, dotted) {
|
|
28
|
-
const parts = dotted.split(".");
|
|
29
|
-
let cur = obj;
|
|
30
|
-
for (const p of parts) {
|
|
31
|
-
if (cur == null) return undefined;
|
|
32
|
-
cur = cur[p];
|
|
33
|
-
}
|
|
34
|
-
return cur;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function secretMarker(value) {
|
|
38
|
-
if (typeof value !== "string" || !value.length) return value;
|
|
39
|
-
const suffix = value.slice(-5);
|
|
40
|
-
return `*** set *** (...${suffix})`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isSecretMarker(value) {
|
|
44
|
-
return typeof value === "string" && value.startsWith("*** set ***");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Returns a deep copy with `*** set ***` for every present secret value.
|
|
48
|
-
function redact(cfg) {
|
|
49
|
-
const out = JSON.parse(JSON.stringify(cfg || {}));
|
|
50
|
-
const mark = (val) => (typeof val === "string" && val.length ? secretMarker(val) : val);
|
|
51
|
-
|
|
52
|
-
// Engine api keys + voice tts keys
|
|
53
|
-
for (const path of SECRET_PATHS) {
|
|
54
|
-
if (path.includes("*")) continue;
|
|
55
|
-
const parts = path.split(".");
|
|
56
|
-
let cur = out;
|
|
57
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
58
|
-
if (!cur[parts[i]] || typeof cur[parts[i]] !== "object") { cur = null; break; }
|
|
59
|
-
cur = cur[parts[i]];
|
|
60
|
-
}
|
|
61
|
-
if (cur && cur[parts[parts.length - 1]]) {
|
|
62
|
-
cur[parts[parts.length - 1]] = mark(cur[parts[parts.length - 1]]);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Telegram channels — array, redact bot_token per item (keep the suffix so
|
|
66
|
-
// the UI can show which token is set, e.g. "*** set *** (...AB12)").
|
|
67
|
-
const channels = out?.telegram?.channels;
|
|
68
|
-
if (Array.isArray(channels)) {
|
|
69
|
-
for (const ch of channels) {
|
|
70
|
-
if (ch && typeof ch.bot_token === "string" && ch.bot_token.length) {
|
|
71
|
-
ch.bot_token = mark(ch.bot_token);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function mergeRedactedChannels(nextChannels, priorChannels) {
|
|
79
|
-
if (!Array.isArray(nextChannels)) return nextChannels;
|
|
80
|
-
const priorByName = new Map(
|
|
81
|
-
(Array.isArray(priorChannels) ? priorChannels : [])
|
|
82
|
-
.filter((c) => c && typeof c.name === "string")
|
|
83
|
-
.map((c) => [c.name, c])
|
|
84
|
-
);
|
|
85
|
-
return nextChannels.map((channel) => {
|
|
86
|
-
if (!channel || typeof channel !== "object") return channel;
|
|
87
|
-
const prior = priorByName.get(channel.name);
|
|
88
|
-
if (prior?.bot_token && (channel.bot_token === undefined || isSecretMarker(channel.bot_token))) {
|
|
89
|
-
return { ...channel, bot_token: prior.bot_token };
|
|
90
|
-
}
|
|
91
|
-
return channel;
|
|
92
|
-
});
|
|
93
|
-
}
|
|
12
|
+
import {
|
|
13
|
+
redactConfig as redact,
|
|
14
|
+
isSecretMarker,
|
|
15
|
+
mergeRedactedChannels,
|
|
16
|
+
} from "#core/config/redact.js";
|
|
94
17
|
|
|
95
18
|
export function register(app, { config, scheduler, plugins }) {
|
|
96
19
|
app.get("/admin/config", (_req, res) => {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { readAgents, readVaultAgents, readVaultAgent } from "#core/apc/parser.js";
|
|
10
|
+
import { apcAgentFile, apcDir, apcMemoryFile } from "#core/apc/paths.js";
|
|
10
11
|
import {
|
|
11
12
|
writeAgentFile,
|
|
12
13
|
writeVaultAgentFile,
|
|
@@ -202,7 +203,7 @@ export function register(app, { projects, project }) {
|
|
|
202
203
|
const p = project(req, res);
|
|
203
204
|
if (!p) return;
|
|
204
205
|
const slug = req.params.slug;
|
|
205
|
-
const file =
|
|
206
|
+
const file = apcAgentFile(p.path, slug);
|
|
206
207
|
const runtimeDir = path.dirname(agentMemoryPath(p, slug));
|
|
207
208
|
const legacyDir = path.dirname(legacyAgentMemoryPath(p.path, slug));
|
|
208
209
|
if (!fs.existsSync(file) && !fs.existsSync(runtimeDir) && !fs.existsSync(legacyDir))
|
|
@@ -222,7 +223,7 @@ export function register(app, { projects, project }) {
|
|
|
222
223
|
app.get("/projects/:pid/memory", (req, res) => {
|
|
223
224
|
const p = project(req, res);
|
|
224
225
|
if (!p) return;
|
|
225
|
-
const memPath =
|
|
226
|
+
const memPath = apcMemoryFile(p.path);
|
|
226
227
|
const body = fs.existsSync(memPath) ? fs.readFileSync(memPath, "utf8") : "";
|
|
227
228
|
res.json({ body, path: memPath });
|
|
228
229
|
});
|
|
@@ -233,9 +234,8 @@ export function register(app, { projects, project }) {
|
|
|
233
234
|
const { body } = req.body || {};
|
|
234
235
|
if (typeof body !== "string")
|
|
235
236
|
return res.status(400).json({ error: "body must be string" });
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const memPath = path.join(apcDir, "memory.md");
|
|
237
|
+
fs.mkdirSync(apcDir(p.path), { recursive: true });
|
|
238
|
+
const memPath = apcMemoryFile(p.path);
|
|
239
239
|
fs.writeFileSync(memPath, body);
|
|
240
240
|
try { projects.rebuild(p.id); } catch {}
|
|
241
241
|
res.json({ ok: true, bytes: Buffer.byteLength(body, "utf8") });
|
|
@@ -16,6 +16,7 @@ import { runSuperAgent } from "#core/agent/super-agent.js";
|
|
|
16
16
|
import { appendSuperAgentErrorTrace } from "./shared.js";
|
|
17
17
|
import { createWebConfirmAdapter } from "#core/confirmation/adapters/web.js";
|
|
18
18
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
19
|
+
import { CODE_MODES, DEFAULT_CODE_MODE } from "#core/constants/code-modes.js";
|
|
19
20
|
import {
|
|
20
21
|
listCodeSessions,
|
|
21
22
|
getCodeSession,
|
|
@@ -23,181 +24,28 @@ import {
|
|
|
23
24
|
updateCodeSession,
|
|
24
25
|
removeCodeSession,
|
|
25
26
|
appendTurn,
|
|
27
|
+
codeSessionHistory,
|
|
26
28
|
} from "#core/stores/code-sessions.js";
|
|
29
|
+
import { makeTurnAccumulator } from "#core/agent/stream/turn-accumulator.js";
|
|
27
30
|
import { captureBaseline, diffAgainstBaseline, initGitRepo } from "#core/git-baseline.js";
|
|
28
31
|
import { loggerFor } from "#core/logging.js";
|
|
29
32
|
import { readAgents } from "#core/apc/parser.js";
|
|
33
|
+
import { CODE_PLAN_TOOLS, CODE_BUILD_TOOLS } from "#core/agent/tools/names.js";
|
|
34
|
+
import { codeModeGuidance } from "#core/agent/prompts/modes/index.js";
|
|
30
35
|
|
|
31
36
|
const log = loggerFor("code");
|
|
32
37
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
"read_file",
|
|
38
|
-
"list_files",
|
|
39
|
-
"search_files",
|
|
40
|
-
"grep",
|
|
41
|
-
"glob",
|
|
42
|
-
"list_projects",
|
|
43
|
-
"list_agents",
|
|
44
|
-
"list_mcps",
|
|
45
|
-
"read_agent_memory",
|
|
46
|
-
"read_self_memory",
|
|
47
|
-
"search_sessions",
|
|
48
|
-
"search_messages",
|
|
49
|
-
"tail_messages",
|
|
50
|
-
"list_skills",
|
|
51
|
-
"load_skill",
|
|
52
|
-
"list_tasks",
|
|
53
|
-
"ask_questions",
|
|
54
|
-
"fetch",
|
|
55
|
-
"search",
|
|
56
|
-
];
|
|
57
|
-
|
|
38
|
+
// Mode-specific tool allow-lists and prompt fragments are owned by core/:
|
|
39
|
+
// - tool names + plan/build lists → #core/agent/tools/names.js
|
|
40
|
+
// - per-mode guidance text → #core/agent/prompts/modes/*.md
|
|
41
|
+
// This file just picks the right pair for the request.
|
|
58
42
|
function modeGuidanceFor(mode) {
|
|
59
|
-
|
|
60
|
-
return [
|
|
61
|
-
"MODE: plan. Investigate the codebase (read/list/search/grep) and propose",
|
|
62
|
-
"an approach with the EXACT changes you would make (files + diffs/snippets).",
|
|
63
|
-
"Do NOT write or edit files and do NOT run mutating shell commands — your",
|
|
64
|
-
"editing tools are disabled in this mode. End with a concise, ordered plan.",
|
|
65
|
-
].join(" ");
|
|
66
|
-
}
|
|
67
|
-
return [
|
|
68
|
-
"MODE: build. Make the changes directly using your file and shell tools",
|
|
69
|
-
"(read_file, write_file, edit_file, run_shell, …). Do not ask for",
|
|
70
|
-
"confirmation and do not stop after one step — keep calling tools until the",
|
|
71
|
-
"entire task is done, then briefly summarize what you changed and why.",
|
|
72
|
-
"Prefer surgical edits over rewrites.",
|
|
73
|
-
"When the user asks for a reusable script, snippet, or 'artifact' (something",
|
|
74
|
-
"they want to keep and run later), put it under `artifacts/<name>` inside",
|
|
75
|
-
"the project — it then shows up in the Artifacts tab. Don't drop reusable",
|
|
76
|
-
"scripts at the project root.",
|
|
77
|
-
"If a parameter you need is missing (API key, app id, target URL, …), call",
|
|
78
|
-
"`ask_questions` ONCE with all your questions and stop — control returns",
|
|
79
|
-
"to the user. Do not call ask_questions again in the same turn; you'll just",
|
|
80
|
-
"get the same blank state back. Each question can be a string (free-text",
|
|
81
|
-
"answer) OR an object {question, options:[{label, description}], multiSelect}",
|
|
82
|
-
"for choices. Prefer 2–4 mutually-exclusive options when a question has a",
|
|
83
|
-
"natural shortlist (yes/no, which-of-these, …); leave options empty for",
|
|
84
|
-
"open-ended answers (API keys, names, free-form ideas).",
|
|
85
|
-
"If the previous assistant turn already asked these same questions and the",
|
|
86
|
-
"current user message is the compiled answers, DO NOT call ask_questions",
|
|
87
|
-
"again — process the answers and proceed with the task.",
|
|
88
|
-
].join(" ");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Build the [{role, content}] history the super-agent expects from the stored
|
|
92
|
-
// rich transcript: flatten each turn's text parts. Tool parts are normally
|
|
93
|
-
// internal, but ask_questions is special — without surfacing it the model
|
|
94
|
-
// loses track that it ALREADY asked, sees the user's compiled-answer reply
|
|
95
|
-
// as a fresh request, and asks again forever. We render a one-line synthetic
|
|
96
|
-
// summary of each ask_questions call so the next turn's context shows
|
|
97
|
-
// "I asked X, the user replied Y" naturally.
|
|
98
|
-
function summarizeAskQuestionsPart(part) {
|
|
99
|
-
const raw = part?.args?.questions;
|
|
100
|
-
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
101
|
-
const lines = raw
|
|
102
|
-
.map((q) => {
|
|
103
|
-
if (typeof q === "string") return `- ${q}`;
|
|
104
|
-
if (!q || typeof q !== "object" || typeof q.question !== "string") return null;
|
|
105
|
-
const opts = Array.isArray(q.options) ? q.options : [];
|
|
106
|
-
const optStr = opts
|
|
107
|
-
.map((o) => (typeof o === "string" ? o : (o && typeof o.label === "string" ? o.label : "")))
|
|
108
|
-
.filter(Boolean)
|
|
109
|
-
.join(", ");
|
|
110
|
-
return optStr ? `- ${q.question} (opciones: ${optStr})` : `- ${q.question}`;
|
|
111
|
-
})
|
|
112
|
-
.filter(Boolean);
|
|
113
|
-
if (lines.length === 0) return null;
|
|
114
|
-
return `[ask_questions]\n${lines.join("\n")}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function historyFrom(session) {
|
|
118
|
-
return (session.messages || []).map((m) => {
|
|
119
|
-
const chunks = [];
|
|
120
|
-
for (const p of m.parts || []) {
|
|
121
|
-
if (!p) continue;
|
|
122
|
-
if (p.kind === "text" && p.text) chunks.push(p.text);
|
|
123
|
-
else if (p.kind === "tool" && p.tool === "ask_questions") {
|
|
124
|
-
const summary = summarizeAskQuestionsPart(p);
|
|
125
|
-
if (summary) chunks.push(summary);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return { role: m.role, content: chunks.join("\n\n").trim() };
|
|
129
|
-
});
|
|
43
|
+
return codeModeGuidance(mode);
|
|
130
44
|
}
|
|
131
45
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
function makeTurnAccumulator() {
|
|
136
|
-
const parts = [];
|
|
137
|
-
const notes = [];
|
|
138
|
-
let model = null;
|
|
139
|
-
let usage = null;
|
|
140
|
-
const findTool = (id) => parts.find((p) => p.kind === "tool" && p.id === id);
|
|
141
|
-
return {
|
|
142
|
-
apply(ev) {
|
|
143
|
-
switch (ev?.type) {
|
|
144
|
-
case "model_start":
|
|
145
|
-
if (ev.model) model = ev.model;
|
|
146
|
-
break;
|
|
147
|
-
case "model_routed":
|
|
148
|
-
if (ev.model) model = ev.model;
|
|
149
|
-
if (ev.from_fallback) notes.push(`routing fell back → ${ev.model}`);
|
|
150
|
-
break;
|
|
151
|
-
case "engine_failed":
|
|
152
|
-
notes.push(`engine ${ev.model || "?"} failed → ${ev.retry_with || "retry"}`);
|
|
153
|
-
break;
|
|
154
|
-
case "model_retry":
|
|
155
|
-
notes.push(`retry (${ev.reason || "?"})`);
|
|
156
|
-
break;
|
|
157
|
-
case "tools_suppressed":
|
|
158
|
-
notes.push(`tools suppressed: ${(ev.tools || []).join(", ")}`);
|
|
159
|
-
break;
|
|
160
|
-
case "assistant_text":
|
|
161
|
-
if (ev.text) parts.push({ kind: "text", text: ev.text });
|
|
162
|
-
break;
|
|
163
|
-
case "tool_start":
|
|
164
|
-
if (ev.trace)
|
|
165
|
-
parts.push({
|
|
166
|
-
kind: "tool",
|
|
167
|
-
id: ev.trace.id,
|
|
168
|
-
tool: ev.trace.tool,
|
|
169
|
-
args: ev.trace.args,
|
|
170
|
-
status: "running",
|
|
171
|
-
});
|
|
172
|
-
break;
|
|
173
|
-
case "tool_deduped": {
|
|
174
|
-
const t = ev.trace && findTool(ev.trace.id);
|
|
175
|
-
if (t) t.status = "deduped";
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
case "tool_result": {
|
|
179
|
-
const t = ev.trace && findTool(ev.trace.id);
|
|
180
|
-
if (t) {
|
|
181
|
-
t.result = ev.trace.result;
|
|
182
|
-
const errored =
|
|
183
|
-
ev.trace.result && typeof ev.trace.result === "object" && ev.trace.result.error;
|
|
184
|
-
t.status = errored ? "error" : t.status === "deduped" ? "deduped" : "done";
|
|
185
|
-
}
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
case "final":
|
|
189
|
-
usage = ev.result?.usage ?? usage;
|
|
190
|
-
if (!model) model = ev.result?.name || null;
|
|
191
|
-
break;
|
|
192
|
-
default:
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
build() {
|
|
197
|
-
return { parts, notes, model, usage };
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
46
|
+
// History flattening + stream-event accumulator now live in core/ — see
|
|
47
|
+
// codeSessionHistory() (transcript → engine messages) and makeTurnAccumulator()
|
|
48
|
+
// (stream events → persistable ChatParts) imported above.
|
|
201
49
|
|
|
202
50
|
export function register(app, { projects, project, config, registries, plugins }) {
|
|
203
51
|
const findProject = (req, res) => project(req, res);
|
|
@@ -290,8 +138,8 @@ export function register(app, { projects, project, config, registries, plugins }
|
|
|
290
138
|
const { prompt } = req.body || {};
|
|
291
139
|
if (!prompt) return res.status(400).json({ error: "prompt required" });
|
|
292
140
|
|
|
293
|
-
const mode = session.mode ===
|
|
294
|
-
const previousMessages =
|
|
141
|
+
const mode = session.mode === CODE_MODES.PLAN ? CODE_MODES.PLAN : DEFAULT_CODE_MODE;
|
|
142
|
+
const previousMessages = codeSessionHistory(session);
|
|
295
143
|
|
|
296
144
|
// If a project agent is selected, inject its system prompt as a suffix so
|
|
297
145
|
// the super-agent's tool loop runs with the agent's personality/context.
|
|
@@ -340,7 +188,7 @@ export function register(app, { projects, project, config, registries, plugins }
|
|
|
340
188
|
previousMessages,
|
|
341
189
|
systemSuffix: agentSystemSuffix,
|
|
342
190
|
overrideModel: session.model || undefined,
|
|
343
|
-
allowedTools: mode ===
|
|
191
|
+
allowedTools: mode === CODE_MODES.PLAN ? CODE_PLAN_TOOLS : CODE_BUILD_TOOLS,
|
|
344
192
|
// Coding tasks are multi-step: give the loop a high safety ceiling so it
|
|
345
193
|
// can chain 20-30+ tools (read → edit → run → verify …) and a real
|
|
346
194
|
// output budget for substantial code / explanations per turn. The
|
|
@@ -351,7 +199,7 @@ export function register(app, { projects, project, config, registries, plugins }
|
|
|
351
199
|
// Build mode = the model must keep calling tools until it calls `finish`.
|
|
352
200
|
// Plan mode is read-only investigation that ends with a written plan, so
|
|
353
201
|
// it keeps the normal "text ends the turn" behavior.
|
|
354
|
-
completionContract: mode ===
|
|
202
|
+
completionContract: mode === CODE_MODES.BUILD,
|
|
355
203
|
onEvent,
|
|
356
204
|
requestConfirmation: createWebConfirmAdapter({ onEvent }),
|
|
357
205
|
});
|
|
@@ -10,10 +10,9 @@ import {
|
|
|
10
10
|
setDottedKey,
|
|
11
11
|
unsetDottedKey,
|
|
12
12
|
} from "../project-config.js";
|
|
13
|
+
import { apcProjectFile, apcProjectConfigFile } from "#core/apc/paths.js";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
return path.join(root, ".apc", "project.json");
|
|
16
|
-
}
|
|
15
|
+
const projectJsonPath = apcProjectFile;
|
|
17
16
|
|
|
18
17
|
function readProjectJson(root) {
|
|
19
18
|
const p = projectJsonPath(root);
|
|
@@ -34,7 +33,7 @@ export function register(app, { projects, project }) {
|
|
|
34
33
|
res.json({
|
|
35
34
|
effective: p.config || {},
|
|
36
35
|
project_only: readProjectConfig(p.path),
|
|
37
|
-
project_config_path:
|
|
36
|
+
project_config_path: apcProjectConfigFile(p.path),
|
|
38
37
|
apc_project: readProjectJson(p.path),
|
|
39
38
|
project_json_path: projectJsonPath(p.path),
|
|
40
39
|
});
|
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
// POST /projects/:pid/agents/:slug/compact
|
|
5
5
|
// POST /projects/:pid/agents/:slug/conversations/:id/compact
|
|
6
6
|
// POST /projects/:pid/send (agent-to-agent)
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
7
|
import { readAgents } from "#core/apc/parser.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
8
|
+
import { listConversations, readConversation } from "#core/stores/conversations.js";
|
|
9
|
+
import { compactConversation } from "#core/stores/conversations-compactor.js";
|
|
10
|
+
import { replyAsAgent } from "#core/agent/a2a/reply.js";
|
|
13
11
|
import { nowIso } from "./shared.js";
|
|
14
12
|
|
|
15
13
|
export function register(app, { project, config }) {
|
|
@@ -102,30 +100,11 @@ export function register(app, { project, config }) {
|
|
|
102
100
|
let reply = null;
|
|
103
101
|
if (deliver && toAgent.fields.Model) {
|
|
104
102
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
parts.push(
|
|
111
|
-
`You are ${toAgent.slug}. You just received a message from ${fromAgent.slug}. Reply concisely.`
|
|
112
|
-
);
|
|
113
|
-
const memPath = path.join(
|
|
114
|
-
p.path,
|
|
115
|
-
".apc",
|
|
116
|
-
"agents",
|
|
117
|
-
toAgent.slug,
|
|
118
|
-
"memory.md"
|
|
119
|
-
);
|
|
120
|
-
if (fs.existsSync(memPath))
|
|
121
|
-
parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
|
|
122
|
-
|
|
123
|
-
const result = await callEngine({
|
|
124
|
-
modelId: toAgent.fields.Model,
|
|
125
|
-
system: parts.join("\n\n"),
|
|
126
|
-
messages: [
|
|
127
|
-
{ role: "user", content: `From ${fromAgent.slug}:\n\n${body}` },
|
|
128
|
-
],
|
|
103
|
+
const result = await replyAsAgent({
|
|
104
|
+
projectPath: p.path,
|
|
105
|
+
toAgent,
|
|
106
|
+
fromAgent,
|
|
107
|
+
body,
|
|
129
108
|
config: p.config || config,
|
|
130
109
|
});
|
|
131
110
|
|