@agentprojectcontext/apx 1.10.1 → 1.10.2

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.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -300,31 +300,93 @@ async function submitPrompt(pid, state, previousMessages, renderScreen, close) {
300
300
  model: state.activeModel,
301
301
  };
302
302
 
303
- const result = await http.post(`/projects/${pid}/super-agent/chat`, body);
304
- state.transcript.pop();
305
- for (const trace of result.trace || []) {
306
- state.transcript.push({ type: "tool", trace });
303
+ let result;
304
+ try {
305
+ result = await http.streamPost(
306
+ `/projects/${pid}/super-agent/chat/stream`,
307
+ body,
308
+ (event) => handleProgressEvent(event, state, renderScreen)
309
+ );
310
+ } catch (e) {
311
+ if (e.status !== 404) throw e;
312
+ result = await http.post(`/projects/${pid}/super-agent/chat`, body);
313
+ removeStatus(state);
314
+ for (const trace of result.trace || []) {
315
+ state.transcript.push({ type: "tool", trace });
316
+ }
307
317
  }
308
318
 
309
- previousMessages.push({ role: "user", content: text });
310
- previousMessages.push({ role: "assistant", content: result.text });
311
- if (previousMessages.length > 20) previousMessages.splice(0, previousMessages.length - 20);
319
+ completeSuperAgentResult(result, text, startTime, state, previousMessages);
320
+ } catch (e) {
321
+ removeStatus(state);
322
+ state.transcript.push({ type: "error", text: e.message });
323
+ }
324
+
325
+ renderScreen();
326
+ }
312
327
 
313
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
314
- state.usage.input += result.usage?.input_tokens || 0;
315
- state.usage.output += result.usage?.output_tokens || 0;
316
- state.usage.percent = Math.min(99, Math.round((state.usage.input / 200000) * 100));
328
+ function removeStatus(state) {
329
+ const last = state.transcript[state.transcript.length - 1];
330
+ if (last?.type === "status") state.transcript.pop();
331
+ }
317
332
 
333
+ function handleProgressEvent(event, state, renderScreen) {
334
+ if (event.type === "model_start") {
335
+ const last = state.transcript[state.transcript.length - 1];
336
+ if (last?.type === "status") {
337
+ last.text = event.iteration > 1 ? `Thinking... step ${event.iteration}` : "Thinking...";
338
+ renderScreen();
339
+ }
340
+ return;
341
+ }
342
+
343
+ if (event.type === "assistant_text" && event.text) {
344
+ removeStatus(state);
318
345
  state.transcript.push({
319
346
  type: "assistant",
320
347
  name: state.activeAgent,
321
- text: result.text,
322
- meta: `${elapsed}s · In: ${result.usage?.input_tokens || 0} Out: ${result.usage?.output_tokens || 0}`,
348
+ text: event.text,
349
+ meta: "intermediate",
323
350
  });
324
- } catch (e) {
325
- state.transcript.pop();
326
- state.transcript.push({ type: "error", text: e.message });
351
+ renderScreen();
352
+ return;
327
353
  }
328
354
 
329
- renderScreen();
355
+ if (event.type === "tool_start" && event.trace) {
356
+ removeStatus(state);
357
+ state.transcript.push({ type: "tool", trace: event.trace });
358
+ renderScreen();
359
+ return;
360
+ }
361
+
362
+ if (event.type === "tool_result" && event.trace) {
363
+ removeStatus(state);
364
+ const idx = state.transcript.findIndex(
365
+ (item) => item.type === "tool" && item.trace?.id && item.trace.id === event.trace.id
366
+ );
367
+ if (idx >= 0) state.transcript[idx] = { type: "tool", trace: event.trace };
368
+ else state.transcript.push({ type: "tool", trace: event.trace });
369
+ renderScreen();
370
+ }
371
+ }
372
+
373
+ function completeSuperAgentResult(result, userText, startTime, state, previousMessages) {
374
+ removeStatus(state);
375
+ if (!result) throw new Error("super-agent stream ended without final result");
376
+
377
+ previousMessages.push({ role: "user", content: userText });
378
+ previousMessages.push({ role: "assistant", content: result.text });
379
+ if (previousMessages.length > 20) previousMessages.splice(0, previousMessages.length - 20);
380
+
381
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
382
+ state.usage.input += result.usage?.input_tokens || 0;
383
+ state.usage.output += result.usage?.output_tokens || 0;
384
+ state.usage.percent = Math.min(99, Math.round((state.usage.input / 200000) * 100));
385
+
386
+ state.transcript.push({
387
+ type: "assistant",
388
+ name: state.activeAgent,
389
+ text: result.text,
390
+ meta: `${elapsed}s · In: ${result.usage?.input_tokens || 0} Out: ${result.usage?.output_tokens || 0}`,
391
+ });
330
392
  }
package/src/cli/http.js CHANGED
@@ -91,9 +91,66 @@ async function request(method, path, body, opts = {}) {
91
91
  return json;
92
92
  }
93
93
 
94
+ async function streamRequest(method, path, body, onEvent, opts = {}) {
95
+ if (opts.autoStart !== false) await ensureDaemon();
96
+ else if (!(await ping())) {
97
+ throw new Error(`apx daemon not running (no response on ${baseUrl()})`);
98
+ }
99
+
100
+ const res = await fetch(`${baseUrl()}${path}`, {
101
+ method,
102
+ headers: body ? { "content-type": "application/json" } : {},
103
+ body: body ? JSON.stringify(body) : undefined,
104
+ });
105
+
106
+ if (!res.ok) {
107
+ const text = await res.text();
108
+ let json = null;
109
+ try { json = text ? JSON.parse(text) : null; } catch {}
110
+ const err = new Error(json?.error || `${method} ${path} → ${res.status}`);
111
+ err.status = res.status;
112
+ throw err;
113
+ }
114
+
115
+ if (!res.body?.getReader) {
116
+ throw new Error("streaming response is not supported by this Node.js runtime");
117
+ }
118
+
119
+ const reader = res.body.getReader();
120
+ const decoder = new TextDecoder();
121
+ let buffer = "";
122
+ let finalResult = null;
123
+
124
+ while (true) {
125
+ const { value, done } = await reader.read();
126
+ if (done) break;
127
+ buffer += decoder.decode(value, { stream: true });
128
+ const lines = buffer.split(/\r?\n/);
129
+ buffer = lines.pop() || "";
130
+ for (const line of lines) {
131
+ if (!line.trim()) continue;
132
+ const event = JSON.parse(line);
133
+ if (event.type === "final") finalResult = event.result;
134
+ if (event.type === "error") throw new Error(event.error || "stream error");
135
+ await onEvent?.(event);
136
+ }
137
+ }
138
+
139
+ buffer += decoder.decode();
140
+ if (buffer.trim()) {
141
+ const event = JSON.parse(buffer);
142
+ if (event.type === "final") finalResult = event.result;
143
+ if (event.type === "error") throw new Error(event.error || "stream error");
144
+ await onEvent?.(event);
145
+ }
146
+
147
+ return finalResult;
148
+ }
149
+
94
150
  export const http = {
95
151
  get: (p, opts) => request("GET", p, undefined, opts),
96
152
  post: (p, body, opts) => request("POST", p, body, opts),
153
+ streamPost: (p, body, onEvent, opts) => streamRequest("POST", p, body, onEvent, opts),
97
154
  put: (p, body, opts) => request("PUT", p, body, opts),
98
155
  patch: (p, body, opts) => request("PATCH", p, body, opts),
99
156
  delete: (p, opts) => request("DELETE", p, undefined, opts),
@@ -229,6 +229,11 @@ function addToolBlock(lines, item, width) {
229
229
  addLine(lines, "", C.bg);
230
230
  addLine(lines, margin + C.muted + `→ ${label}${target ? " " + fit(String(target), inner) : ""}`, C.bg);
231
231
 
232
+ if (trace.pending) {
233
+ addLine(lines, margin + C.dim + " " + C.muted + "running...", C.bg);
234
+ return;
235
+ }
236
+
232
237
  if (trace.tool === "write_file") {
233
238
  const heading = `# Wrote ${args.path || "file"}`;
234
239
  addLine(lines, margin + C.panel + " " + C.muted + heading + " ".repeat(Math.max(0, inner - visible(heading))), C.bg);
package/src/daemon/api.js CHANGED
@@ -525,6 +525,50 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
525
525
  });
526
526
 
527
527
  // POST /projects/:pid/super-agent/chat
528
+ app.post("/projects/:pid/super-agent/chat/stream", async (req, res) => {
529
+ const p = project(req, res);
530
+ if (!p) return;
531
+ const { prompt, contextNote, previousMessages, model } = req.body || {};
532
+ if (!prompt) return res.status(400).json({ error: "prompt required" });
533
+
534
+ res.setHeader("content-type", "application/x-ndjson; charset=utf-8");
535
+ res.setHeader("cache-control", "no-cache, no-transform");
536
+ res.setHeader("x-accel-buffering", "no");
537
+ res.flushHeaders?.();
538
+
539
+ const send = (event) => {
540
+ res.write(JSON.stringify(event) + "\n");
541
+ };
542
+
543
+ try {
544
+ const saResult = await runSuperAgent({
545
+ globalConfig: config,
546
+ projects,
547
+ plugins,
548
+ registries,
549
+ prompt,
550
+ contextNote: contextNote || `Context: Project ${p.id} (${p.name}) at ${p.path}`,
551
+ previousMessages: previousMessages || [],
552
+ overrideModel: model,
553
+ onEvent: send,
554
+ });
555
+ projects.rebuild(p.id);
556
+ send({
557
+ type: "final",
558
+ result: {
559
+ text: saResult.text,
560
+ usage: saResult.usage,
561
+ name: saResult.name,
562
+ trace: saResult.trace,
563
+ },
564
+ });
565
+ res.end();
566
+ } catch (e) {
567
+ send({ type: "error", error: e.message });
568
+ res.end();
569
+ }
570
+ });
571
+
528
572
  app.post("/projects/:pid/super-agent/chat", async (req, res) => {
529
573
  const p = project(req, res);
530
574
  if (!p) return;
@@ -8,6 +8,23 @@ export default {
8
8
  async chat({ system, messages, model = "mock" }) {
9
9
  const last = [...messages].reverse().find((m) => m.role === "user");
10
10
  const userText = last?.content || "";
11
+ const requestedTool = userText.match(/\[mock:tool:([a-z_]+)\]/)?.[1];
12
+ const hasToolResult = messages.some((m) => m.role === "tool");
13
+ if (requestedTool && !hasToolResult) {
14
+ const toolCall = {
15
+ id: "mock-call-1",
16
+ type: "function",
17
+ function: { name: requestedTool, arguments: "{}" },
18
+ };
19
+ return {
20
+ text: "",
21
+ tool_calls: [toolCall],
22
+ message: { tool_calls: [toolCall] },
23
+ usage: { input_tokens: userText.length, output_tokens: 4 },
24
+ raw: { model, mock: true },
25
+ };
26
+ }
27
+
11
28
  const sysHint = system ? ` (system: ${system.slice(0, 40)}…)` : "";
12
29
  return {
13
30
  text: `[mock:${model}] received: ${userText}${sysHint}`,
@@ -7,6 +7,8 @@ import {
7
7
  createRuntimeSession,
8
8
  extractApfResult,
9
9
  } from "../../apc-runtime-context.js";
10
+ import { detectAll } from "../../env-detect.js";
11
+ import { runProcess } from "../../runtimes/_spawn.js";
10
12
  import { getRuntime, RUNTIME_IDS } from "../../runtimes/index.js";
11
13
  import { buildAgentSystem, confirmedProperty, resolveProject } from "../helpers.js";
12
14
 
@@ -61,18 +63,43 @@ function buildRuntimeSystem(project, agent, runtime, sessionId, caller) {
61
63
  ].join("\n\n");
62
64
  }
63
65
 
66
+ async function runtimeAvailability(runtime, rt) {
67
+ const probe = await runProcess({
68
+ command: rt.binary,
69
+ args: rt.versionFlag ? [rt.versionFlag] : ["--version"],
70
+ timeoutMs: 3000,
71
+ });
72
+ if (probe.exitCode === 0 || probe.stdout || probe.stderr) {
73
+ return { ok: true };
74
+ }
75
+
76
+ const detected = await detectAll();
77
+ const current = detected.find((d) => d.id === runtime || d.binary === rt.binary);
78
+ if (current?.installed) {
79
+ return { ok: true, detected };
80
+ }
81
+ return {
82
+ ok: false,
83
+ reason: current?.reason || `${rt.binary} not found`,
84
+ detected,
85
+ installed: detected
86
+ .filter((d) => d.category === "runtime" && d.installed)
87
+ .map((d) => d.id),
88
+ };
89
+ }
90
+
64
91
  export default {
65
92
  name: "call_runtime",
66
93
  schema: {
67
94
  type: "function",
68
95
  function: {
69
96
  name: "call_runtime",
70
- description: "Spawn an external CLI runtime (Claude Code, Codex, OpenCode, Aider, Cursor Agent, Gemini CLI, Qwen Code), optionally impersonating an APC agent.",
97
+ description: "Spawn an external CLI runtime (Claude Code, Codex, OpenCode, Aider, Cursor Agent, Gemini CLI, Qwen Code). Omit agent for the base APX/default self-run.",
71
98
  parameters: {
72
99
  type: "object",
73
100
  properties: {
74
101
  project: { type: "string" },
75
- agent: { type: "string", description: "Optional APC agent slug from AGENTS.md, not runtime name. Omit when the user did not name an agent." },
102
+ agent: { type: "string", description: "Optional APC agent slug from AGENTS.md, not runtime name. Use only when the user explicitly named that agent. Omit for 'vos mismo', 'default', 'base', or no agent." },
76
103
  runtime: {
77
104
  type: "string",
78
105
  enum: RUNTIME_IDS,
@@ -108,6 +135,19 @@ export default {
108
135
  return { error: `${e.message}. Available runtimes: ${RUNTIME_IDS.join(", ")}` };
109
136
  }
110
137
 
138
+ const availability = await runtimeAvailability(runtime, rt);
139
+ if (!availability.ok) {
140
+ return {
141
+ error: `runtime "${runtime}" is not installed or not runnable (${availability.reason})`,
142
+ runtime,
143
+ binary: rt.binary,
144
+ installed_runtimes: availability.installed,
145
+ hint: availability.installed.length
146
+ ? `Try one of: ${availability.installed.join(", ")}`
147
+ : "No external runtime CLIs were detected. Run apx env detect for details.",
148
+ };
149
+ }
150
+
111
151
  const actor = agent?.slug || "apx";
112
152
  const session = createRuntimeSession({
113
153
  projectRoot: p.path,
@@ -163,6 +203,7 @@ export default {
163
203
  apc_session: session.id,
164
204
  exit_code: r.exitCode,
165
205
  output: (r.output || "").slice(0, 4000),
206
+ stderr: (r.stderr || "").slice(0, 2000),
166
207
  truncated: (r.output || "").length > 4000,
167
208
  external_session_path: r.externalSessionPath || null,
168
209
  session_id: r.sessionId || null,
@@ -53,13 +53,14 @@ HARD RULES (do not deviate):
53
53
  7. Stay brief: under 6 sentences unless asked for detail.
54
54
  8. You DO see recent prior turns of this chat as previous messages when applicable. **Use them ONLY to disambiguate references** (e.g. "el primero" → first project mentioned earlier). For ANY factual data — agent details, MCP details, file contents, memory — RE-CALL the tool. Past turns are context, not a cache. Models change, agents change, files change.
55
55
  9. /reset or /new from the user means "forget previous turns and answer this one fresh" — if you see those prefixes the operator already cleared the context for you.
56
- 10. DELEGATION RULE: when the user asks an agent to do a task, use call_agent (unless they specify opening it in a runtime, then see rule 11).
57
- 11. DISPATCH RULE: when the user asks to work inside Claude, Codex, OpenCode, Aider, Cursor, Gemini CLI, or Qwen Code, use call_runtime({runtime: 'claude-code'|'codex'|'opencode'|'aider'|'cursor-agent'|'gemini-cli'|'qwen-code', prompt: <user's request>}). If they explicitly name an agent to spawn, pass agent: <slug>. If they don't name an agent, DO NOT pass an agent argument. When an agent is passed, its memory + skills become the system prompt of the runtime.
58
- 12. PROJECT RULE: when the user gives no project, use project "default". Do not infer a non-default project from old chat history unless the user references it. If they mention a path or project name, look it up or add it with add_project.
59
- 13. VAULT RULE: when the user wants a new existing agent/template, call list_vault_agents first. If a suitable vault agent exists, import_agent into the chosen project. If none fits, say briefly what is missing.
60
- 14. 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.
61
- 15. 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.
62
- 16. 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.`;
56
+ 10. SELF-RUN RULE: if the user says "vos mismo", "tu mismo", "same", "base", "default", "sin agente", or does not explicitly name an agent slug, act as APX itself. Do NOT call list_agents to choose a candidate. Do NOT pass agent to call_runtime/call_agent.
57
+ 11. DELEGATION RULE: when the user asks a named APC agent to do a task, use call_agent (unless they specify opening it in a runtime, then see rule 12).
58
+ 12. DISPATCH RULE: when the user asks to work inside Claude, Codex, OpenCode, Aider, Cursor, Gemini CLI, or Qwen Code, use call_runtime({runtime: 'claude-code'|'codex'|'opencode'|'aider'|'cursor-agent'|'gemini-cli'|'qwen-code', prompt: <user's request>}). First prefer runtimes the tool reports as installed/runnable; if a runtime is missing or fails, report that fact and its stderr/error instead of pretending success. If the user explicitly named an agent slug, pass agent: <slug>. If they didn't, DO NOT pass agent. When an agent is passed, its memory + skills become the system prompt of the runtime.
59
+ 13. PROJECT RULE: when the user gives no project, use project "default". Do not infer a non-default project from old chat history unless the user references it. If they mention a path or project name, look it up or add it with add_project.
60
+ 14. VAULT RULE: when the user wants a new existing agent/template, call list_vault_agents first. If a suitable vault agent exists, import_agent into the chosen project. If none fits, say briefly what is missing.
61
+ 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.
62
+ 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.
63
+ 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.`;
63
64
 
64
65
  function isShortConfirmation(text) {
65
66
  return /^(yes|y|si|si dale|dale|ok|okay|confirm|confirmed|go|proceed|do it)\b/i
@@ -87,6 +88,7 @@ export async function runSuperAgent({
87
88
  contextNote = "",
88
89
  previousMessages = [],
89
90
  overrideModel = null,
91
+ onEvent = null,
90
92
  }) {
91
93
  if (!isSuperAgentEnabled(globalConfig)) {
92
94
  throw new Error("super-agent not enabled (set super_agent.enabled and .model in ~/.apx/config.json)");
@@ -141,6 +143,7 @@ export async function runSuperAgent({
141
143
  let lastText = "";
142
144
 
143
145
  for (let iter = 0; iter < MAX_TOOL_ITERS; iter++) {
146
+ await emitProgress(onEvent, { type: "model_start", iteration: iter + 1 });
144
147
  const result = await callEngine({
145
148
  modelId: activeModel,
146
149
  system,
@@ -174,6 +177,11 @@ export async function runSuperAgent({
174
177
  break;
175
178
  }
176
179
 
180
+ const visibleText = cleanTextOfPseudoToolCalls(lastText).trim();
181
+ if (visibleText) {
182
+ await emitProgress(onEvent, { type: "assistant_text", text: visibleText, iteration: iter + 1 });
183
+ }
184
+
177
185
  // Append the assistant turn (with its tool_calls) and execute each call.
178
186
  conversation.push({
179
187
  role: "assistant",
@@ -191,6 +199,12 @@ export async function runSuperAgent({
191
199
  args = args || {};
192
200
 
193
201
  let toolResult;
202
+ const traceId = `${iter + 1}:${trace.length + 1}`;
203
+ await emitProgress(onEvent, {
204
+ type: "tool_start",
205
+ trace: { id: traceId, tool: name, args, pending: true },
206
+ iteration: iter + 1,
207
+ });
194
208
  try {
195
209
  const handler = handlers[name];
196
210
  if (!handler) {
@@ -202,7 +216,13 @@ export async function runSuperAgent({
202
216
  toolResult = { error: e.message };
203
217
  }
204
218
 
205
- trace.push({ tool: name, args, result: summarizeForTrace(toolResult) });
219
+ const traceItem = { id: traceId, tool: name, args, result: summarizeForTrace(toolResult) };
220
+ trace.push(traceItem);
221
+ await emitProgress(onEvent, {
222
+ type: "tool_result",
223
+ trace: traceItem,
224
+ iteration: iter + 1,
225
+ });
206
226
 
207
227
  conversation.push({
208
228
  role: "tool",
@@ -220,6 +240,11 @@ export async function runSuperAgent({
220
240
  };
221
241
  }
222
242
 
243
+ async function emitProgress(onEvent, event) {
244
+ if (typeof onEvent !== "function") return;
245
+ await onEvent(event);
246
+ }
247
+
223
248
  function summarizeForTrace(r) {
224
249
  if (r === null || r === undefined) return r;
225
250
  const s = JSON.stringify(r);