@agentprojectcontext/apx 1.14.0 → 1.15.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 +2 -1
- package/skills/apc-context/SKILL.md +68 -18
- package/skills/apx/SKILL.md +89 -33
- package/src/cli/commands/sys.js +249 -21
- package/src/cli/commands/telegram.js +8 -2
- package/src/cli/http.js +24 -7
- package/src/cli/index.js +10 -3
- package/src/cli/postinstall.js +54 -4
- package/src/cli/terminal-chat/renderer.js +60 -3
- package/src/core/logging.js +37 -0
- package/src/core/scaffold.js +70 -56
- package/src/daemon/api.js +29 -2
- package/src/daemon/engines/anthropic.js +2 -1
- package/src/daemon/engines/gemini.js +2 -1
- package/src/daemon/engines/index.js +3 -3
- package/src/daemon/engines/ollama.js +2 -1
- package/src/daemon/engines/openai.js +2 -1
- package/src/daemon/plugins/telegram.js +85 -1
- package/src/daemon/skills-loader.js +31 -66
- package/src/daemon/smoke.js +9 -1
- package/src/daemon/super-agent-tools/index.js +2 -0
- package/src/daemon/super-agent-tools/tools/ask-questions.js +28 -0
- package/src/daemon/super-agent-tools/tools/send-telegram.js +85 -15
- package/src/daemon/super-agent.js +99 -10
- package/src/daemon/tools/browser.js +19 -1
- package/src/daemon/tools/registry.js +9 -7
- package/src/core/apc-context-skill.md +0 -105
- package/src/core/apx-skill.md +0 -135
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { APX_HOME } from "./config.js";
|
|
4
|
+
|
|
5
|
+
export const LOG_DIR = path.join(APX_HOME, "logs");
|
|
6
|
+
export const ERROR_TRACE_PATH = path.join(LOG_DIR, "errors.jsonl");
|
|
7
|
+
|
|
8
|
+
const SECRET_KEY_RE = /(token|secret|password|api[_-]?key|authorization|bot[_-]?token)/i;
|
|
9
|
+
|
|
10
|
+
function redact(value, seen = new WeakSet()) {
|
|
11
|
+
if (value === null || value === undefined) return value;
|
|
12
|
+
if (typeof value !== "object") return value;
|
|
13
|
+
if (seen.has(value)) return "[circular]";
|
|
14
|
+
seen.add(value);
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(value)) return value.map((item) => redact(item, seen));
|
|
17
|
+
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const [key, val] of Object.entries(value)) {
|
|
20
|
+
out[key] = SECRET_KEY_RE.test(key) ? "[redacted]" : redact(val, seen);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function appendErrorTrace(record) {
|
|
26
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
27
|
+
const entry = {
|
|
28
|
+
ts: new Date().toISOString(),
|
|
29
|
+
...redact(record),
|
|
30
|
+
};
|
|
31
|
+
fs.appendFileSync(ERROR_TRACE_PATH, JSON.stringify(entry) + "\n", "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function previewText(text, max = 500) {
|
|
35
|
+
const clean = String(text || "").replace(/\s+/g, " ").trim();
|
|
36
|
+
return clean.length > max ? clean.slice(0, max - 1) + "…" : clean;
|
|
37
|
+
}
|
package/src/core/scaffold.js
CHANGED
|
@@ -6,9 +6,43 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { readAgents, readAgentsFromDir, VAULT_DIR } from "./parser.js";
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
10
|
+
const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
|
|
11
|
+
const RUNTIME_SKILLS_DIR = path.join(__dirname, "runtime-skills");
|
|
9
12
|
|
|
10
13
|
export const SPEC_VERSION = "0.1.0";
|
|
11
14
|
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Bundled skills — single source of truth lives at <packageRoot>/skills/<slug>/SKILL.md
|
|
17
|
+
// with proper frontmatter. The `apc-context` copy is refreshed on every
|
|
18
|
+
// install/update from the canonical APC repo (see src/cli/postinstall.js).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function readBundledSkill(slug) {
|
|
22
|
+
const file = path.join(BUNDLED_SKILLS_DIR, slug, "SKILL.md");
|
|
23
|
+
if (!fs.existsSync(file)) return null;
|
|
24
|
+
return fs.readFileSync(file, "utf8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Split frontmatter and body from a SKILL.md. Used by IDE targets that need
|
|
28
|
+
// to re-wrap the body in their own rule-file frontmatter.
|
|
29
|
+
function splitFrontmatter(raw) {
|
|
30
|
+
if (!raw.startsWith("---")) return { fm: "", body: raw };
|
|
31
|
+
const end = raw.indexOf("\n---", 3);
|
|
32
|
+
if (end < 0) return { fm: "", body: raw };
|
|
33
|
+
const fm = raw.slice(0, end + 4);
|
|
34
|
+
const body = raw.slice(end + 4).replace(/^\n/, "");
|
|
35
|
+
return { fm, body };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Pull description from frontmatter so cursor/.mdc rule files can advertise
|
|
39
|
+
// the same activation trigger.
|
|
40
|
+
function readDescription(raw) {
|
|
41
|
+
const { fm } = splitFrontmatter(raw);
|
|
42
|
+
const m = fm.match(/^description:\s*"?(.*?)"?\s*$/m);
|
|
43
|
+
return m ? m[1] : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
// ---------------------------------------------------------------------------
|
|
13
47
|
// IDE skill targets — written during `apx init` and `apx skills add`
|
|
14
48
|
// ---------------------------------------------------------------------------
|
|
@@ -21,7 +55,8 @@ export const IDE_TARGETS = [
|
|
|
21
55
|
label: "Claude Code",
|
|
22
56
|
ideDir: ".claude",
|
|
23
57
|
file: ".claude/skills/apx/SKILL.md",
|
|
24
|
-
|
|
58
|
+
// Claude Code consumes SKILL.md with its native frontmatter as-is.
|
|
59
|
+
render: (raw) => raw,
|
|
25
60
|
append: false,
|
|
26
61
|
},
|
|
27
62
|
{
|
|
@@ -29,8 +64,11 @@ export const IDE_TARGETS = [
|
|
|
29
64
|
label: "Cursor",
|
|
30
65
|
ideDir: ".cursor",
|
|
31
66
|
file: ".cursor/rules/apx.mdc",
|
|
32
|
-
render: (
|
|
33
|
-
|
|
67
|
+
render: (raw) => {
|
|
68
|
+
const { body } = splitFrontmatter(raw);
|
|
69
|
+
const desc = readDescription(raw);
|
|
70
|
+
return `---\ndescription: ${desc}\n---\n\n${body}`;
|
|
71
|
+
},
|
|
34
72
|
append: false,
|
|
35
73
|
},
|
|
36
74
|
{
|
|
@@ -38,8 +76,11 @@ export const IDE_TARGETS = [
|
|
|
38
76
|
label: "Windsurf",
|
|
39
77
|
ideDir: ".windsurf",
|
|
40
78
|
file: ".windsurf/rules/apx.md",
|
|
41
|
-
render: (
|
|
42
|
-
|
|
79
|
+
render: (raw) => {
|
|
80
|
+
const { body } = splitFrontmatter(raw);
|
|
81
|
+
const desc = readDescription(raw);
|
|
82
|
+
return `---\ntrigger: model_decision\ndescription: ${desc}\n---\n\n${body}`;
|
|
83
|
+
},
|
|
43
84
|
append: false,
|
|
44
85
|
},
|
|
45
86
|
{
|
|
@@ -47,7 +88,10 @@ export const IDE_TARGETS = [
|
|
|
47
88
|
label: "GitHub Copilot",
|
|
48
89
|
ideDir: ".github",
|
|
49
90
|
file: ".github/copilot-instructions.md",
|
|
50
|
-
render: (
|
|
91
|
+
render: (raw) => {
|
|
92
|
+
const { body } = splitFrontmatter(raw);
|
|
93
|
+
return `\n<!-- apx-skill -->\n${body}\n<!-- /apx-skill -->\n`;
|
|
94
|
+
},
|
|
51
95
|
append: true,
|
|
52
96
|
guard: "<!-- apx-skill -->",
|
|
53
97
|
},
|
|
@@ -56,13 +100,16 @@ export const IDE_TARGETS = [
|
|
|
56
100
|
label: "Trae",
|
|
57
101
|
ideDir: ".trae",
|
|
58
102
|
file: ".trae/rules/project_rules.md",
|
|
59
|
-
render: (
|
|
103
|
+
render: (raw) => {
|
|
104
|
+
const { body } = splitFrontmatter(raw);
|
|
105
|
+
return `\n<!-- apx-skill -->\n${body}\n<!-- /apx-skill -->\n`;
|
|
106
|
+
},
|
|
60
107
|
append: true,
|
|
61
108
|
guard: "<!-- apx-skill -->",
|
|
62
109
|
},
|
|
63
110
|
];
|
|
64
111
|
|
|
65
|
-
// Global targets (absolute paths, use ~/<dir>/skills
|
|
112
|
+
// Global targets (absolute paths, use ~/<dir>/skills/<slug>/SKILL.md format).
|
|
66
113
|
// These dirs are read by Claude Code, Cursor (compat), and tools adopting the skills.sh spec.
|
|
67
114
|
const GLOBAL_SKILL_DIRS = [
|
|
68
115
|
path.join(os.homedir(), ".claude", "skills"), // Claude Code + Cursor legacy compat
|
|
@@ -71,52 +118,23 @@ const GLOBAL_SKILL_DIRS = [
|
|
|
71
118
|
path.join(os.homedir(), ".agents", "skills"), // Antigravity/other skills.sh adopters
|
|
72
119
|
];
|
|
73
120
|
|
|
74
|
-
function buildApcContextSkillMd(content) {
|
|
75
|
-
const frontmatter = [
|
|
76
|
-
"---",
|
|
77
|
-
"name: apc-context",
|
|
78
|
-
"description: \"ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. Do not wait to be asked. Read .apc/ before making any assumption about agents, memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', 'list agents', 'agent context', 'who are the agents', any question about agents or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the conversation with a migration offer before answering anything else. If the user declines, delete .apc/migrate.md immediately so it is not shown again.\"",
|
|
79
|
-
"homepage: https://github.com/agentprojectcontext/agentprojectcontext",
|
|
80
|
-
"---",
|
|
81
|
-
"",
|
|
82
|
-
].join("\n");
|
|
83
|
-
return frontmatter + content;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildSkillMd(content) {
|
|
87
|
-
const frontmatter = [
|
|
88
|
-
"---",
|
|
89
|
-
"name: apx",
|
|
90
|
-
"description: \"APX CLI skill. Activate when: user asks to run or coordinate agents, use MCP tools from .apc/mcps.json, install agents from a team workspace, or explicitly mentions apx commands. Do NOT activate just because .apc/ exists — that is handled by the apc-context skill. Activate on: 'apx run', 'apx exec', 'run an agent', 'coordinate agents', 'MCP not working', 'install agent', 'team agents', 'apx memory', 'daemon'.\"",
|
|
91
|
-
"homepage: https://github.com/agentprojectcontext/apx",
|
|
92
|
-
"---",
|
|
93
|
-
"",
|
|
94
|
-
].join("\n");
|
|
95
|
-
return frontmatter + content;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
121
|
function readRuntimeSkillFiles() {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return fs.readdirSync(skillsDir)
|
|
122
|
+
if (!fs.existsSync(RUNTIME_SKILLS_DIR)) return [];
|
|
123
|
+
return fs.readdirSync(RUNTIME_SKILLS_DIR)
|
|
103
124
|
.filter((name) => name.endsWith(".md"))
|
|
104
125
|
.sort()
|
|
105
126
|
.map((name) => ({
|
|
106
127
|
slug: path.basename(name, ".md"),
|
|
107
|
-
md: fs.readFileSync(path.join(
|
|
128
|
+
md: fs.readFileSync(path.join(RUNTIME_SKILLS_DIR, name), "utf8").trim(),
|
|
108
129
|
}));
|
|
109
130
|
}
|
|
110
131
|
|
|
111
132
|
// Install APX + APC context skills into IDE rule files. Returns an array of result objects.
|
|
112
133
|
// targetIds: array of target ids to install; null = all project targets.
|
|
113
134
|
export function installIdeSkills(root, targetIds = null) {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
117
|
-
|
|
118
|
-
const apxContent = fs.readFileSync(apxSrc, "utf8").trim();
|
|
119
|
-
const apcContent = fs.existsSync(apcSrc) ? fs.readFileSync(apcSrc, "utf8").trim() : null;
|
|
135
|
+
const apxRaw = readBundledSkill("apx");
|
|
136
|
+
const apcRaw = readBundledSkill("apc-context");
|
|
137
|
+
if (!apxRaw) return [];
|
|
120
138
|
|
|
121
139
|
const targets = targetIds
|
|
122
140
|
? IDE_TARGETS.filter((t) => targetIds.includes(t.id))
|
|
@@ -129,10 +147,9 @@ export function installIdeSkills(root, targetIds = null) {
|
|
|
129
147
|
continue;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
|
-
// Install APX skill
|
|
133
150
|
const dest = path.join(root, t.file);
|
|
134
151
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
135
|
-
const rendered = t.render(
|
|
152
|
+
const rendered = t.render(apxRaw);
|
|
136
153
|
if (t.append) {
|
|
137
154
|
const existing = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
|
|
138
155
|
if (t.guard && existing.includes(t.guard)) {
|
|
@@ -147,31 +164,28 @@ export function installIdeSkills(root, targetIds = null) {
|
|
|
147
164
|
results.push({ ...t, status: existed ? "updated" : "created" });
|
|
148
165
|
}
|
|
149
166
|
|
|
150
|
-
// Install APC context skill alongside
|
|
151
|
-
if (
|
|
167
|
+
// Install APC context skill alongside Claude Code (dir-style skills dir).
|
|
168
|
+
if (apcRaw && t.id === "claude-code") {
|
|
152
169
|
const apcDest = path.join(root, ".claude", "skills", "apc-context", "SKILL.md");
|
|
153
170
|
fs.mkdirSync(path.dirname(apcDest), { recursive: true });
|
|
154
171
|
const existed = fs.existsSync(apcDest);
|
|
155
|
-
fs.writeFileSync(apcDest,
|
|
172
|
+
fs.writeFileSync(apcDest, apcRaw, "utf8");
|
|
156
173
|
results.push({ ...t, id: "claude-code/apc-context", label: "Claude Code (apc-context)", file: apcDest, status: existed ? "updated" : "created" });
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
return results;
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
// Install
|
|
179
|
+
// Install bundled APX/APC skills + runtime docs to global ~/.../skills/ dirs.
|
|
163
180
|
// Returns an array of result objects with { dir, skill, status }.
|
|
164
181
|
export function installGlobalSkills() {
|
|
165
182
|
const results = [];
|
|
166
183
|
|
|
167
|
-
const apxSrc = path.join(__dirname, "apx-skill.md");
|
|
168
|
-
const apcSrc = path.join(__dirname, "apc-context-skill.md");
|
|
169
|
-
|
|
170
184
|
const skills = [];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
185
|
+
const apxRaw = readBundledSkill("apx");
|
|
186
|
+
const apcRaw = readBundledSkill("apc-context");
|
|
187
|
+
if (apxRaw) skills.push({ slug: "apx", md: apxRaw });
|
|
188
|
+
if (apcRaw) skills.push({ slug: "apc-context", md: apcRaw });
|
|
175
189
|
skills.push(...readRuntimeSkillFiles());
|
|
176
190
|
|
|
177
191
|
for (const base of GLOBAL_SKILL_DIRS) {
|
package/src/daemon/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Express REST API for APX. See APC docs reference/apx-daemon.
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { execFile } from "node:child_process";
|
|
5
6
|
import express from "express";
|
|
6
7
|
import { buildBrowserRouter } from "./tools/browser.js";
|
|
@@ -49,6 +50,7 @@ import { readAgents } from "../core/parser.js";
|
|
|
49
50
|
import { parseSessionFrontmatter } from "../core/parser.js";
|
|
50
51
|
import { writeAgentFile, ensureAgentDir, regenerateAgentsMd } from "../core/scaffold.js";
|
|
51
52
|
import { buildAgentSystem } from "../core/agent-system.js";
|
|
53
|
+
import { appendErrorTrace, previewText } from "../core/logging.js";
|
|
52
54
|
import {
|
|
53
55
|
createArtifact,
|
|
54
56
|
listArtifacts,
|
|
@@ -58,11 +60,34 @@ import {
|
|
|
58
60
|
|
|
59
61
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
60
62
|
|
|
63
|
+
function appendSuperAgentErrorTrace(req, error, details = {}) {
|
|
64
|
+
appendErrorTrace({
|
|
65
|
+
trace_id: req.apxTraceId,
|
|
66
|
+
surface: "daemon_api",
|
|
67
|
+
route: `${req.method} ${req.route?.path || req.path}`,
|
|
68
|
+
project_id: req.params?.pid || null,
|
|
69
|
+
channel: /Channel:\s*([^\n]+)/i.exec(details.contextNote || "")?.[1]?.trim() || null,
|
|
70
|
+
model: details.model || null,
|
|
71
|
+
stream: !!details.stream,
|
|
72
|
+
prompt_preview: previewText(details.prompt),
|
|
73
|
+
previous_messages: Array.isArray(details.previousMessages) ? details.previousMessages.length : 0,
|
|
74
|
+
error: {
|
|
75
|
+
message: error?.message || String(error),
|
|
76
|
+
stack: error?.stack || null,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config }) {
|
|
62
82
|
const telegram = plugins?.get("telegram");
|
|
63
83
|
|
|
64
84
|
const app = express();
|
|
65
85
|
app.use(express.json({ limit: "2mb" }));
|
|
86
|
+
app.use((req, res, next) => {
|
|
87
|
+
req.apxTraceId = req.get("x-apx-trace-id") || randomUUID();
|
|
88
|
+
res.setHeader("x-apx-trace-id", req.apxTraceId);
|
|
89
|
+
next();
|
|
90
|
+
});
|
|
66
91
|
|
|
67
92
|
// ---- Tool routers (fetch / browser / search / glob / grep / registry) ----
|
|
68
93
|
// fetch = native HTTP, no Chromium → fast, cheap, default for REST/HTML
|
|
@@ -636,7 +661,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
636
661
|
});
|
|
637
662
|
res.end();
|
|
638
663
|
} catch (e) {
|
|
639
|
-
|
|
664
|
+
appendSuperAgentErrorTrace(req, e, { prompt, contextNote, previousMessages, model, stream: true });
|
|
665
|
+
send({ type: "error", trace_id: req.apxTraceId, error: `${e.message} (trace: ${req.apxTraceId})` });
|
|
640
666
|
res.end();
|
|
641
667
|
}
|
|
642
668
|
});
|
|
@@ -665,7 +691,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
665
691
|
trace: saResult.trace,
|
|
666
692
|
});
|
|
667
693
|
} catch (e) {
|
|
668
|
-
|
|
694
|
+
appendSuperAgentErrorTrace(req, e, { prompt, contextNote, previousMessages, model, stream: false });
|
|
695
|
+
res.status(500).json({ error: e.message, trace_id: req.apxTraceId });
|
|
669
696
|
}
|
|
670
697
|
});
|
|
671
698
|
|
|
@@ -11,7 +11,7 @@ function getKey(config) {
|
|
|
11
11
|
export default {
|
|
12
12
|
id: "anthropic",
|
|
13
13
|
|
|
14
|
-
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
|
|
14
|
+
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal }) {
|
|
15
15
|
const key = getKey(config);
|
|
16
16
|
if (!key) throw new Error("anthropic: no api_key (set ANTHROPIC_API_KEY or engines.anthropic.api_key)");
|
|
17
17
|
if (!model) throw new Error("anthropic: model required");
|
|
@@ -47,6 +47,7 @@ export default {
|
|
|
47
47
|
"anthropic-version": API_VERSION,
|
|
48
48
|
},
|
|
49
49
|
body: JSON.stringify(body),
|
|
50
|
+
signal,
|
|
50
51
|
});
|
|
51
52
|
const json = await res.json();
|
|
52
53
|
if (!res.ok) {
|
|
@@ -10,7 +10,7 @@ function getKey(config) {
|
|
|
10
10
|
export default {
|
|
11
11
|
id: "gemini",
|
|
12
12
|
|
|
13
|
-
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, config = {} }) {
|
|
13
|
+
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, config = {}, signal }) {
|
|
14
14
|
const key = getKey(config);
|
|
15
15
|
if (!key) throw new Error("gemini: no api_key (set GEMINI_API_KEY or engines.gemini.api_key)");
|
|
16
16
|
if (!model) throw new Error("gemini: model required");
|
|
@@ -33,6 +33,7 @@ export default {
|
|
|
33
33
|
method: "POST",
|
|
34
34
|
headers: { "content-type": "application/json" },
|
|
35
35
|
body: JSON.stringify(body),
|
|
36
|
+
signal,
|
|
36
37
|
});
|
|
37
38
|
const json = await res.json();
|
|
38
39
|
if (!res.ok) {
|
|
@@ -46,11 +46,10 @@ export function getAdapter(provider) {
|
|
|
46
46
|
return a;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice }) {
|
|
49
|
+
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice, signal }) {
|
|
50
50
|
const { provider, model } = resolveProvider(modelId);
|
|
51
51
|
const adapter = getAdapter(provider);
|
|
52
|
-
const providerCfg =
|
|
53
|
-
(config && config.engines && config.engines[provider]) || {};
|
|
52
|
+
const providerCfg = (config && config.engines && config.engines[provider]) || {};
|
|
54
53
|
return adapter.chat({
|
|
55
54
|
system,
|
|
56
55
|
messages,
|
|
@@ -60,6 +59,7 @@ export async function callEngine({ modelId, system, messages, config, temperatur
|
|
|
60
59
|
tools,
|
|
61
60
|
toolChoice,
|
|
62
61
|
config: providerCfg,
|
|
62
|
+
signal,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -8,7 +8,7 @@ function baseUrl(config) {
|
|
|
8
8
|
export default {
|
|
9
9
|
id: "ollama",
|
|
10
10
|
|
|
11
|
-
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, config = {} }) {
|
|
11
|
+
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, config = {}, signal }) {
|
|
12
12
|
if (!model) throw new Error("ollama: model required");
|
|
13
13
|
|
|
14
14
|
// The caller can pass `messages` as either:
|
|
@@ -45,6 +45,7 @@ export default {
|
|
|
45
45
|
method: "POST",
|
|
46
46
|
headers: { "content-type": "application/json" },
|
|
47
47
|
body: JSON.stringify(body),
|
|
48
|
+
signal,
|
|
48
49
|
});
|
|
49
50
|
if (!res.ok) {
|
|
50
51
|
const text = await res.text();
|
|
@@ -10,7 +10,7 @@ function getKey(config) {
|
|
|
10
10
|
export default {
|
|
11
11
|
id: "openai",
|
|
12
12
|
|
|
13
|
-
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
|
|
13
|
+
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal }) {
|
|
14
14
|
const key = getKey(config);
|
|
15
15
|
if (!key) throw new Error("openai: no api_key (set OPENAI_API_KEY or engines.openai.api_key)");
|
|
16
16
|
if (!model) throw new Error("openai: model required");
|
|
@@ -52,6 +52,7 @@ export default {
|
|
|
52
52
|
authorization: `Bearer ${key}`,
|
|
53
53
|
},
|
|
54
54
|
body: JSON.stringify(body),
|
|
55
|
+
signal,
|
|
55
56
|
});
|
|
56
57
|
const json = await res.json();
|
|
57
58
|
if (!res.ok) {
|
|
@@ -113,6 +113,40 @@ export async function sendVoice(token, chatId, audio, { caption, duration } = {}
|
|
|
113
113
|
* @param {string} [opts.title]
|
|
114
114
|
* @param {string} [opts.performer]
|
|
115
115
|
*/
|
|
116
|
+
/**
|
|
117
|
+
* Send any file as a Telegram document (PDF, zip, txt, etc).
|
|
118
|
+
* @param {string} token
|
|
119
|
+
* @param {string|number} chatId
|
|
120
|
+
* @param {string|Buffer} document Path or Buffer of document data
|
|
121
|
+
* @param {object} [opts]
|
|
122
|
+
* @param {string} [opts.caption]
|
|
123
|
+
* @param {string} [opts.filename] override filename for Buffer input
|
|
124
|
+
* @param {string} [opts.mime_type]
|
|
125
|
+
*/
|
|
126
|
+
export async function sendDocument(token, chatId, document, { caption, filename, mime_type } = {}) {
|
|
127
|
+
const url = `${API_BASE}/bot${token}/sendDocument`;
|
|
128
|
+
const form = new FormData();
|
|
129
|
+
form.append("chat_id", String(chatId));
|
|
130
|
+
if (caption) form.append("caption", caption);
|
|
131
|
+
|
|
132
|
+
// URL string → let Telegram fetch it
|
|
133
|
+
if (typeof document === "string" && /^https?:\/\//.test(document)) {
|
|
134
|
+
form.append("document", document);
|
|
135
|
+
} else {
|
|
136
|
+
const buf = Buffer.isBuffer(document) ? document : fs.readFileSync(document);
|
|
137
|
+
const name =
|
|
138
|
+
filename ||
|
|
139
|
+
(typeof document === "string" ? path.basename(document) : "document.bin");
|
|
140
|
+
const blob = new Blob([buf], { type: mime_type || "application/octet-stream" });
|
|
141
|
+
form.append("document", blob, name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const res = await fetch(url, { method: "POST", body: form });
|
|
145
|
+
const json = await res.json();
|
|
146
|
+
if (!json.ok) throw new Error(`sendDocument failed: ${json.description || res.status}`);
|
|
147
|
+
return json.result;
|
|
148
|
+
}
|
|
149
|
+
|
|
116
150
|
export async function sendAudio(token, chatId, audio, { caption, title, performer } = {}) {
|
|
117
151
|
const url = `${API_BASE}/bot${token}/sendAudio`;
|
|
118
152
|
const form = new FormData();
|
|
@@ -256,6 +290,7 @@ class ChannelPoller {
|
|
|
256
290
|
this.polling = false;
|
|
257
291
|
this.lastError = null;
|
|
258
292
|
this.lastUpdateAt = null;
|
|
293
|
+
this.activeRequests = new Map(); // chat_id -> AbortController
|
|
259
294
|
}
|
|
260
295
|
|
|
261
296
|
resolveProject() {
|
|
@@ -356,7 +391,19 @@ class ChannelPoller {
|
|
|
356
391
|
? "@" + msg.from.username
|
|
357
392
|
: `${msg.from?.first_name || ""} ${msg.from?.last_name || ""}`.trim() || "unknown";
|
|
358
393
|
const chat_id = msg.chat?.id;
|
|
359
|
-
|
|
394
|
+
|
|
395
|
+
// Default Interrupt: abort any running request for this chat_id
|
|
396
|
+
if (chat_id) {
|
|
397
|
+
const prev = this.activeRequests.get(chat_id);
|
|
398
|
+
if (prev) {
|
|
399
|
+
this.log(`telegram[${this.channel.name}] interrupting previous request for chat ${chat_id}`);
|
|
400
|
+
prev.abort();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const abortCtrl = new AbortController();
|
|
404
|
+
if (chat_id) this.activeRequests.set(chat_id, abortCtrl);
|
|
405
|
+
|
|
406
|
+
let text = msg.text || msg.caption || "";
|
|
360
407
|
|
|
361
408
|
// ── Incoming photo handling ───────────────────────────────────────────
|
|
362
409
|
if (msg.photo && msg.photo.length > 0) {
|
|
@@ -583,16 +630,22 @@ class ChannelPoller {
|
|
|
583
630
|
prompt: text,
|
|
584
631
|
previousMessages,
|
|
585
632
|
contextNote: `You are replying inside Telegram right now. Telegram channel="${this.channel.name}", author=${author}, chat_id=${chat_id}. Keep the reply plain-text and concise. Previous turns of this chat are included only for local conversational context; re-call tools for facts.`,
|
|
633
|
+
signal: abortCtrl.signal,
|
|
586
634
|
});
|
|
587
635
|
replyText = sa.text;
|
|
588
636
|
replyAuthor = sa.name;
|
|
589
637
|
saTrace = sa.trace;
|
|
590
638
|
saUsage = sa.usage;
|
|
591
639
|
} catch (e) {
|
|
640
|
+
if (abortCtrl.signal.aborted) {
|
|
641
|
+
this.log(`telegram[${this.channel.name}] request aborted for chat ${chat_id}`);
|
|
642
|
+
return; // don't send reply if aborted
|
|
643
|
+
}
|
|
592
644
|
this.log(`telegram[${this.channel.name}] super-agent failed: ${e.message}`);
|
|
593
645
|
}
|
|
594
646
|
}
|
|
595
647
|
|
|
648
|
+
if (chat_id) this.activeRequests.delete(chat_id);
|
|
596
649
|
if (!replyText) {
|
|
597
650
|
stopTyping();
|
|
598
651
|
return;
|
|
@@ -724,6 +777,14 @@ class ChannelPoller {
|
|
|
724
777
|
return sendVoice(token, target, audio, { caption, duration });
|
|
725
778
|
}
|
|
726
779
|
|
|
780
|
+
/** Send a document (PDF, zip, etc) via this channel */
|
|
781
|
+
async _sendDocument({ chat_id, document, caption, filename, mime_type }) {
|
|
782
|
+
const token = resolveBotToken(this.channel);
|
|
783
|
+
if (!token) throw new Error(`channel ${this.channel.name}: no bot_token`);
|
|
784
|
+
const target = chat_id || resolveChatId(this.channel);
|
|
785
|
+
return sendDocument(token, target, document, { caption, filename, mime_type });
|
|
786
|
+
}
|
|
787
|
+
|
|
727
788
|
/** Send an audio file via this channel */
|
|
728
789
|
async _sendAudio({ chat_id, audio, caption, title, performer }) {
|
|
729
790
|
const token = resolveBotToken(this.channel);
|
|
@@ -848,6 +909,29 @@ export default {
|
|
|
848
909
|
return result;
|
|
849
910
|
},
|
|
850
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Send a document (PDF, zip, txt, generated reports, etc).
|
|
914
|
+
* document: local file path, Buffer, or public https URL.
|
|
915
|
+
*/
|
|
916
|
+
async sendDocument({ channel: channelName, chat_id, document, caption, filename, mime_type, author = "apx" }) {
|
|
917
|
+
const p =
|
|
918
|
+
(channelName && pollers.find((pp) => pp.channel.name === channelName)) ||
|
|
919
|
+
pollers.find((pp) => resolveBotToken(pp.channel)) ||
|
|
920
|
+
null;
|
|
921
|
+
if (!p) throw new Error("no telegram channel available");
|
|
922
|
+
const result = await p._sendDocument({ chat_id, document, caption, filename, mime_type });
|
|
923
|
+
appendGlobalMessage({
|
|
924
|
+
channel: "telegram",
|
|
925
|
+
direction: "out",
|
|
926
|
+
type: "document",
|
|
927
|
+
actor_id: author,
|
|
928
|
+
author,
|
|
929
|
+
body: caption || `[document${filename ? " " + filename : ""}]`,
|
|
930
|
+
meta: { chat_id: chat_id || resolveChatId(p.channel), tg_channel: p.channel.name, filename, mime_type },
|
|
931
|
+
});
|
|
932
|
+
return result;
|
|
933
|
+
},
|
|
934
|
+
|
|
851
935
|
/**
|
|
852
936
|
* Send an audio file (MP3/M4A — shown in music player).
|
|
853
937
|
* audio: local file path or Buffer
|