@agentprojectcontext/apx 1.19.0 → 1.20.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,7 +50,7 @@ const DEFAULT_CONFIG = {
50
50
  name: "apx",
51
51
  model: "", // e.g. "ollama:llama3.2:3b"
52
52
  system: "", // optional override; defaults baked into super-agent.js
53
- permission_mode: "total", // total | automatico | permiso
53
+ permission_mode: "automatico", // total | automatico | permiso
54
54
  allowed_tools: [], // used by permission_mode="permiso"
55
55
  },
56
56
  engines: {
@@ -403,6 +403,11 @@ export function getRecentTelegramTurnsFromFs({
403
403
  all.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
404
404
  const filtered = all
405
405
  .filter((m) => String(m.meta?.chat_id ?? "") === String(chat_id))
406
+ // Only conversational turns become model context. `tool` / `system`
407
+ // entries are kept in the store for the audit trail (and for channels
408
+ // that DO render tools), but replaying them as assistant messages would
409
+ // look like bogus answers to the model.
410
+ .filter((m) => m.type === "user" || m.type === "agent")
406
411
  .slice(-limit);
407
412
  return filtered.map((m) => {
408
413
  const role = m.direction === "in" ? "user" : "assistant";
@@ -626,10 +626,69 @@ class ChannelPoller {
626
626
  }
627
627
  }
628
628
 
629
- // Fallback: super-agent
630
- let saTrace = null;
629
+ // Fallback: super-agent — STREAMED.
630
+ // Each iteration's assistant text is sent to Telegram as its own message
631
+ // the moment the model produces it (its running commentary), so the user
632
+ // sees a real back-and-forth instead of one giant final dump. Tool calls
633
+ // are logged to the message store — visible via apx log / apx search and
634
+ // to channels that render tools — but NEVER sent to Telegram; tools are
635
+ // internal. The conversation saved on disk is the full, real exchange;
636
+ // Telegram is just the prose-only view of it.
631
637
  let saUsage = null;
638
+ let streamedCount = 0;
639
+ let lastStreamedText = "";
632
640
  if (!replyText && isSuperAgentEnabled(this.globalConfig)) {
641
+ const onEvent = async (ev) => {
642
+ try {
643
+ if (ev.type === "assistant_text" && ev.text) {
644
+ const piece = stripThinking(ev.text).trim();
645
+ if (!piece) return;
646
+ await this._send({ chat_id, text: piece });
647
+ lastStreamedText = piece;
648
+ streamedCount += 1;
649
+ appendGlobalMessage({
650
+ channel: "telegram",
651
+ direction: "out",
652
+ type: "agent",
653
+ actor_id: "apx",
654
+ agent_slug: "apx",
655
+ author: "apx",
656
+ body: piece,
657
+ meta: {
658
+ chat_id,
659
+ tg_channel: this.channel.name,
660
+ in_reply_to: u.update_id,
661
+ streamed: true,
662
+ iteration: ev.iteration,
663
+ },
664
+ });
665
+ } else if (ev.type === "tool_result" && ev.trace) {
666
+ // Logged for the audit trail / other channels — NOT sent to Telegram.
667
+ const t = ev.trace;
668
+ appendGlobalMessage({
669
+ channel: "telegram",
670
+ direction: "out",
671
+ type: "tool",
672
+ actor_id: t.tool,
673
+ author: "apx",
674
+ body: `${t.tool}(${JSON.stringify(t.args || {}).slice(0, 200)})`,
675
+ meta: {
676
+ chat_id,
677
+ tg_channel: this.channel.name,
678
+ in_reply_to: u.update_id,
679
+ tool: t.tool,
680
+ args: t.args,
681
+ result: t.result,
682
+ iteration: ev.iteration,
683
+ },
684
+ });
685
+ }
686
+ } catch (e) {
687
+ // A failed intermediate send must not abort the whole run.
688
+ this.log(`telegram[${this.channel.name}] stream event failed: ${e.message}`);
689
+ }
690
+ };
691
+
633
692
  try {
634
693
  const sa = await runSuperAgent({
635
694
  globalConfig: this.globalConfig,
@@ -640,15 +699,20 @@ class ChannelPoller {
640
699
  previousMessages,
641
700
  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.`,
642
701
  signal: abortCtrl.signal,
702
+ onEvent,
643
703
  });
644
704
  replyText = sa.text;
645
705
  replyAuthor = sa.name;
646
- saTrace = sa.trace;
647
706
  saUsage = sa.usage;
648
707
  } catch (e) {
649
708
  if (abortCtrl.signal.aborted) {
709
+ // A newer message superseded this one. Whatever streamed so far is
710
+ // already sent + logged; the newer message's run continues the
711
+ // thread from that history.
650
712
  this.log(`telegram[${this.channel.name}] request aborted for chat ${chat_id}`);
651
- return; // don't send reply if aborted
713
+ if (chat_id) this.activeRequests.delete(chat_id);
714
+ stopTyping();
715
+ return;
652
716
  }
653
717
  this.log(`telegram[${this.channel.name}] super-agent failed: ${e.message}`);
654
718
  // Surface the failure to the user instead of silently dropping the
@@ -660,37 +724,29 @@ class ChannelPoller {
660
724
  }
661
725
 
662
726
  if (chat_id) this.activeRequests.delete(chat_id);
663
- if (!replyText) {
664
- stopTyping();
665
- return;
666
- }
667
727
 
668
- // Strip <thinking>...</thinking> blocks before sending to Telegram
669
- // reasoning is noise to the chat reader. The full text (with thinking)
670
- // stays in the daemon log and in messages with channel='engine' if the
671
- // model produced any.
672
- const clean = stripThinking(replyText);
728
+ // Final answer. The intermediate prose was already streamed; only send the
729
+ // final text if it's non-empty AND not a duplicate of the last streamed
730
+ // piece (the loop can end on an iteration whose text was already sent).
731
+ // If nothing streamed and there's no final text, send a minimal ack so the
732
+ // turn isn't silently empty.
733
+ const finalClean = replyText ? stripThinking(replyText).trim() : "";
734
+ let toSend = "";
735
+ if (finalClean && finalClean !== lastStreamedText) toSend = finalClean;
736
+ else if (!finalClean && streamedCount === 0) toSend = "Listo.";
673
737
 
674
- // Send reply via this channel's bot
675
738
  stopTyping();
739
+ if (!toSend) return; // everything was already streamed — nothing left to send
740
+
676
741
  try {
677
- await this._send({ chat_id, text: clean || replyText });
678
- // Log outbound — store the cleaned text (what we actually sent). The
679
- // full reasoning (if any) goes in meta_json so it's recoverable.
742
+ await this._send({ chat_id, text: toSend });
680
743
  const meta = {
681
744
  chat_id,
682
745
  tg_channel: this.channel.name,
683
746
  in_reply_to: u.update_id,
747
+ final: true,
684
748
  };
685
- if (clean !== replyText) meta.thinking_stripped = true;
686
- if (saTrace && saTrace.length > 0) {
687
- // Compact representation: [{tool, args}] without the full result
688
- // (results can be huge — keep them out of the long-lived FS log).
689
- meta.tools_called = saTrace.map((t) => ({
690
- tool: t.tool,
691
- args: t.args,
692
- }));
693
- }
749
+ if (replyText && stripThinking(replyText) !== replyText) meta.thinking_stripped = true;
694
750
  if (saUsage) meta.usage = saUsage;
695
751
  appendGlobalMessage({
696
752
  channel: "telegram",
@@ -699,7 +755,7 @@ class ChannelPoller {
699
755
  actor_id: replyAuthor || "apx",
700
756
  agent_slug: replyAuthor || "apx",
701
757
  author: replyAuthor || "apx",
702
- body: clean || replyText,
758
+ body: toSend,
703
759
  meta,
704
760
  });
705
761
  } catch (e) {
@@ -711,15 +767,12 @@ class ChannelPoller {
711
767
  actor_id: replyAuthor || "apx",
712
768
  agent_slug: replyAuthor || "apx",
713
769
  author: replyAuthor || "apx",
714
- body: `[send_failed] ${clean || replyText}`,
770
+ body: `[send_failed] ${toSend}`,
715
771
  meta: {
716
772
  chat_id,
717
773
  tg_channel: this.channel.name,
718
774
  in_reply_to: u.update_id,
719
775
  send_error: e.message,
720
- ...(saTrace && saTrace.length > 0
721
- ? { tools_called: saTrace.map((t) => ({ tool: t.tool, args: t.args })) }
722
- : {}),
723
776
  ...(saUsage ? { usage: saUsage } : {}),
724
777
  },
725
778
  });
@@ -54,16 +54,10 @@ Argentinian developer; English replies feel broken to him. If you find
54
54
  yourself writing English, stop and rewrite in Spanish before sending.
55
55
  This rule beats every other formatting hint below.
56
56
 
57
- # Cómo se reciben los mensajes de audio
58
- Cuando el usuario manda un audio por Telegram, el sistema lo transcribe
59
- automáticamente y te lo entrega en este formato:
60
- [audio] <texto transcripto del audio>
61
-
62
- Cuando veas "[audio]" al inicio del mensaje, significa que el usuario HABLÓ ese
63
- mensaje — lo que viene después es la transcripción exacta de lo que dijo.
64
- Tratalo exactamente igual que si el usuario lo hubiera escrito, pero sabiendo
65
- que fue hablado. Nunca le digas al usuario que "no escuchaste nada" o que "no
66
- hay ningún audio" — el audio YA fue procesado y lo tenés en texto delante tuyo.
57
+ # Mensajes de audio
58
+ Si un mensaje empieza con "[audio]", lo que sigue es la transcripción de un
59
+ audio que el usuario habló. Tratalo como su mensaje normal — no digas que "no
60
+ escuchaste nada".
67
61
 
68
62
  # What you must NOT do
69
63
  - Do NOT explain code or write essays about "the provided snippet".
@@ -140,7 +134,7 @@ HARD RULES (do not deviate):
140
134
  15. NO-PENDING RULE: never say "give me a second", "I will do it", or "I will try later" as a final answer. Either call the tool in this same turn or say what blocks you.
141
135
  16. IDENTITY RULE: when the user asks you to change your name, call yourself something, or update your personality/language, call set_identity and persist the change. Then confirm with your new name.
142
136
  17. ROUTINES RULE: NEVER create a routine in the default project (id=0). Routines MUST be tied to a specific registered project. Before adding a routine, call list_projects to find the correct project id or name. Then pass --project <id|name> to apx routine add. If no project fits, ask the user which project to use. Creating routines in project 0/default mixes unrelated projects' schedules and corrupts state.
143
- 18. **NO BARE ACKS AS FINAL ANSWER**: Empty acknowledgments ("ok", "entendido", "dame un minuto", "voy", "checking") are invalid as a FINAL response when a tool was needed they will be re-prompted. EXCEPTION: a short contextual ack sent via send_telegram BEFORE another tool call is encouraged on Telegram audio inputs and on tool calls that take more than a few seconds (browser_screenshot, web_search, run_shell, long file edits). The ack must be **contextual and varied** in Spanish e.g. "Ya te escucho 🎧", "Dame un seg, transcribiendo…", "Buscando eso ahora", "Voy a revisar el repo…", "Un momento, ejecutando…". Never reuse the exact same ack twice in a row. The ack is the FIRST tool call in the turn; the actual work follows immediately in the SAME turn (do not return without doing the work).
137
+ 18. **NO BARE ACKS**: Empty acknowledgments ("ok", "entendido", "dame un minuto", "voy", "checking", "ya te escucho", "ahora lo reviso") are never a valid messagenot as a final answer and not as a standalone update. Don't announce that you're about to do something: just do it and report. The user already sees your progress step by step (each iteration's text is shown as its own message), so every line you produce must carry real content a result, a finding, or a concrete question.
144
138
  19. **CWD RULE**: When the channel context includes a "CWD: <path>" line, that is the user's current working directory. References to "este directorio", "este proyecto", "esta carpeta", "acá", "aquí", "this directory", "this project", "current dir/folder" all mean that exact CWD path. Use it as the path argument directly — DO NOT ask the user "what's the path?" when CWD is already given. Example: if user says "agregá este proyecto a la lista", call add_project({path: <CWD>}) immediately.
145
139
  20. **NO MANUAL SCAFFOLDING**: To register or scaffold a project, ALWAYS use add_project — it auto-creates AGENTS.md and .apc/project.json when missing (one call, atomic). NEVER write AGENTS.md, .apc/project.json, or any APC scaffold file by hand via run_shell / write_file / shell pipes. The schema must come from the official initApf scaffold, not improvised. If add_project errors, report the error to the user — don't try to work around it with shell hacks. Same for any other APC-managed file (.apc/agents/*, .apc/skills/*, etc.) — use the dedicated tool, never raw filesystem writes.
146
140
  21. **SKILLS — ON DEMAND**: The "# Available skills" section below lists every skill available to you (slug + description, NO body). When the user asks about specific APX/APC commands, project structure, agent runtimes, or anything where exact syntax or detailed behavior matches a skill description (in ANY language — match semantically, not by keyword), call load_skill({slug}) to fetch the full markdown body. If a CWD is in the contextNote, pass it as project_path so project-scoped skills resolve. If the user explicitly asks "what skills do you have?", you can either read the catalog below directly OR call list_skills to get a fresh enumeration. Do NOT load skills for trivial / unrelated questions — that wastes tokens. Don't guess CLI syntax when a skill can tell you; load it.
@@ -276,7 +270,7 @@ export async function runSuperAgent({
276
270
  .map((p) => ` ${p.id}: ${p.id === 0 ? "[default]" : "[project]"} "${p.name}" (${p.path})`)
277
271
  .join("\n");
278
272
 
279
- const permissionMode = sa.permission_mode || "total";
273
+ const permissionMode = sa.permission_mode || "automatico";
280
274
  const allowedTools = Array.isArray(sa.allowed_tools) ? sa.allowed_tools : [];
281
275
  const permissionNote = [
282
276
  "# Permission mode",
@@ -2,16 +2,16 @@ export type LogoShape = { left: string[]; right: string[] }
2
2
 
3
3
  export const logo: LogoShape = {
4
4
  left: [
5
- " __ ____ _ _ ",
6
- " /__\\ ( _ )( \\/ ) ",
7
- "/ _ \\ ) __/ ) ( ",
8
- "\\___/ (__) (__/\\_)",
5
+ " _ ___ __ __",
6
+ " /_\\ | _ \\\\ \\/ /",
7
+ " / _ \\ | _/ > < ",
8
+ "/_/ \\_\\|_| /_/\\_\\",
9
9
  ],
10
10
  right: [
11
- " __ ____ _ _ ",
12
- " / _\\ ( _ \\( \\/ ) ",
13
- "/ \\ )___/ ) / ",
14
- "\\_/\\_/(__) (__/ ",
11
+ " ___ ___ ___ ___ ",
12
+ " / __| / _ \\ | \\ | __|",
13
+ "| (__ | (_) || |) || _| ",
14
+ " \\___| \\___/ |___/ |___|",
15
15
  ],
16
16
  }
17
17