@agentprojectcontext/apx 1.33.0 → 1.33.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.33.0",
3
+ "version": "1.33.1",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: apc-context
3
- description: "Activate whenever the project has a .apc/ directory or AGENTS.md — read .apc/ before assuming anything about agents, memory, or structure. If .apc/migrate.md exists, open with a migration offer first; if the user declines, delete it. Triggers: .apc/, AGENTS.md, 'which agents', 'list agents', 'agent context', 'who are the agents'."
3
+ description: "ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. Do not wait to be asked. Read .apc/ before making any assumption about agents, memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', 'list agents', 'agent context', 'who are the agents', any question about agents or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the conversation with a migration offer before answering anything else. If the user declines, delete .apc/migrate.md immediately so it is not shown again."
4
4
  homepage: https://github.com/agentprojectcontext/agentprojectcontext
5
5
  ---
6
6
 
@@ -66,13 +66,11 @@ After migration:
66
66
  ```text
67
67
  AGENTS.md ← root project contract
68
68
  .apc/
69
- project.json ← project metadata (may carry an `apx` field)
70
- config.json ← project-only config overrides (e.g. super_agent.model)
69
+ project.json ← project metadata
71
70
  .gitignore ← safety guard
72
71
  agents/<name>.md ← agent definition
73
72
  agents/<name>/memory.md ← optional curated project memory
74
73
  skills/<name>.md ← reusable project instructions
75
- commands/ ← custom slash-commands (optional)
76
74
  mcps.json ← MCP hints without secrets
77
75
  ```
78
76
 
@@ -83,7 +81,6 @@ Do not store:
83
81
  .apc/sessions/
84
82
  .apc/conversations/
85
83
  .apc/messages/
86
- .apc/project.db
87
84
  .apc/cache/
88
85
  .apc/tmp/
89
86
  .apc/private/
@@ -123,7 +123,7 @@ import { fileURLToPath } from "node:url";
123
123
  const __parserDir = path.dirname(fileURLToPath(import.meta.url));
124
124
 
125
125
  export const VAULT_DIR = path.join(os.homedir(), ".apx", "agents");
126
- export const BUNDLED_VAULT_DIR = path.resolve(__parserDir, "../../assets/agent-vault-defaults");
126
+ export const BUNDLED_VAULT_DIR = path.resolve(__parserDir, "../../../assets/agent-vault-defaults");
127
127
  export const VAULT_TOMBSTONE_PATH = path.join(VAULT_DIR, ".removed.json");
128
128
 
129
129
  function readVaultDirRaw(dir) {
@@ -16,7 +16,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
16
  // Now under src/core/apc/ — one more "../" to escape than before.
17
17
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..");
18
18
  const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
19
- const RUNTIME_SKILLS_DIR = path.join(__dirname, "runtime-skills");
19
+ // runtime-skills lives at src/core/runtime-skills/, one level up from this
20
+ // file's new home in src/core/apc/ (was a sibling before the Phase 3 move).
21
+ const RUNTIME_SKILLS_DIR = path.join(__dirname, "..", "runtime-skills");
20
22
 
21
23
  export const SPEC_VERSION = "0.1.0";
22
24
 
@@ -4,7 +4,9 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- export const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
7
+ // __dirname is src/core/apc/ after the Phase 3 move (was src/core/ before).
8
+ // Repo root is three levels up, not two.
9
+ export const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..");
8
10
 
9
11
  export const APC_SKILL_REL = path.join("skills", "apc-context", "SKILL.md");
10
12
  export const APC_SKILL_REMOTE =
@@ -51,15 +51,24 @@ function toGeminiContents(messages) {
51
51
  if (m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
52
52
  out.push({
53
53
  role: "model",
54
- parts: m.tool_calls.map((tc) => ({
55
- functionCall: {
56
- name: tc.function?.name || tc.name,
57
- args:
58
- typeof tc.function?.arguments === "string"
59
- ? safeParseJson(tc.function.arguments)
60
- : tc.function?.arguments || tc.arguments || {},
61
- },
62
- })),
54
+ parts: m.tool_calls.map((tc) => {
55
+ const part = {
56
+ functionCall: {
57
+ name: tc.function?.name || tc.name,
58
+ args:
59
+ typeof tc.function?.arguments === "string"
60
+ ? safeParseJson(tc.function.arguments)
61
+ : tc.function?.arguments || tc.arguments || {},
62
+ },
63
+ };
64
+ // Gemini 3.x thinking models require us to echo back the
65
+ // thoughtSignature that came attached to the original functionCall
66
+ // part, or the API rejects the next turn with 400. We captured it
67
+ // in the response parser; replay it verbatim when present.
68
+ const sig = tc._thoughtSignature || tc.thought_signature;
69
+ if (sig) part.thoughtSignature = sig;
70
+ return part;
71
+ }),
63
72
  });
64
73
  continue;
65
74
  }
@@ -143,14 +152,22 @@ export default {
143
152
  for (const p of parts) {
144
153
  const fc = p.functionCall || p.function_call;
145
154
  if (fc?.name) {
146
- toolCalls.push({
155
+ const tc = {
147
156
  id: `gemini_${randomUUID().slice(0, 8)}`,
148
157
  type: "function",
149
158
  function: {
150
159
  name: fc.name,
151
160
  arguments: typeof fc.args === "string" ? fc.args : JSON.stringify(fc.args || {}),
152
161
  },
153
- });
162
+ };
163
+ // Thinking models (Gemini 3.x) attach a thoughtSignature to the part
164
+ // alongside the functionCall. We must replay it on the next request
165
+ // or the API 400s. Carry it on the tool_call so the next call to
166
+ // toGeminiContents() can put it back. Underscore prefix marks it as
167
+ // adapter-private metadata other engines should ignore.
168
+ const sig = p.thoughtSignature || p.thought_signature;
169
+ if (sig) tc._thoughtSignature = sig;
170
+ toolCalls.push(tc);
154
171
  }
155
172
  }
156
173
 
@@ -52,12 +52,22 @@ export async function callEngine({ modelId, system, messages, config, temperatur
52
52
  const { provider, model } = resolveProvider(modelId);
53
53
  const adapter = getAdapter(provider);
54
54
  const providerCfg = (config && config.engines && config.engines[provider]) || {};
55
+ // The per-provider `default_max_tokens` set in the web admin (Provider modal
56
+ // slider) acts as a floor: callers may ask for more, but never less. This
57
+ // matters for "thinking" models (e.g. Gemini 3.x) whose internal reasoning
58
+ // tokens count against maxOutputTokens — too low a cap and the visible reply
59
+ // gets truncated mid-sentence. Fallback chain:
60
+ // caller value → provider cfg → 2048 (safe baseline that survives thinking
61
+ // models without truncating; non-thinking models just don't fill it).
62
+ const providerCap = Number(providerCfg.default_max_tokens) || 0;
63
+ const callerCap = Number(maxTokens) || 0;
64
+ const effectiveMaxTokens = Math.max(callerCap, providerCap) || 2048;
55
65
  return adapter.chat({
56
66
  system,
57
67
  messages,
58
68
  model,
59
69
  temperature,
60
- maxTokens,
70
+ maxTokens: effectiveMaxTokens,
61
71
  tools,
62
72
  toolChoice,
63
73
  config: providerCfg,
@@ -13,6 +13,11 @@ const DEFAULT_BASE = {
13
13
  ollama: "http://localhost:11434",
14
14
  };
15
15
 
16
+ // Gemini's native models endpoint returns a much richer catalog than the
17
+ // OpenAI-compat shim (which only echoes back a handful). We always query the
18
+ // native URL regardless of the user's configured base_url.
19
+ const GEMINI_NATIVE_BASE = "https://generativelanguage.googleapis.com/v1beta";
20
+
16
21
  // Returns { models } or { error }. Reads the right /models endpoint per engine.
17
22
  async function listModels(engine, baseUrl, apiKey) {
18
23
  const base = String(baseUrl || DEFAULT_BASE[engine] || "").replace(/\/$/, "");
@@ -37,7 +42,32 @@ async function listModels(engine, baseUrl, apiKey) {
37
42
  return { models: data.map((m) => m?.id).filter(Boolean) };
38
43
  }
39
44
 
40
- // openai-compatible family: openai, groq, openrouter, gemini, azure, custom
45
+ if (engine === "gemini") {
46
+ if (!apiKey) return { error: "falta api_key" };
47
+ // Native Gemini API: returns a `models` array with rich metadata, including
48
+ // `supportedGenerationMethods` so we can drop embeddings/vision-only entries.
49
+ // Names come back as "models/<id>"; strip the prefix for display.
50
+ const r = await fetchJsonWithTimeout(
51
+ `${GEMINI_NATIVE_BASE}/models?key=${encodeURIComponent(apiKey)}&pageSize=200`,
52
+ { timeoutMs: 5000 },
53
+ );
54
+ if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
55
+ const data = Array.isArray(r.json?.models) ? r.json.models : [];
56
+ const models = data
57
+ .filter((m) => {
58
+ const methods = m?.supportedGenerationMethods;
59
+ if (!Array.isArray(methods)) return true;
60
+ return methods.includes("generateContent");
61
+ })
62
+ .map((m) => {
63
+ const name = typeof m?.name === "string" ? m.name : "";
64
+ return name.startsWith("models/") ? name.slice("models/".length) : name;
65
+ })
66
+ .filter(Boolean);
67
+ return { models };
68
+ }
69
+
70
+ // openai-compatible family: openai, groq, openrouter, azure, custom
41
71
  if (!apiKey) return { error: "falta api_key" };
42
72
  if (!base) return { error: "falta base_url" };
43
73
  const r = await fetchJsonWithTimeout(`${base}/models`, {