@agentprojectcontext/apx 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function listField(value) {
5
+ if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
6
+ return String(value || "").split(",").map((s) => s.trim()).filter(Boolean);
7
+ }
8
+
9
+ function projectName(project) {
10
+ if (project?.name) return project.name;
11
+ try {
12
+ const meta = JSON.parse(fs.readFileSync(path.join(project.path, ".apc", "project.json"), "utf8"));
13
+ return meta.name || path.basename(project.path);
14
+ } catch {
15
+ return path.basename(project?.path || "");
16
+ }
17
+ }
18
+
19
+ export function agentSkills(agent) {
20
+ return listField(agent?.fields?.Skills);
21
+ }
22
+
23
+ export function buildAgentSystem(project, agent, {
24
+ invocation = "engine",
25
+ runtime = null,
26
+ channel = null,
27
+ caller = null,
28
+ routine = null,
29
+ extraParts = [],
30
+ } = {}) {
31
+ const fields = agent.fields || {};
32
+ const parts = [
33
+ `You are APC agent "${agent.slug}".`,
34
+ `Project: ${projectName(project)} (${project.path}).`,
35
+ ];
36
+
37
+ if (fields.Description) parts.push(String(fields.Description));
38
+ if (fields.Role) parts.push(`Role: ${fields.Role}`);
39
+ if (fields.Language) parts.push(`Default language: ${fields.Language}`);
40
+
41
+ const declaredTools = listField(fields.Tools);
42
+ if (declaredTools.length) {
43
+ parts.push(
44
+ [
45
+ "## Declared Tool Hints",
46
+ declaredTools.join(", "),
47
+ "These are agent-level tool expectations, not a guarantee. Actual callable tools depend on invocation surface.",
48
+ ].join("\n")
49
+ );
50
+ }
51
+
52
+ parts.push(buildInvocationContext({ invocation, runtime, channel, caller, routine }));
53
+
54
+ const memPath = path.join(project.path, ".apc", "agents", agent.slug, "memory.md");
55
+ if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
56
+
57
+ const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
58
+ if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
59
+
60
+ for (const skill of agentSkills(agent)) {
61
+ const skillPath = path.join(project.path, ".apc", "skills", `${skill}.md`);
62
+ if (fs.existsSync(skillPath)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(skillPath, "utf8"));
63
+ }
64
+
65
+ for (const ep of extraParts) {
66
+ if (ep) parts.push(ep);
67
+ }
68
+
69
+ return parts.join("\n\n");
70
+ }
71
+
72
+ function buildInvocationContext({ invocation, runtime, channel, caller, routine }) {
73
+ const lines = [
74
+ "## Invocation Context",
75
+ `invocation: ${invocation}`,
76
+ ];
77
+ if (runtime) lines.push(`runtime: ${runtime}`);
78
+ if (channel) lines.push(`channel: ${channel}`);
79
+ if (caller) lines.push(`caller: ${caller}`);
80
+ if (routine) lines.push(`routine: ${routine}`);
81
+
82
+ if (runtime) {
83
+ lines.push(
84
+ "You are running inside the named external runtime. Use only tools and permissions that runtime actually exposes."
85
+ );
86
+ } else if (invocation === "engine") {
87
+ lines.push("You are a direct LLM call through APX. Do not claim shell, file, MCP, or Telegram tools unless APX explicitly provided them.");
88
+ } else if (invocation === "telegram") {
89
+ lines.push("You are replying through Telegram. Keep responses brief, plain text, and matched to the user's language.");
90
+ } else if (invocation === "routine") {
91
+ lines.push("You were invoked by an APX routine. Complete the requested work now; do not say you will do it later.");
92
+ }
93
+
94
+ return lines.join("\n");
95
+ }
@@ -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
@@ -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:
@@ -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
- export function appendMessageToFs({ projectRoot, channel, direction, author, body, meta = {}, ts, agent_slug, session_id, external_id }) {
36
- ts = ts || nowIso();
37
- const file = dayPathJsonl(projectRoot, ts);
38
- fs.mkdirSync(path.dirname(file), { recursive: true });
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
- // Compose meta from explicit fields plus the bag
41
- const fullMeta = {
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(m.meta || {}),
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: meta.agent,
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
- agent_slug: meta.agent,
162
- session_id: typeof meta.apc_session_id === "number" ? meta.apc_session_id : null,
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
  }
@@ -223,9 +297,9 @@ export function getRecentTelegramTurns(
223
297
  // answer that mixes fragments of the old one with hallucinations. The
224
298
  // failure observed with qwen2.5:14b was:
225
299
  //
226
- // prev assistant: "agente sandbox con modelo ollama:llama3.2:3b"
227
- // user: "y en el otro proyecto qué agente tiene?"
228
- // assistant (alucinated): "agente assistant con modelo ollama:llama3.2:3b"
300
+ // prev assistant: "sandbox agent with model ollama:llama3.2:3b"
301
+ // user: "and what agent does the other project have?"
302
+ // assistant (hallucinated): "assistant agent with model ollama:llama3.2:3b"
229
303
  // (sofia exists, not "assistant", and her model is
230
304
  // claude-haiku-4-5, not the carry-over from above)
231
305
  //
@@ -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 fullMeta = {
352
- ...(agent_slug ? { agent: agent_slug } : {}),
353
- ...(external_id ? { external_id } : {}),
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,
@@ -66,7 +66,7 @@ export function createRuntimeSession({ projectRoot, storageRoot = projectRoot, a
66
66
  `agent: ${agentSlug}\n` +
67
67
  `title: ${sessionTitle}\n` +
68
68
  `task_ref: ${taskRef}\n` +
69
- `status: 🔄 En progreso\n` +
69
+ `status: 🔄 In progress\n` +
70
70
  `started: ${started}\n` +
71
71
  `completed: \n` +
72
72
  `result: \n` +
@@ -91,7 +91,7 @@ export function closeRuntimeSession({ filePath, externalSessionPath, exitCode, r
91
91
  } else if (result) {
92
92
  text = setField(text, "result", result.slice(0, 300));
93
93
  }
94
- text = setField(text, "status", exitCode === 0 ? "✅ Completada" : "⚠️ Cerrada con error");
94
+ text = setField(text, "status", exitCode === 0 ? "✅ Completed" : "⚠️ Closed with error");
95
95
  fs.writeFileSync(filePath, text);
96
96
  }
97
97
 
package/src/daemon/api.js CHANGED
@@ -42,6 +42,7 @@ import { readGlobalMessages, readProjectMessages, searchProjectMessages } from "
42
42
  import { readAgents } from "../core/parser.js";
43
43
  import { parseSessionFrontmatter } from "../core/parser.js";
44
44
  import { writeAgentFile, ensureAgentDir, regenerateAgentsMd } from "../core/scaffold.js";
45
+ import { buildAgentSystem } from "../core/agent-system.js";
45
46
 
46
47
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
47
48
 
@@ -348,13 +349,13 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
348
349
  app.post("/projects/:pid/messages", (req, res) => {
349
350
  const p = project(req, res);
350
351
  if (!p) return;
351
- const { channel, direction, agent_slug, body, meta = {}, author = null } =
352
+ const { channel, direction, type, actor_id, agent_slug, body, meta = {}, author = null } =
352
353
  req.body || {};
353
354
  if (!channel || !direction || !body)
354
355
  return res.status(400).json({ error: "channel, direction, body required" });
355
356
  if (!["in", "out"].includes(direction))
356
357
  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 });
358
+ const r = p.logMessage({ agent_slug: agent_slug || null, channel, direction, type, actor_id, author, body, meta });
358
359
  res.status(201).json({ ok: true, ts: r.ts });
359
360
  });
360
361
 
@@ -421,7 +422,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
421
422
  if (!modelId) return res.status(400).json({ error: "agent has no model and none provided" });
422
423
 
423
424
  try {
424
- const system = buildAgentSystem(p, agent);
425
+ const system = buildAgentSystem(p, agent, { invocation: "engine" });
425
426
  const conv = startConversation({ storagePath: p.storagePath, agentSlug: agent.slug, engine: modelId, system });
426
427
  appendTurn({ filePath: conv.path, role: "user", content: prompt });
427
428
 
@@ -488,9 +489,9 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
488
489
 
489
490
  // Build system prompt — inject compact summary if this conversation was compacted.
490
491
  const extraParts = compactSummary
491
- ? [`## Contexto de conversación anterior (compactado)\n${compactSummary}`]
492
+ ? [`## Previous Conversation Context (Compacted)\n${compactSummary}`]
492
493
  : [];
493
- const system = buildAgentSystem(p, agent, { extraParts });
494
+ const system = buildAgentSystem(p, agent, { invocation: "engine", extraParts });
494
495
 
495
496
  if (!conversation_id) {
496
497
  const conv = startConversation({ storagePath: p.storagePath, agentSlug: agent.slug, engine: modelId, system });
@@ -688,6 +689,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
688
689
  });
689
690
 
690
691
  const system = buildAgentSystem(p, agent, {
692
+ invocation: "runtime",
693
+ runtime,
691
694
  extraParts: [
692
695
  buildApfHint({
693
696
  projectName,
@@ -775,11 +778,11 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
775
778
  if (req.query.summarize === "true" && isSuperAgentEnabled(config)) {
776
779
  try {
777
780
  const prompt =
778
- `Resumí qué pasó en esta sesión APC en 4 bullets concretos.\n\n` +
781
+ `Summarize what happened in this APC session in 4 concrete bullets.\n\n` +
779
782
  `Frontmatter:\n${JSON.stringify(out.frontmatter, null, 2)}\n\n` +
780
783
  (out.external_transcript
781
- ? `Transcript externo (últimos ${out.external_transcript.tail.length} chars):\n${out.external_transcript.tail}`
782
- : `(sin transcript externo)`);
784
+ ? `External transcript (last ${out.external_transcript.tail.length} chars):\n${out.external_transcript.tail}`
785
+ : `(no external transcript)`);
783
786
  const sa = await runSuperAgent({ globalConfig: config, projects, plugins, registries, prompt, contextNote: `Resume request for session ${id}.` });
784
787
  out.summary = sa.text;
785
788
  } catch (e) {
@@ -843,7 +846,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
843
846
  const r = getRoutine(p.path, req.params.name);
844
847
  if (!r) return res.status(404).json({ error: "routine not found" });
845
848
  try {
846
- const result = await runRoutineNow({ project: p, plugins, globalConfig: config }, r);
849
+ const result = await runRoutineNow({ project: p, projects, plugins, registries, globalConfig: config }, r);
847
850
  res.json(result);
848
851
  } catch (e) {
849
852
  res.status(500).json({ error: e.message });
@@ -1084,26 +1087,3 @@ function agentToResponse(a) {
1084
1087
  extra,
1085
1088
  };
1086
1089
  }
1087
-
1088
- // Build system prompt from an agent's fields + memory + skills.
1089
- // Optional `extraParts` are appended at the end.
1090
- function buildAgentSystem(p, agent, { extraParts = [] } = {}) {
1091
- const f = agent.fields || {};
1092
- const parts = [];
1093
- if (f.Description) parts.push(f.Description);
1094
- if (f.Role) parts.push(`Role: ${f.Role}`);
1095
- if (f.Language) parts.push(`Default language: ${f.Language}`);
1096
- const memPath = path.join(p.path, ".apc", "agents", agent.slug, "memory.md");
1097
- if (fs.existsSync(memPath)) {
1098
- parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
1099
- }
1100
- const apxSkill = path.join(p.path, ".apc", "skills", "apx.md");
1101
- if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
1102
- const skills = Array.isArray(f.Skills) ? f.Skills : [];
1103
- for (const skill of skills) {
1104
- const sp = path.join(p.path, ".apc", "skills", `${skill}.md`);
1105
- if (fs.existsSync(sp)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(sp, "utf8"));
1106
- }
1107
- for (const ep of extraParts) parts.push(ep);
1108
- return parts.join("\n\n");
1109
- }
@@ -126,6 +126,7 @@ async function main() {
126
126
  const scheduler = new RoutineScheduler({
127
127
  projects,
128
128
  plugins,
129
+ registries,
129
130
  globalConfig: cfg,
130
131
  log,
131
132
  });
@@ -28,13 +28,13 @@
28
28
  // }
29
29
 
30
30
  import fs from "node:fs";
31
- import path from "node:path";
32
31
  import { TELEGRAM_STATE_PATH } from "../../core/config.js";
33
32
  import { callEngine } from "../engines/index.js";
34
33
  import { runSuperAgent, isSuperAgentEnabled } from "../super-agent.js";
35
34
  import { stripThinking } from "../thinking.js";
36
35
  import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "../../core/messages-store.js";
37
36
  import { readAgents } from "../../core/parser.js";
37
+ import { buildAgentSystem } from "../../core/agent-system.js";
38
38
 
39
39
  const API_BASE = "https://api.telegram.org";
40
40
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -252,7 +252,7 @@ class ChannelPoller {
252
252
  if (chat_id && !isReset) {
253
253
  previousMessages = getRecentTelegramTurnsFromFs({
254
254
  chat_id,
255
- limit: 12,
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
  },
@@ -294,11 +297,13 @@ class ChannelPoller {
294
297
  // honor it for future messages.
295
298
  if (isReset) {
296
299
  try {
297
- const ack = "Listo, contexto limpiado. Empezamos de cero ¿qué necesitás?";
300
+ const ack = "Done, context cleared. Starting fresh. What do you need?";
298
301
  await this._send({ chat_id, text: ack });
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 },
@@ -322,7 +327,11 @@ class ChannelPoller {
322
327
  const agent = readAgents(target.path).find((a) => a.slug === routeSlug);
323
328
  if (agent && agent.fields.Model) {
324
329
  try {
325
- const system = buildAgentSystem(target, agent, author);
330
+ const system = buildAgentSystem(target, agent, {
331
+ invocation: "telegram",
332
+ channel: this.channel.name,
333
+ caller: author,
334
+ });
326
335
  const result = await callEngine({
327
336
  modelId: agent.fields.Model,
328
337
  system,
@@ -355,7 +364,7 @@ class ChannelPoller {
355
364
  registries: this.registries,
356
365
  prompt: text,
357
366
  previousMessages,
358
- contextNote: `Inbound came on Telegram channel "${this.channel.name}" from ${author}. Previous turns of this chat are included for context.`,
367
+ 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
368
  });
360
369
  replyText = sa.text;
361
370
  replyAuthor = sa.name;
@@ -401,12 +410,34 @@ class ChannelPoller {
401
410
  appendGlobalMessage({
402
411
  channel: "telegram",
403
412
  direction: "out",
413
+ type: "agent",
414
+ actor_id: replyAuthor || "apx",
415
+ agent_slug: replyAuthor || "apx",
404
416
  author: replyAuthor || "apx",
405
417
  body: clean || replyText,
406
418
  meta,
407
419
  });
408
420
  } catch (e) {
409
421
  this.log(`telegram[${this.channel.name}] send-back error: ${e.message}`);
422
+ appendGlobalMessage({
423
+ channel: "telegram",
424
+ direction: "out",
425
+ type: "agent",
426
+ actor_id: replyAuthor || "apx",
427
+ agent_slug: replyAuthor || "apx",
428
+ author: replyAuthor || "apx",
429
+ body: `[send_failed] ${clean || replyText}`,
430
+ meta: {
431
+ chat_id,
432
+ tg_channel: this.channel.name,
433
+ in_reply_to: u.update_id,
434
+ send_error: e.message,
435
+ ...(saTrace && saTrace.length > 0
436
+ ? { tools_called: saTrace.map((t) => ({ tool: t.tool, args: t.args })) }
437
+ : {}),
438
+ ...(saUsage ? { usage: saUsage } : {}),
439
+ },
440
+ });
410
441
  }
411
442
  }
412
443
 
@@ -459,26 +490,6 @@ class ChannelPoller {
459
490
  }
460
491
  }
461
492
 
462
- // ---------- system-prompt builder (same as /exec) ---------------------------
463
-
464
- function buildAgentSystem(target, agent, author) {
465
- const parts = [];
466
- if (agent.fields.Description) parts.push(agent.fields.Description);
467
- if (agent.fields.Role) parts.push(`Role: ${agent.fields.Role}`);
468
- if (agent.fields.Language) parts.push(`Default language: ${agent.fields.Language}`);
469
- parts.push(
470
- `You are speaking via Telegram with ${author}. Keep responses brief — ideally under 4 sentences. Mirror their language.`
471
- );
472
- const memPath = path.join(target.path, ".apc", "agents", agent.slug, "memory.md");
473
- if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
474
- const skills = (agent.fields.Skills || "").split(",").map((s) => s.trim()).filter(Boolean);
475
- for (const skill of skills) {
476
- const sp = path.join(target.path, ".apc", "skills", `${skill}.md`);
477
- if (fs.existsSync(sp)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(sp, "utf8"));
478
- }
479
- return parts.join("\n\n");
480
- }
481
-
482
493
  function sleep(ms) {
483
494
  return new Promise((r) => setTimeout(r, ms));
484
495
  }
@@ -533,6 +544,9 @@ export default {
533
544
  appendGlobalMessage({
534
545
  channel: "telegram",
535
546
  direction: "out",
547
+ type: "agent",
548
+ actor_id: author,
549
+ agent_slug: author,
536
550
  author,
537
551
  body: text,
538
552
  meta: {