@agentprojectcontext/apx 1.6.0 → 1.8.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/README.md +4 -0
- package/package.json +1 -1
- package/src/cli/commands/config.js +23 -0
- package/src/cli/commands/messages.js +45 -0
- package/src/cli/commands/routine.js +27 -2
- package/src/cli/commands/setup.js +2 -2
- package/src/cli/index.js +969 -3
- package/src/core/apc-context-skill.md +3 -0
- package/src/core/apx-skill.md +30 -0
- package/src/core/config.js +2 -0
- package/src/core/mascot.js +5 -7
- package/src/core/messages-store.js +94 -20
- package/src/core/routines-store.js +3 -1
- package/src/daemon/api.js +3 -3
- package/src/daemon/index.js +38 -2
- package/src/daemon/plugins/telegram.js +32 -2
- package/src/daemon/routines.js +64 -2
- package/src/daemon/super-agent-tools/helpers.js +120 -0
- package/src/daemon/super-agent-tools/index.js +56 -0
- package/src/daemon/super-agent-tools/tools/add-project.js +36 -0
- package/src/daemon/super-agent-tools/tools/call-agent.js +45 -0
- package/src/daemon/super-agent-tools/tools/call-mcp.js +30 -0
- package/src/daemon/super-agent-tools/tools/call-runtime.js +107 -0
- package/src/daemon/super-agent-tools/tools/edit-file.js +44 -0
- package/src/daemon/super-agent-tools/tools/import-agent.js +48 -0
- package/src/daemon/super-agent-tools/tools/list-agents.js +36 -0
- package/src/daemon/super-agent-tools/tools/list-files.js +38 -0
- package/src/daemon/super-agent-tools/tools/list-mcps.js +48 -0
- package/src/daemon/super-agent-tools/tools/list-projects.js +20 -0
- package/src/daemon/super-agent-tools/tools/list-vault-agents.js +18 -0
- package/src/daemon/super-agent-tools/tools/read-agent-memory.js +28 -0
- package/src/daemon/super-agent-tools/tools/read-file.js +33 -0
- package/src/daemon/super-agent-tools/tools/run-shell.js +86 -0
- package/src/daemon/super-agent-tools/tools/search-messages.js +34 -0
- package/src/daemon/super-agent-tools/tools/send-telegram.js +30 -0
- package/src/daemon/super-agent-tools/tools/set-identity.js +35 -0
- package/src/daemon/super-agent-tools/tools/set-permission-mode.js +32 -0
- package/src/daemon/super-agent-tools/tools/tail-messages.js +39 -0
- package/src/daemon/super-agent-tools/tools/write-file.js +33 -0
- package/src/daemon/super-agent-tools.js +1 -539
- package/src/daemon/super-agent.js +56 -7
|
@@ -95,6 +95,9 @@ APX can provide a local daemon, MCP management, Telegram bridge, routines, and r
|
|
|
95
95
|
across Codex, Claude Code, OpenCode, Aider, or direct LLM engines. Those are APX runtime features,
|
|
96
96
|
not APC portable-core requirements.
|
|
97
97
|
|
|
98
|
+
The APX super-agent uses `~/.apx/projects/default` for system-level work when no project is named.
|
|
99
|
+
APX routines can run heartbeat, shell, Telegram, project agent, or super-agent tasks on a schedule.
|
|
100
|
+
|
|
98
101
|
APX runtime state belongs outside the repository:
|
|
99
102
|
|
|
100
103
|
```text
|
package/src/core/apx-skill.md
CHANGED
|
@@ -5,6 +5,9 @@ The daemon runs on `127.0.0.1:7430` and auto-starts on first `apx` call.
|
|
|
5
5
|
APX reads APC project context from `.apc/`, but APX runtime state belongs outside the repository
|
|
6
6
|
under `~/.apx/projects/<project-id>/`.
|
|
7
7
|
|
|
8
|
+
The APX super-agent has an always-available default workspace at `~/.apx/projects/default`.
|
|
9
|
+
When no project is named, system-level work belongs there.
|
|
10
|
+
|
|
8
11
|
---
|
|
9
12
|
|
|
10
13
|
## Coordinate with other agents
|
|
@@ -73,10 +76,37 @@ apx session check # exits 1 if session already ac
|
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
78
|
apx messages tail # last 50 messages, all channels
|
|
79
|
+
apx messages chat --channel telegram -n 20 # chat view with user/agent/system type
|
|
76
80
|
apx messages tail --channel runtime # only agent invocations
|
|
77
81
|
apx messages tail --agent <slug> -n 20
|
|
78
82
|
```
|
|
79
83
|
|
|
84
|
+
Message rows expose `type` (`user`, `agent`, `tool`, `system`) and `actor_id`; use `messages chat`
|
|
85
|
+
when you need a readable transcript.
|
|
86
|
+
|
|
87
|
+
## Super-agent permissions
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
apx permission show
|
|
91
|
+
apx permission set automatico # total | automatico | permiso
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`automatico` runs read/list/safe shell checks directly and asks before destructive shell, MCP,
|
|
95
|
+
runtime, outbound, config, or filesystem mutation actions.
|
|
96
|
+
|
|
97
|
+
## Routines
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
apx routine list
|
|
101
|
+
apx routine get <name>
|
|
102
|
+
apx routine history <name>
|
|
103
|
+
apx routine add clima --kind super_agent --schedule every:5m \
|
|
104
|
+
--permission-mode total \
|
|
105
|
+
--spec '{"prompt":"Check weather and send Telegram update."}'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Routine kinds: `heartbeat`, `exec_agent`, `super_agent`, `telegram`, `shell`.
|
|
109
|
+
|
|
80
110
|
## APC_RESULT
|
|
81
111
|
|
|
82
112
|
Print on the last meaningful line of your output so the invoker captures it:
|
package/src/core/config.js
CHANGED
|
@@ -46,6 +46,8 @@ const DEFAULT_CONFIG = {
|
|
|
46
46
|
name: "apx",
|
|
47
47
|
model: "", // e.g. "ollama:llama3.2:3b"
|
|
48
48
|
system: "", // optional override; defaults baked into super-agent.js
|
|
49
|
+
permission_mode: "automatico", // total | automatico | permiso
|
|
50
|
+
allowed_tools: [], // used by permission_mode="permiso"
|
|
49
51
|
},
|
|
50
52
|
engines: {
|
|
51
53
|
anthropic: { api_key: "" },
|
package/src/core/mascot.js
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
const R = "\x1b[0m";
|
|
5
5
|
const B = "\x1b[1m";
|
|
6
6
|
const W = "\x1b[97m"; // bright white
|
|
7
|
-
const K = "\x1b[30m"; // black
|
|
8
7
|
const BK = "\x1b[40m"; // bg black
|
|
9
|
-
const BW = "\x1b[47m"; // bg white
|
|
10
8
|
const CY = "\x1b[36m";
|
|
11
9
|
const YE = "\x1b[33m";
|
|
12
10
|
const GR = "\x1b[32m";
|
|
@@ -34,10 +32,10 @@ const MOODS = {
|
|
|
34
32
|
wave: {
|
|
35
33
|
color: CY,
|
|
36
34
|
lines: [
|
|
37
|
-
` ${BK}${W} ▄███████▄ ${R}
|
|
38
|
-
` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
|
|
39
|
-
` ${BK}${W} █ ◕ ◕ █ ${R}`,
|
|
40
|
-
` ${BK}${W} █ ╰▽╯ █ ${R}`,
|
|
35
|
+
` ${BK}${W} ▄███████▄ ${R} ${DI}/)${R}`,
|
|
36
|
+
` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R} ${DI}//${R}`,
|
|
37
|
+
` ${BK}${W} █ ◕ ◕ █ ${R} ${DI}//${R}`,
|
|
38
|
+
` ${BK}${W} █ ╰▽╯ █ ${R}${DI}/${R}`,
|
|
41
39
|
` ${BK}${W} ▀███████▀ ${R}`,
|
|
42
40
|
` ${DI} ╱ ╲ ╱ ╲ ${R}`,
|
|
43
41
|
],
|
|
@@ -76,7 +74,7 @@ const MOODS = {
|
|
|
76
74
|
excited: {
|
|
77
75
|
color: BL,
|
|
78
76
|
lines: [
|
|
79
|
-
` ${BK}${W} ▄███████▄ ${R} ${BL}
|
|
77
|
+
` ${BK}${W} ▄███████▄ ${R} ${BL}↑${R}`,
|
|
80
78
|
` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
|
|
81
79
|
` ${BK}${W} █ ★ ★ █ ${R}`,
|
|
82
80
|
` ${BK}${W} █ ╰◡╯ █ ${R}`,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// ~/.apx/messages/<channel>/YYYY-MM-DD.jsonl
|
|
8
8
|
//
|
|
9
9
|
// Each line:
|
|
10
|
-
// {"ts":"...","channel":"...","direction":"in|out","author":"...","body":"...","meta":{...}}
|
|
10
|
+
// {"ts":"...","channel":"...","direction":"in|out","type":"user|agent|tool|system","author":"...","actor_id":"...","body":"...","meta":{...}}
|
|
11
11
|
//
|
|
12
12
|
// Why JSONL: same shape as Claude Code's ~/.claude/projects/<id>.jsonl.
|
|
13
13
|
// Streamable, structured, no markdown parsing fragility.
|
|
@@ -32,24 +32,61 @@ function dayPathMd(projectRoot, ts) {
|
|
|
32
32
|
return path.join(projectRoot, "messages", `${day}.md`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const VALID_MESSAGE_TYPES = new Set(["user", "agent", "tool", "system"]);
|
|
36
|
+
|
|
37
|
+
function normalizeMessageType(type) {
|
|
38
|
+
return typeof type === "string" && VALID_MESSAGE_TYPES.has(type) ? type : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function inferMessageType({ type, channel, direction, author, agent_slug, meta = {} } = {}) {
|
|
42
|
+
const explicit = normalizeMessageType(type) || normalizeMessageType(meta.type) || normalizeMessageType(meta.actor_type);
|
|
43
|
+
if (explicit) return explicit;
|
|
44
|
+
if (channel === "a2a") return "agent";
|
|
45
|
+
if (meta.tool || meta.tool_name) return "tool";
|
|
46
|
+
if (author === "system") return "system";
|
|
47
|
+
if (agent_slug && author && author !== "user" && !String(author).startsWith("@")) return "agent";
|
|
48
|
+
if (direction === "in" && (author === "user" || String(author || "").startsWith("@"))) return "user";
|
|
49
|
+
if (direction === "out") return "agent";
|
|
50
|
+
return direction === "in" ? "user" : "agent";
|
|
51
|
+
}
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
function inferActorId({ type, actor_id, author, agent_slug, meta = {} } = {}) {
|
|
54
|
+
if (actor_id) return actor_id;
|
|
55
|
+
if (meta.actor_id) return meta.actor_id;
|
|
56
|
+
if (type === "user") return meta.user_id ? String(meta.user_id) : (author || "user");
|
|
57
|
+
if (type === "agent") return agent_slug || author || "agent";
|
|
58
|
+
if (type === "tool") return meta.tool || meta.tool_name || author || "tool";
|
|
59
|
+
if (type === "system") return author || "system";
|
|
60
|
+
return author || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function messageMeta({ type, actor_id, agent_slug, session_id, external_id, meta = {} }) {
|
|
64
|
+
return {
|
|
65
|
+
...meta,
|
|
66
|
+
type,
|
|
67
|
+
...(actor_id ? { actor_id } : {}),
|
|
42
68
|
...(agent_slug ? { agent: agent_slug } : {}),
|
|
43
69
|
...(session_id ? { session_id } : {}),
|
|
44
70
|
...(external_id ? { external_id } : {}),
|
|
45
|
-
...meta,
|
|
46
71
|
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function appendMessageToFs({ projectRoot, channel, direction, type, actor_id, author, body, meta = {}, ts, agent_slug, session_id, external_id }) {
|
|
75
|
+
ts = ts || nowIso();
|
|
76
|
+
const file = dayPathJsonl(projectRoot, ts);
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
78
|
+
|
|
79
|
+
const msgType = inferMessageType({ type, channel, direction, author, agent_slug, meta });
|
|
80
|
+
const msgActorId = inferActorId({ type: msgType, actor_id, author, agent_slug, meta });
|
|
81
|
+
const fullMeta = messageMeta({ type: msgType, actor_id: msgActorId, agent_slug, session_id, external_id, meta });
|
|
47
82
|
|
|
48
83
|
const record = {
|
|
49
84
|
ts,
|
|
50
85
|
channel,
|
|
51
86
|
direction,
|
|
87
|
+
type: msgType,
|
|
52
88
|
author: author || null,
|
|
89
|
+
...(msgActorId ? { actor_id: msgActorId } : {}),
|
|
53
90
|
body: body || "",
|
|
54
91
|
...(Object.keys(fullMeta).length ? { meta: fullMeta } : {}),
|
|
55
92
|
};
|
|
@@ -65,6 +102,16 @@ export function insertMessageRow(db, m) {
|
|
|
65
102
|
const a = db.prepare("SELECT id FROM agents WHERE slug = ?").get(m.agent_slug);
|
|
66
103
|
if (a) agent_id = a.id;
|
|
67
104
|
}
|
|
105
|
+
const type = inferMessageType(m);
|
|
106
|
+
const actor_id = inferActorId({ ...m, type });
|
|
107
|
+
const meta = messageMeta({
|
|
108
|
+
type,
|
|
109
|
+
actor_id,
|
|
110
|
+
agent_slug: m.agent_slug,
|
|
111
|
+
session_id: m.session_id,
|
|
112
|
+
external_id: m.external_id,
|
|
113
|
+
meta: m.meta || {},
|
|
114
|
+
});
|
|
68
115
|
return db
|
|
69
116
|
.prepare(
|
|
70
117
|
`INSERT INTO messages (agent_id, session_id, channel, direction, external_id, author, body, meta_json, ts)
|
|
@@ -78,17 +125,19 @@ export function insertMessageRow(db, m) {
|
|
|
78
125
|
m.external_id || null,
|
|
79
126
|
m.author || null,
|
|
80
127
|
m.body || "",
|
|
81
|
-
JSON.stringify(
|
|
128
|
+
JSON.stringify(meta),
|
|
82
129
|
m.ts
|
|
83
130
|
);
|
|
84
131
|
}
|
|
85
132
|
|
|
86
133
|
// Single entry point used by everywhere the daemon writes a message.
|
|
87
|
-
export function appendMessage({ projectRoot, db, channel, direction, author, body, meta = {}, ts, agent_slug, session_id, external_id }) {
|
|
134
|
+
export function appendMessage({ projectRoot, db, channel, direction, type, actor_id, author, body, meta = {}, ts, agent_slug, session_id, external_id }) {
|
|
88
135
|
const written = appendMessageToFs({
|
|
89
136
|
projectRoot,
|
|
90
137
|
channel,
|
|
91
138
|
direction,
|
|
139
|
+
type,
|
|
140
|
+
actor_id,
|
|
92
141
|
author,
|
|
93
142
|
body,
|
|
94
143
|
meta,
|
|
@@ -100,6 +149,8 @@ export function appendMessage({ projectRoot, db, channel, direction, author, bod
|
|
|
100
149
|
insertMessageRow(db, {
|
|
101
150
|
channel,
|
|
102
151
|
direction,
|
|
152
|
+
type,
|
|
153
|
+
actor_id,
|
|
103
154
|
author,
|
|
104
155
|
body,
|
|
105
156
|
meta,
|
|
@@ -121,15 +172,33 @@ export function parseDayJsonl(text) {
|
|
|
121
172
|
try { obj = JSON.parse(trimmed); } catch { continue; }
|
|
122
173
|
if (!obj || typeof obj !== "object") continue;
|
|
123
174
|
const meta = obj.meta || {};
|
|
175
|
+
const agent_slug = obj.agent_slug || meta.agent;
|
|
176
|
+
const type = inferMessageType({
|
|
177
|
+
type: obj.type,
|
|
178
|
+
channel: obj.channel,
|
|
179
|
+
direction: obj.direction,
|
|
180
|
+
author: obj.author,
|
|
181
|
+
agent_slug,
|
|
182
|
+
meta,
|
|
183
|
+
});
|
|
184
|
+
const actor_id = inferActorId({
|
|
185
|
+
type,
|
|
186
|
+
actor_id: obj.actor_id,
|
|
187
|
+
author: obj.author,
|
|
188
|
+
agent_slug,
|
|
189
|
+
meta,
|
|
190
|
+
});
|
|
124
191
|
out.push({
|
|
125
192
|
ts: obj.ts,
|
|
126
193
|
channel: obj.channel,
|
|
127
194
|
direction: obj.direction,
|
|
195
|
+
type,
|
|
128
196
|
author: obj.author,
|
|
197
|
+
actor_id,
|
|
129
198
|
body: obj.body || "",
|
|
130
199
|
meta,
|
|
131
|
-
agent_slug
|
|
132
|
-
session_id: typeof meta.apc_session_id === "number" ? meta.apc_session_id : null,
|
|
200
|
+
agent_slug,
|
|
201
|
+
session_id: meta.session_id ?? (typeof meta.apc_session_id === "number" ? meta.apc_session_id : null),
|
|
133
202
|
external_id: meta.external_id,
|
|
134
203
|
});
|
|
135
204
|
}
|
|
@@ -156,10 +225,15 @@ export function parseDayFile(text) {
|
|
|
156
225
|
body = body.replace(metaMatch[0], "");
|
|
157
226
|
}
|
|
158
227
|
body = body.trim();
|
|
228
|
+
const agent_slug = meta.agent;
|
|
229
|
+
const type = inferMessageType({ channel, direction, author, agent_slug, meta });
|
|
230
|
+
const actor_id = inferActorId({ type, author, agent_slug, meta });
|
|
159
231
|
out.push({
|
|
160
232
|
ts, channel, direction, author, body, meta,
|
|
161
|
-
|
|
162
|
-
|
|
233
|
+
type,
|
|
234
|
+
actor_id,
|
|
235
|
+
agent_slug,
|
|
236
|
+
session_id: meta.session_id ?? (typeof meta.apc_session_id === "number" ? meta.apc_session_id : null),
|
|
163
237
|
external_id: meta.external_id,
|
|
164
238
|
});
|
|
165
239
|
}
|
|
@@ -343,21 +417,21 @@ export function getRecentTelegramTurnsFromFs({
|
|
|
343
417
|
// ---------------------------------------------------------------------------
|
|
344
418
|
|
|
345
419
|
// Write a message to the global channel store. No SQL cache — JSONL only.
|
|
346
|
-
export function appendGlobalMessage({ channel, direction, author, body, meta = {}, ts, agent_slug, external_id }) {
|
|
420
|
+
export function appendGlobalMessage({ channel, direction, type, actor_id, author, body, meta = {}, ts, agent_slug, external_id }) {
|
|
347
421
|
ts = ts || nowIso();
|
|
348
422
|
const dir = path.join(GLOBAL_MESSAGES_DIR, channel);
|
|
349
423
|
fs.mkdirSync(dir, { recursive: true });
|
|
350
424
|
const file = path.join(dir, `${ts.slice(0, 10)}.jsonl`);
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
...meta,
|
|
355
|
-
};
|
|
425
|
+
const msgType = inferMessageType({ type, channel, direction, author, agent_slug, meta });
|
|
426
|
+
const msgActorId = inferActorId({ type: msgType, actor_id, author, agent_slug, meta });
|
|
427
|
+
const fullMeta = messageMeta({ type: msgType, actor_id: msgActorId, agent_slug, external_id, meta });
|
|
356
428
|
const record = {
|
|
357
429
|
ts,
|
|
358
430
|
channel,
|
|
359
431
|
direction,
|
|
432
|
+
type: msgType,
|
|
360
433
|
author: author || null,
|
|
434
|
+
...(msgActorId ? { actor_id: msgActorId } : {}),
|
|
361
435
|
body: body || "",
|
|
362
436
|
...(Object.keys(fullMeta).length ? { meta: fullMeta } : {}),
|
|
363
437
|
};
|
|
@@ -75,7 +75,7 @@ export function getRoutine(projectPath, name) {
|
|
|
75
75
|
return readFile(projectPath).find((r) => r.name === name) || null;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export function upsertRoutine(projectPath, { name, kind, schedule, spec, enabled = true }) {
|
|
78
|
+
export function upsertRoutine(projectPath, { name, kind, schedule, spec, enabled = true, permission_mode, allowed_tools }) {
|
|
79
79
|
if (!name || !kind || !schedule) throw new Error("routine requires name, kind, schedule");
|
|
80
80
|
const now = nowIso();
|
|
81
81
|
const routines = readFile(projectPath);
|
|
@@ -87,6 +87,8 @@ export function upsertRoutine(projectPath, { name, kind, schedule, spec, enabled
|
|
|
87
87
|
kind,
|
|
88
88
|
schedule,
|
|
89
89
|
spec: spec || {},
|
|
90
|
+
permission_mode: permission_mode || prev?.permission_mode || null,
|
|
91
|
+
allowed_tools: Array.isArray(allowed_tools) ? allowed_tools : (prev?.allowed_tools || []),
|
|
90
92
|
enabled: enabled !== false,
|
|
91
93
|
last_run_at: prev?.last_run_at ?? null,
|
|
92
94
|
last_status: prev?.last_status ?? null,
|
package/src/daemon/api.js
CHANGED
|
@@ -348,13 +348,13 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
348
348
|
app.post("/projects/:pid/messages", (req, res) => {
|
|
349
349
|
const p = project(req, res);
|
|
350
350
|
if (!p) return;
|
|
351
|
-
const { channel, direction, agent_slug, body, meta = {}, author = null } =
|
|
351
|
+
const { channel, direction, type, actor_id, agent_slug, body, meta = {}, author = null } =
|
|
352
352
|
req.body || {};
|
|
353
353
|
if (!channel || !direction || !body)
|
|
354
354
|
return res.status(400).json({ error: "channel, direction, body required" });
|
|
355
355
|
if (!["in", "out"].includes(direction))
|
|
356
356
|
return res.status(400).json({ error: "direction must be in|out" });
|
|
357
|
-
const r = p.logMessage({ agent_slug: agent_slug || null, channel, direction, author, body, meta });
|
|
357
|
+
const r = p.logMessage({ agent_slug: agent_slug || null, channel, direction, type, actor_id, author, body, meta });
|
|
358
358
|
res.status(201).json({ ok: true, ts: r.ts });
|
|
359
359
|
});
|
|
360
360
|
|
|
@@ -843,7 +843,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
843
843
|
const r = getRoutine(p.path, req.params.name);
|
|
844
844
|
if (!r) return res.status(404).json({ error: "routine not found" });
|
|
845
845
|
try {
|
|
846
|
-
const result = await runRoutineNow({ project: p, plugins, globalConfig: config }, r);
|
|
846
|
+
const result = await runRoutineNow({ project: p, projects, plugins, registries, globalConfig: config }, r);
|
|
847
847
|
res.json(result);
|
|
848
848
|
} catch (e) {
|
|
849
849
|
res.status(500).json({ error: e.message });
|
package/src/daemon/index.js
CHANGED
|
@@ -44,6 +44,32 @@ function writePid() {
|
|
|
44
44
|
} catch {}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function pidIsAlive(pid) {
|
|
48
|
+
if (!pid || pid === process.pid) return false;
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function claimSingleton() {
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(PID_PATH)) {
|
|
60
|
+
const pid = parseInt(fs.readFileSync(PID_PATH, "utf8"), 10);
|
|
61
|
+
if (pidIsAlive(pid)) {
|
|
62
|
+
log(`fatal: apx-daemon already running with pid ${pid}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
fs.unlinkSync(PID_PATH);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
log(`fatal: cannot claim daemon pid file: ${e.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
function clearPid() {
|
|
48
74
|
try {
|
|
49
75
|
if (fs.existsSync(PID_PATH)) fs.unlinkSync(PID_PATH);
|
|
@@ -71,6 +97,7 @@ class RegistryCache {
|
|
|
71
97
|
|
|
72
98
|
async function main() {
|
|
73
99
|
ensureHome();
|
|
100
|
+
claimSingleton();
|
|
74
101
|
|
|
75
102
|
const cfg = readConfig();
|
|
76
103
|
const host = effectiveHost(cfg);
|
|
@@ -95,15 +122,14 @@ async function main() {
|
|
|
95
122
|
|
|
96
123
|
const plugins = new PluginManager({ projects, config: cfg, log, registries });
|
|
97
124
|
plugins.initAll();
|
|
98
|
-
plugins.startAll();
|
|
99
125
|
|
|
100
126
|
const scheduler = new RoutineScheduler({
|
|
101
127
|
projects,
|
|
102
128
|
plugins,
|
|
129
|
+
registries,
|
|
103
130
|
globalConfig: cfg,
|
|
104
131
|
log,
|
|
105
132
|
});
|
|
106
|
-
scheduler.start();
|
|
107
133
|
|
|
108
134
|
const startedAt = Date.now();
|
|
109
135
|
const app = buildApi({
|
|
@@ -130,10 +156,20 @@ async function main() {
|
|
|
130
156
|
writePid();
|
|
131
157
|
log(`apx-daemon ${PKG.version} listening on http://${host}:${port}`);
|
|
132
158
|
log(`projects: ${projects.list().length} | plugins: ${Object.keys(plugins.status()).join(", ") || "(none)"}`);
|
|
159
|
+
plugins.startAll();
|
|
160
|
+
scheduler.start();
|
|
133
161
|
// Fire wake-up message after a short delay so plugins (Telegram) are ready
|
|
134
162
|
setTimeout(() => triggerWakeup(cfg, log), 3000);
|
|
135
163
|
});
|
|
136
164
|
|
|
165
|
+
server.on("error", (e) => {
|
|
166
|
+
log(`fatal: listen ${host}:${port} failed: ${e.message}`);
|
|
167
|
+
plugins.stopAll();
|
|
168
|
+
registries.shutdown();
|
|
169
|
+
clearPid();
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
137
173
|
function shutdown(signal) {
|
|
138
174
|
log(`received ${signal}, shutting down...`);
|
|
139
175
|
scheduler.stop();
|
|
@@ -252,7 +252,7 @@ class ChannelPoller {
|
|
|
252
252
|
if (chat_id && !isReset) {
|
|
253
253
|
previousMessages = getRecentTelegramTurnsFromFs({
|
|
254
254
|
chat_id,
|
|
255
|
-
limit:
|
|
255
|
+
limit: 20,
|
|
256
256
|
max_age_hours: 24,
|
|
257
257
|
});
|
|
258
258
|
// Honour a /reset marker: drop everything up to and including it.
|
|
@@ -276,11 +276,14 @@ class ChannelPoller {
|
|
|
276
276
|
appendGlobalMessage({
|
|
277
277
|
channel: "telegram",
|
|
278
278
|
direction: "in",
|
|
279
|
+
type: "user",
|
|
280
|
+
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
279
281
|
external_id: String(u.update_id),
|
|
280
282
|
author,
|
|
281
283
|
body: text,
|
|
282
284
|
meta: {
|
|
283
285
|
chat_id,
|
|
286
|
+
user_id: msg.from?.id || null,
|
|
284
287
|
message_id: msg.message_id,
|
|
285
288
|
tg_channel: this.channel.name,
|
|
286
289
|
},
|
|
@@ -299,6 +302,8 @@ class ChannelPoller {
|
|
|
299
302
|
appendGlobalMessage({
|
|
300
303
|
channel: "telegram",
|
|
301
304
|
direction: "out",
|
|
305
|
+
type: "agent",
|
|
306
|
+
actor_id: "apx",
|
|
302
307
|
author: "apx",
|
|
303
308
|
body: ack,
|
|
304
309
|
meta: { chat_id, tg_channel: this.channel.name, in_reply_to: u.update_id, reset: true },
|
|
@@ -355,7 +360,7 @@ class ChannelPoller {
|
|
|
355
360
|
registries: this.registries,
|
|
356
361
|
prompt: text,
|
|
357
362
|
previousMessages,
|
|
358
|
-
contextNote: `
|
|
363
|
+
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.`,
|
|
359
364
|
});
|
|
360
365
|
replyText = sa.text;
|
|
361
366
|
replyAuthor = sa.name;
|
|
@@ -401,12 +406,34 @@ class ChannelPoller {
|
|
|
401
406
|
appendGlobalMessage({
|
|
402
407
|
channel: "telegram",
|
|
403
408
|
direction: "out",
|
|
409
|
+
type: "agent",
|
|
410
|
+
actor_id: replyAuthor || "apx",
|
|
411
|
+
agent_slug: replyAuthor || "apx",
|
|
404
412
|
author: replyAuthor || "apx",
|
|
405
413
|
body: clean || replyText,
|
|
406
414
|
meta,
|
|
407
415
|
});
|
|
408
416
|
} catch (e) {
|
|
409
417
|
this.log(`telegram[${this.channel.name}] send-back error: ${e.message}`);
|
|
418
|
+
appendGlobalMessage({
|
|
419
|
+
channel: "telegram",
|
|
420
|
+
direction: "out",
|
|
421
|
+
type: "agent",
|
|
422
|
+
actor_id: replyAuthor || "apx",
|
|
423
|
+
agent_slug: replyAuthor || "apx",
|
|
424
|
+
author: replyAuthor || "apx",
|
|
425
|
+
body: `[send_failed] ${clean || replyText}`,
|
|
426
|
+
meta: {
|
|
427
|
+
chat_id,
|
|
428
|
+
tg_channel: this.channel.name,
|
|
429
|
+
in_reply_to: u.update_id,
|
|
430
|
+
send_error: e.message,
|
|
431
|
+
...(saTrace && saTrace.length > 0
|
|
432
|
+
? { tools_called: saTrace.map((t) => ({ tool: t.tool, args: t.args })) }
|
|
433
|
+
: {}),
|
|
434
|
+
...(saUsage ? { usage: saUsage } : {}),
|
|
435
|
+
},
|
|
436
|
+
});
|
|
410
437
|
}
|
|
411
438
|
}
|
|
412
439
|
|
|
@@ -533,6 +560,9 @@ export default {
|
|
|
533
560
|
appendGlobalMessage({
|
|
534
561
|
channel: "telegram",
|
|
535
562
|
direction: "out",
|
|
563
|
+
type: "agent",
|
|
564
|
+
actor_id: author,
|
|
565
|
+
agent_slug: author,
|
|
536
566
|
author,
|
|
537
567
|
body: text,
|
|
538
568
|
meta: {
|
package/src/daemon/routines.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Kinds:
|
|
7
7
|
// heartbeat — log a heartbeat message. spec: { channel?, message? }
|
|
8
8
|
// exec_agent — call an agent engine. spec: { agent: slug, prompt }
|
|
9
|
+
// super_agent — call the APX super-agent. spec: { prompt }
|
|
9
10
|
// telegram — send a Telegram message. spec: { channel?, chat_id?, text }
|
|
10
11
|
// shell — run a shell command. spec: { command, timeout_ms? }
|
|
11
12
|
|
|
@@ -13,6 +14,7 @@ import { spawn } from "node:child_process";
|
|
|
13
14
|
import path from "node:path";
|
|
14
15
|
import fs from "node:fs";
|
|
15
16
|
import { callEngine } from "./engines/index.js";
|
|
17
|
+
import { runSuperAgent } from "./super-agent.js";
|
|
16
18
|
import { readAgents } from "../core/parser.js";
|
|
17
19
|
import {
|
|
18
20
|
listRoutines,
|
|
@@ -48,6 +50,8 @@ async function handleHeartbeat(ctx, routine) {
|
|
|
48
50
|
project.logMessage({
|
|
49
51
|
channel,
|
|
50
52
|
direction: "out",
|
|
53
|
+
type: "system",
|
|
54
|
+
actor_id: "apx:routine",
|
|
51
55
|
author: "apx",
|
|
52
56
|
body: message,
|
|
53
57
|
meta: { routine: routine.name },
|
|
@@ -85,6 +89,8 @@ async function handleExecAgent(ctx, routine) {
|
|
|
85
89
|
agent_slug: slug,
|
|
86
90
|
channel: "routine",
|
|
87
91
|
direction: "out",
|
|
92
|
+
type: "agent",
|
|
93
|
+
actor_id: slug,
|
|
88
94
|
author: slug,
|
|
89
95
|
body: result.text,
|
|
90
96
|
meta: { routine: routine.name, usage: result.usage },
|
|
@@ -92,6 +98,39 @@ async function handleExecAgent(ctx, routine) {
|
|
|
92
98
|
return { status: "ok", reply: result.text };
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
async function handleSuperAgent(ctx, routine) {
|
|
102
|
+
const { project, globalConfig, projects, plugins, registries } = ctx;
|
|
103
|
+
const { prompt } = routine.spec;
|
|
104
|
+
if (!prompt) throw new Error("super_agent: spec needs { prompt }");
|
|
105
|
+
|
|
106
|
+
const cfg = structuredClone(globalConfig || {});
|
|
107
|
+
cfg.super_agent = {
|
|
108
|
+
...(globalConfig?.super_agent || {}),
|
|
109
|
+
...(routine.permission_mode ? { permission_mode: routine.permission_mode } : {}),
|
|
110
|
+
...(Array.isArray(routine.allowed_tools) ? { allowed_tools: routine.allowed_tools } : {}),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const result = await runSuperAgent({
|
|
114
|
+
globalConfig: cfg,
|
|
115
|
+
projects,
|
|
116
|
+
plugins,
|
|
117
|
+
registries,
|
|
118
|
+
prompt,
|
|
119
|
+
contextNote: `You were invoked by APX routine "${routine.name}" in project ${project.path}. This is an autonomous scheduled run, not an interactive Telegram reply.`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
project.logMessage({
|
|
123
|
+
channel: "routine",
|
|
124
|
+
direction: "out",
|
|
125
|
+
type: "agent",
|
|
126
|
+
actor_id: result.name || "super_agent",
|
|
127
|
+
author: result.name || "super_agent",
|
|
128
|
+
body: result.text || "",
|
|
129
|
+
meta: { routine: routine.name, tool_trace: result.trace, usage: result.usage },
|
|
130
|
+
});
|
|
131
|
+
return { status: "ok", reply: result.text, trace: result.trace };
|
|
132
|
+
}
|
|
133
|
+
|
|
95
134
|
async function handleTelegram(ctx, routine) {
|
|
96
135
|
const { plugins } = ctx;
|
|
97
136
|
const tg = plugins?.get("telegram");
|
|
@@ -130,6 +169,7 @@ function handleShell(ctx, routine) {
|
|
|
130
169
|
const HANDLERS = {
|
|
131
170
|
heartbeat: handleHeartbeat,
|
|
132
171
|
exec_agent: handleExecAgent,
|
|
172
|
+
super_agent: handleSuperAgent,
|
|
133
173
|
telegram: handleTelegram,
|
|
134
174
|
shell: handleShell,
|
|
135
175
|
};
|
|
@@ -144,6 +184,10 @@ export async function runRoutineNow(ctx, routine) {
|
|
|
144
184
|
let errMsg = null;
|
|
145
185
|
try {
|
|
146
186
|
result = await handler(ctx, routine);
|
|
187
|
+
if (result?.status === "error") {
|
|
188
|
+
status = "error";
|
|
189
|
+
errMsg = result.error || result.stderr || `routine ${routine.name} returned error status`;
|
|
190
|
+
}
|
|
147
191
|
} catch (e) {
|
|
148
192
|
status = "error";
|
|
149
193
|
errMsg = e.message;
|
|
@@ -159,13 +203,25 @@ export async function runRoutineNow(ctx, routine) {
|
|
|
159
203
|
next_run_at: next,
|
|
160
204
|
disable: isOnce,
|
|
161
205
|
});
|
|
206
|
+
ctx.project.logMessage?.({
|
|
207
|
+
channel: "routine",
|
|
208
|
+
direction: "out",
|
|
209
|
+
type: "system",
|
|
210
|
+
actor_id: "apx:routine",
|
|
211
|
+
author: "apx",
|
|
212
|
+
body: status === "ok"
|
|
213
|
+
? `routine ${routine.name} ok`
|
|
214
|
+
: `routine ${routine.name} error: ${errMsg}`,
|
|
215
|
+
meta: { routine: routine.name, status, result },
|
|
216
|
+
});
|
|
162
217
|
return { ...result, last_run_at: lastRun, next_run_at: next };
|
|
163
218
|
}
|
|
164
219
|
|
|
165
220
|
export class RoutineScheduler {
|
|
166
|
-
constructor({ projects, plugins, globalConfig, log }) {
|
|
221
|
+
constructor({ projects, plugins, registries, globalConfig, log }) {
|
|
167
222
|
this.projects = projects;
|
|
168
223
|
this.plugins = plugins;
|
|
224
|
+
this.registries = registries;
|
|
169
225
|
this.globalConfig = globalConfig;
|
|
170
226
|
this.log = log || (() => {});
|
|
171
227
|
this._timer = null;
|
|
@@ -199,7 +255,13 @@ export class RoutineScheduler {
|
|
|
199
255
|
for (const r of due) {
|
|
200
256
|
this.log(`routine ${r.name} (${r.kind}) firing in project #${proj.id}`);
|
|
201
257
|
await runRoutineNow(
|
|
202
|
-
{
|
|
258
|
+
{
|
|
259
|
+
project: proj,
|
|
260
|
+
projects: this.projects,
|
|
261
|
+
plugins: this.plugins,
|
|
262
|
+
registries: this.registries,
|
|
263
|
+
globalConfig: this.globalConfig,
|
|
264
|
+
},
|
|
203
265
|
r
|
|
204
266
|
);
|
|
205
267
|
}
|