@agentprojectcontext/apx 1.31.0 → 1.31.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/README.md +0 -1
- package/package.json +1 -1
- package/skills/apc-context/SKILL.md +0 -1
- package/skills/apx-agency-agents/SKILL.md +1 -1
- package/skills/apx-agent/SKILL.md +6 -6
- package/skills/apx-project/SKILL.md +1 -2
- package/src/core/agent/self-memory.js +1 -1
- package/src/core/agent-memory.js +64 -0
- package/src/core/agent-system.js +3 -2
- package/src/core/confirmation/adapters/code.js +41 -0
- package/src/core/confirmation/adapters/telegram.js +134 -0
- package/src/core/confirmation/adapters/terminal.js +35 -0
- package/src/core/confirmation/adapters/web.js +53 -0
- package/src/core/confirmation/index.js +44 -0
- package/src/core/confirmation/pending-store.js +68 -0
- package/src/core/scaffold.js +43 -18
- package/src/core/tools/registry.js +7 -7
- package/src/host/daemon/api/agents.js +19 -21
- package/src/host/daemon/api/code.js +2 -0
- package/src/host/daemon/api/confirm.js +30 -0
- package/src/host/daemon/api/sessions-search.js +1 -1
- package/src/host/daemon/api/shared.js +5 -8
- package/src/host/daemon/api/super-agent.js +12 -4
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/plugins/telegram.js +28 -0
- package/src/host/daemon/super-agent-tools/helpers.js +27 -6
- package/src/host/daemon/super-agent-tools/index.js +1 -0
- package/src/host/daemon/super-agent-tools/tools/add-project.js +2 -2
- package/src/host/daemon/super-agent-tools/tools/call-mcp.js +1 -1
- package/src/host/daemon/super-agent-tools/tools/call-runtime.js +1 -1
- package/src/host/daemon/super-agent-tools/tools/edit-file.js +2 -2
- package/src/host/daemon/super-agent-tools/tools/import-agent.js +4 -2
- package/src/host/daemon/super-agent-tools/tools/read-agent-memory.js +5 -4
- package/src/host/daemon/super-agent-tools/tools/run-shell.js +1 -1
- package/src/host/daemon/super-agent-tools/tools/search-files.js +1 -4
- package/src/host/daemon/super-agent-tools/tools/send-telegram.js +1 -1
- package/src/host/daemon/super-agent-tools/tools/set-identity.js +2 -2
- package/src/host/daemon/super-agent-tools/tools/set-permission-mode.js +2 -2
- package/src/host/daemon/super-agent-tools/tools/write-file.js +2 -2
- package/src/host/daemon/super-agent.js +5 -1
- package/src/interfaces/cli/commands/agent.js +4 -1
- package/src/interfaces/cli/commands/memory.js +9 -10
- package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js → index-BV615I9p.js} +5 -5
- package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js.map → index-BV615I9p.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/i18n/en.ts +6 -6
- package/src/interfaces/web/src/i18n/es.ts +6 -6
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -59,7 +59,7 @@ apx agent vault list --all
|
|
|
59
59
|
# Create a new template (writes ~/.apx/agents/<slug>.md)
|
|
60
60
|
apx agent vault add reviewer \
|
|
61
61
|
--role "Code reviewer" \
|
|
62
|
-
--model
|
|
62
|
+
--model ollama:llama3.2:3b \
|
|
63
63
|
--language es \
|
|
64
64
|
--skills code-review,git \
|
|
65
65
|
--description "Reviews PRs and pushes back on hand-wavy diffs."
|
|
@@ -5,7 +5,7 @@ description: How to create, configure, and use project agents in APX. Load when
|
|
|
5
5
|
|
|
6
6
|
# apx-agent
|
|
7
7
|
|
|
8
|
-
A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data
|
|
8
|
+
A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data (memory, conversations, sessions) lives under `~/.apx/projects/<apx_id>/agents/<slug>/` and is never committed. APX still reads legacy `.apc/agents/<slug>/memory.md` as a migration fallback only.
|
|
9
9
|
|
|
10
10
|
## Concrete CLI calls
|
|
11
11
|
|
|
@@ -13,10 +13,10 @@ A project agent is a named persona inside an APC project. Canonical definition:
|
|
|
13
13
|
# List agents in a project
|
|
14
14
|
apx agent list --project iacrmar
|
|
15
15
|
|
|
16
|
-
# Create a new agent (writes .apc/agents/<slug>.md
|
|
16
|
+
# Create a new agent (writes .apc/agents/<slug>.md, creates runtime dir, regenerates AGENTS.md)
|
|
17
17
|
apx agent add reviewer \
|
|
18
18
|
--role "Code reviewer" \
|
|
19
|
-
--model
|
|
19
|
+
--model ollama:llama3.2:3b \
|
|
20
20
|
--language es \
|
|
21
21
|
--description "Reviews PRs and pushes back on hand-wavy diffs." \
|
|
22
22
|
--tools read,write,run \
|
|
@@ -45,7 +45,7 @@ apx memory <slug> --project iacrmar --replace < file.md # full replace fro
|
|
|
45
45
|
2. Description (from AGENTS.md).
|
|
46
46
|
3. Role + Language fields.
|
|
47
47
|
4. Invocation context: `engine | telegram | routine | runtime` — the channel calling the agent.
|
|
48
|
-
5. Memory:
|
|
48
|
+
5. Memory: `~/.apx/projects/<apx_id>/agents/<slug>/memory.md` if it exists, with legacy `.apc/agents/<slug>/memory.md` as a migration fallback.
|
|
49
49
|
6. Skills declared in the agent's `Skills:` field, each loaded from `.apc/skills/<slug>.md` or the bundled set.
|
|
50
50
|
7. The `apx` meta-skill (so the agent knows how to operate APX).
|
|
51
51
|
8. ACTION_DISCIPLINE_RULES (fixed footer — anti-ghost, anti-disclaimer, action-first).
|
|
@@ -54,13 +54,13 @@ That's the prompt the engine sees on every `apx exec <agent>` or `apx chat <agen
|
|
|
54
54
|
|
|
55
55
|
## Models per agent
|
|
56
56
|
|
|
57
|
-
Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model.
|
|
57
|
+
Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model. Leave it empty when the agent should follow the project/global default.
|
|
58
58
|
|
|
59
59
|
```markdown
|
|
60
60
|
# .apc/agents/reviewer.md
|
|
61
61
|
---
|
|
62
62
|
Role: Code reviewer
|
|
63
|
-
Model:
|
|
63
|
+
Model: ollama:llama3.2:3b ← this agent uses this model, independent of super_agent.model
|
|
64
64
|
Language: es
|
|
65
65
|
---
|
|
66
66
|
```
|
|
@@ -57,7 +57,6 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
|
|
|
57
57
|
└── .apc/
|
|
58
58
|
├── project.json ← { apxId, name, ... }
|
|
59
59
|
├── agents/<slug>.md
|
|
60
|
-
├── agents/<slug>/memory.md
|
|
61
60
|
├── skills/<slug>.md or <slug>/SKILL.md
|
|
62
61
|
├── mcps.json ← shared MCPs (committed)
|
|
63
62
|
├── commands/ ← custom slash-commands
|
|
@@ -65,7 +64,7 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
|
|
|
65
64
|
|
|
66
65
|
~/.apx/projects/<apxId>/ ← runtime state (never committed)
|
|
67
66
|
├── messages/YYYY-MM-DD.jsonl
|
|
68
|
-
├── agents/<slug>/{sessions/, conversations/}
|
|
67
|
+
├── agents/<slug>/{memory.md, sessions/, conversations/}
|
|
69
68
|
├── routines.json
|
|
70
69
|
├── tasks/YYYY-MM.jsonl
|
|
71
70
|
├── artifacts/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This is distinct from:
|
|
4
4
|
// - identity.json → who Roby is (name, personality, owner)
|
|
5
|
-
// - project agents'
|
|
5
|
+
// - project agents' ~/.apx/projects/<apx_id>/agents/<slug>/memory.md → per-agent, per-project
|
|
6
6
|
// - sessions → raw transcripts of past work (search_sessions)
|
|
7
7
|
//
|
|
8
8
|
// It is a single free-form markdown file at ~/.apx/memory.md that Roby keeps
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { projectStorageRoot } from "./config.js";
|
|
4
|
+
import { getOrCreateApxId } from "./scaffold.js";
|
|
5
|
+
|
|
6
|
+
const EMPTY_MEMORY = (slug) =>
|
|
7
|
+
`# Memory — ${slug}\n\n` +
|
|
8
|
+
`## Identity\n- \n\n` +
|
|
9
|
+
`## Long-term facts\n- \n\n` +
|
|
10
|
+
`## Recent context\n- \n`;
|
|
11
|
+
|
|
12
|
+
export function agentRuntimeDir(projectOrRoot, slug) {
|
|
13
|
+
const storagePath =
|
|
14
|
+
typeof projectOrRoot === "object" && projectOrRoot?.storagePath
|
|
15
|
+
? projectOrRoot.storagePath
|
|
16
|
+
: null;
|
|
17
|
+
const root =
|
|
18
|
+
typeof projectOrRoot === "string"
|
|
19
|
+
? projectOrRoot
|
|
20
|
+
: projectOrRoot?.path;
|
|
21
|
+
const base = storagePath || projectStorageRoot(getOrCreateApxId(root));
|
|
22
|
+
return path.join(base, "agents", slug);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function agentMemoryPath(projectOrRoot, slug) {
|
|
26
|
+
return path.join(agentRuntimeDir(projectOrRoot, slug), "memory.md");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function legacyAgentMemoryPath(projectRoot, slug) {
|
|
30
|
+
return path.join(projectRoot, ".apc", "agents", slug, "memory.md");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ensureAgentRuntimeDir(projectOrRoot, slug, { createMemory = false } = {}) {
|
|
34
|
+
const dir = agentRuntimeDir(projectOrRoot, slug);
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
if (createMemory) {
|
|
37
|
+
const memory = path.join(dir, "memory.md");
|
|
38
|
+
if (!fs.existsSync(memory)) fs.writeFileSync(memory, EMPTY_MEMORY(slug));
|
|
39
|
+
}
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readAgentMemory(projectOrRoot, slug) {
|
|
44
|
+
const primary = agentMemoryPath(projectOrRoot, slug);
|
|
45
|
+
if (fs.existsSync(primary)) return fs.readFileSync(primary, "utf8");
|
|
46
|
+
|
|
47
|
+
const root =
|
|
48
|
+
typeof projectOrRoot === "string"
|
|
49
|
+
? projectOrRoot
|
|
50
|
+
: projectOrRoot?.path;
|
|
51
|
+
if (root) {
|
|
52
|
+
const legacy = legacyAgentMemoryPath(root, slug);
|
|
53
|
+
if (fs.existsSync(legacy)) return fs.readFileSync(legacy, "utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function writeAgentMemory(projectOrRoot, slug, body) {
|
|
60
|
+
ensureAgentRuntimeDir(projectOrRoot, slug);
|
|
61
|
+
const memory = agentMemoryPath(projectOrRoot, slug);
|
|
62
|
+
fs.writeFileSync(memory, body);
|
|
63
|
+
return memory;
|
|
64
|
+
}
|
package/src/core/agent-system.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { readAgentMemory } from "./agent-memory.js";
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Anti-ghost-response rules injected into every agent system prompt.
|
|
@@ -62,8 +63,8 @@ export function buildAgentSystem(project, agent, {
|
|
|
62
63
|
|
|
63
64
|
parts.push(buildInvocationContext({ invocation, runtime, channel, caller, routine }));
|
|
64
65
|
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
66
|
+
const memory = readAgentMemory(project, agent.slug);
|
|
67
|
+
if (memory) parts.push("## Memory\n" + memory);
|
|
67
68
|
|
|
68
69
|
const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
|
|
69
70
|
if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Code-surface confirmation adapter.
|
|
2
|
+
//
|
|
3
|
+
// Same stdin readline pattern as terminal, but formatted for the code TUI:
|
|
4
|
+
// more structured output so it's visually distinct from agent output in the
|
|
5
|
+
// split-pane Build mode. Uses stderr to avoid polluting the code output stream.
|
|
6
|
+
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
|
|
9
|
+
const SEPARATOR = "─".repeat(60);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a requestConfirmation function for the code channel.
|
|
13
|
+
*
|
|
14
|
+
* @returns {(tool: string, args: object, description: string) => Promise<boolean>}
|
|
15
|
+
*/
|
|
16
|
+
export function createCodeConfirmAdapter() {
|
|
17
|
+
return async function requestConfirmation(_tool, _args, description) {
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stderr,
|
|
21
|
+
terminal: false,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
`\n${SEPARATOR}\n` +
|
|
27
|
+
`[apx] CONFIRM ACTION\n` +
|
|
28
|
+
` ${description}\n` +
|
|
29
|
+
` Answer [y = yes / N = no]: `
|
|
30
|
+
);
|
|
31
|
+
rl.once("line", (answer) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
const confirmed = /^(y|yes|ok)$/i.test(answer.trim());
|
|
34
|
+
process.stderr.write(confirmed ? "✓ Confirmed\n" : "✗ Cancelled\n");
|
|
35
|
+
process.stderr.write(`${SEPARATOR}\n`);
|
|
36
|
+
resolve(confirmed);
|
|
37
|
+
});
|
|
38
|
+
rl.once("close", () => resolve(false));
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Telegram confirmation adapter — async inline keyboard.
|
|
2
|
+
//
|
|
3
|
+
// Flow (why a Promise survives across two independent HTTP calls):
|
|
4
|
+
//
|
|
5
|
+
// 1. requestConfirmation() is called by the agent loop mid-tool-execution.
|
|
6
|
+
// It creates a pending entry in the shared store, embeds the correlationId
|
|
7
|
+
// in the button callback_data, sends the keyboard to Telegram, and returns
|
|
8
|
+
// a Promise that is NOT yet resolved.
|
|
9
|
+
//
|
|
10
|
+
// 2. The agent loop is now suspended at `await requestConfirmation(...)`.
|
|
11
|
+
// The Telegram bot's polling loop keeps running independently.
|
|
12
|
+
//
|
|
13
|
+
// 3. When the user taps a button, Telegram sends a callback_query update.
|
|
14
|
+
// The plugin's _handleUpdate() routes it to handleCallbackQuery() here.
|
|
15
|
+
//
|
|
16
|
+
// 4. handleCallbackQuery() calls pendingStore.resolve(correlationId, value),
|
|
17
|
+
// which finds the Promise's resolve function in the in-memory map and
|
|
18
|
+
// calls it. The agent loop resumes with true or false.
|
|
19
|
+
//
|
|
20
|
+
// Idempotency: once the promise resolves the entry is removed from the store.
|
|
21
|
+
// Any subsequent tap on the same button won't find an entry and is a no-op.
|
|
22
|
+
//
|
|
23
|
+
// Post-restart stale buttons: if the process restarted after sending the
|
|
24
|
+
// keyboard but before the user tapped, pendingStore.wasKnown() detects the
|
|
25
|
+
// SQLite row with no memory entry and we show "Expirado" instead of an error.
|
|
26
|
+
|
|
27
|
+
const API_BASE = "https://api.telegram.org";
|
|
28
|
+
const TIMEOUT_MS = 60_000; // 60 s — long enough for a human, short enough to not block forever
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ token: string, chatId: string|number, pendingStore: ConfirmationPendingStore }} opts
|
|
32
|
+
* @returns {{ requestConfirmation, handleCallbackQuery }}
|
|
33
|
+
*/
|
|
34
|
+
export function createTelegramConfirmAdapter({ token, chatId, pendingStore }) {
|
|
35
|
+
async function requestConfirmation(tool, _args, description) {
|
|
36
|
+
const { correlationId, promise } = pendingStore.create({ timeoutMs: TIMEOUT_MS });
|
|
37
|
+
|
|
38
|
+
await sendConfirmKeyboard(token, chatId, description, correlationId, TIMEOUT_MS);
|
|
39
|
+
|
|
40
|
+
return promise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Called by ChannelPoller._handleUpdate() when a callback_query arrives.
|
|
44
|
+
// Returns true if the callback matched our pattern (consumed it), false otherwise.
|
|
45
|
+
async function handleCallbackQuery(callbackQuery) {
|
|
46
|
+
const data = callbackQuery.data || "";
|
|
47
|
+
const match = data.match(/^apx:confirm:([a-f0-9]{16}):(yes|no)$/);
|
|
48
|
+
if (!match) return false;
|
|
49
|
+
|
|
50
|
+
const [, correlationId, answer] = match;
|
|
51
|
+
const confirmed = answer === "yes";
|
|
52
|
+
|
|
53
|
+
// ACK the callback immediately to clear the loading spinner on the button.
|
|
54
|
+
// Fire-and-forget — a slow ACK is annoying but not fatal.
|
|
55
|
+
await answerCallbackQuery(token, callbackQuery.id, confirmed ? "✅ Confirmed" : "❌ Cancelled");
|
|
56
|
+
|
|
57
|
+
const resolved = pendingStore.resolve(correlationId, confirmed);
|
|
58
|
+
|
|
59
|
+
// If not resolved, the entry timed out or the process restarted — show "Expired"
|
|
60
|
+
// so the user knows the button is no longer actionable.
|
|
61
|
+
const inlineKeyboard = resolved
|
|
62
|
+
? [[{ text: confirmed ? "✅ Confirmed" : "❌ Cancelled", callback_data: "apx:noop" }]]
|
|
63
|
+
: [[{ text: "⏱ Expired", callback_data: "apx:noop" }]];
|
|
64
|
+
|
|
65
|
+
if (callbackQuery.message?.chat?.id && callbackQuery.message?.message_id) {
|
|
66
|
+
await editMessageButtons(
|
|
67
|
+
token,
|
|
68
|
+
callbackQuery.message.chat.id,
|
|
69
|
+
callbackQuery.message.message_id,
|
|
70
|
+
inlineKeyboard
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { requestConfirmation, handleCallbackQuery };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------- Telegram API helpers --------------------------------------------
|
|
81
|
+
|
|
82
|
+
async function sendConfirmKeyboard(token, chatId, description, correlationId, timeoutMs) {
|
|
83
|
+
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
84
|
+
await fetch(`${API_BASE}/bot${token}/sendMessage`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "content-type": "application/json" },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
chat_id: chatId,
|
|
89
|
+
text:
|
|
90
|
+
`⚠️ *Confirm action*\n\n${escapeMarkdown(description)}\n\n` +
|
|
91
|
+
`_Expires in ${timeoutSec}s. No response → cancelled._`,
|
|
92
|
+
parse_mode: "Markdown",
|
|
93
|
+
reply_markup: {
|
|
94
|
+
inline_keyboard: [[
|
|
95
|
+
{ text: "✅ Yes", callback_data: `apx:confirm:${correlationId}:yes` },
|
|
96
|
+
{ text: "❌ No", callback_data: `apx:confirm:${correlationId}:no` },
|
|
97
|
+
]],
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function answerCallbackQuery(token, callbackQueryId, text) {
|
|
104
|
+
try {
|
|
105
|
+
await fetch(`${API_BASE}/bot${token}/answerCallbackQuery`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
body: JSON.stringify({ callback_query_id: callbackQueryId, text }),
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// best-effort — Telegram gives only 30s to answer; after that it's already cleared
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function editMessageButtons(token, chatId, messageId, inlineKeyboard) {
|
|
116
|
+
try {
|
|
117
|
+
await fetch(`${API_BASE}/bot${token}/editMessageReplyMarkup`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "content-type": "application/json" },
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
chat_id: chatId,
|
|
122
|
+
message_id: messageId,
|
|
123
|
+
reply_markup: { inline_keyboard: inlineKeyboard },
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
// best-effort
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Escape Markdown special chars so description text doesn't break Telegram markup.
|
|
132
|
+
function escapeMarkdown(text) {
|
|
133
|
+
return String(text || "").replace(/[_*[\]()~`>#+\-=|{}.!]/g, "\\$&");
|
|
134
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Terminal confirmation adapter — synchronous stdin readline.
|
|
2
|
+
//
|
|
3
|
+
// The agent loop blocks here while waiting for user input. This is fine on
|
|
4
|
+
// the terminal surface because the whole process is interactive and
|
|
5
|
+
// single-threaded from the user's perspective.
|
|
6
|
+
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a requestConfirmation function for the terminal channel.
|
|
11
|
+
* Uses stderr so stdout remains clean (useful when output is piped).
|
|
12
|
+
*
|
|
13
|
+
* @returns {(tool: string, args: object, description: string) => Promise<boolean>}
|
|
14
|
+
*/
|
|
15
|
+
export function createTerminalConfirmAdapter() {
|
|
16
|
+
return async function requestConfirmation(_tool, _args, description) {
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stderr,
|
|
20
|
+
terminal: false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`\n⚠ Confirmation required\n ${description}\n Continue? [y/N] `
|
|
26
|
+
);
|
|
27
|
+
rl.once("line", (answer) => {
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve(/^(y|yes|ok)$/i.test(answer.trim()));
|
|
30
|
+
});
|
|
31
|
+
// Covers Ctrl+C / EOF while waiting.
|
|
32
|
+
rl.once("close", () => resolve(false));
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Web / TUI confirmation adapter — async SSE + HTTP confirm endpoint.
|
|
2
|
+
//
|
|
3
|
+
// How the Promise resolves across two separate HTTP calls:
|
|
4
|
+
//
|
|
5
|
+
// 1. The agent loop (running inside an SSE request handler) calls
|
|
6
|
+
// requestConfirmation(). This emits a "confirmation_required" SSE event
|
|
7
|
+
// containing the correlationId and description, then suspends by awaiting
|
|
8
|
+
// the pending store's Promise.
|
|
9
|
+
//
|
|
10
|
+
// 2. The browser / TUI receives the SSE event and renders a confirm/cancel
|
|
11
|
+
// dialog. The dialog is keyed by correlationId.
|
|
12
|
+
//
|
|
13
|
+
// 3. The user responds → frontend POSTs to
|
|
14
|
+
// POST /super-agent/confirm/:correlationId { confirmed: boolean }
|
|
15
|
+
//
|
|
16
|
+
// 4. The API handler (api/confirm.js) calls pendingStore.resolve(correlationId,
|
|
17
|
+
// value). This finds the in-memory resolve callback and calls it, unblocking
|
|
18
|
+
// the agent loop. The SSE stream receives the next event and continues.
|
|
19
|
+
//
|
|
20
|
+
// `onEvent` is the SSE emitter for the current turn. It's injected at adapter
|
|
21
|
+
// creation time (each turn gets its own adapter instance) so the "please show
|
|
22
|
+
// a dialog" event reaches the right open SSE connection.
|
|
23
|
+
|
|
24
|
+
import { getConfirmationStore } from "../pending-store.js";
|
|
25
|
+
|
|
26
|
+
const TIMEOUT_MS = 120_000; // 2 min — humans on screens are slower than keyboard
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Factory — call once per SSE turn, passing the turn's `onEvent` emitter.
|
|
30
|
+
*
|
|
31
|
+
* @param {{ onEvent: (event: object) => Promise<void>|void }} opts
|
|
32
|
+
* @returns {(tool: string, args: object, description: string) => Promise<boolean>}
|
|
33
|
+
*/
|
|
34
|
+
export function createWebConfirmAdapter({ onEvent }) {
|
|
35
|
+
return async function requestConfirmation(tool, _args, description) {
|
|
36
|
+
const store = getConfirmationStore();
|
|
37
|
+
|
|
38
|
+
const { correlationId, promise } = store.create({ timeoutMs: TIMEOUT_MS });
|
|
39
|
+
|
|
40
|
+
// Push a structured event to the open SSE stream so the frontend knows
|
|
41
|
+
// to render a confirmation dialog. The correlationId is the shared key
|
|
42
|
+
// between this pending promise and the POST /confirm/:correlationId call.
|
|
43
|
+
await onEvent({
|
|
44
|
+
type: "confirmation_required",
|
|
45
|
+
correlationId,
|
|
46
|
+
tool,
|
|
47
|
+
description,
|
|
48
|
+
timeout_ms: TIMEOUT_MS,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return promise;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Human-in-the-loop confirmation system.
|
|
2
|
+
//
|
|
3
|
+
// Public surface:
|
|
4
|
+
// getConfirmationStore() shared SQLite-backed pending store
|
|
5
|
+
// buildConfirmDescription(t, a) human-readable action summary
|
|
6
|
+
// isConfirmationRequired(err) true when a tool threw requires_confirmation:
|
|
7
|
+
//
|
|
8
|
+
// Adapters (one per channel type) live under ./adapters/.
|
|
9
|
+
|
|
10
|
+
export { getConfirmationStore, ConfirmationPendingStore } from "./pending-store.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns true when `error` was thrown by createPermissionGuard() to signal
|
|
14
|
+
* that this tool call needs explicit user approval before proceeding.
|
|
15
|
+
*/
|
|
16
|
+
export function isConfirmationRequired(error) {
|
|
17
|
+
return (
|
|
18
|
+
error != null &&
|
|
19
|
+
typeof error.message === "string" &&
|
|
20
|
+
error.message.startsWith("requires_confirmation:")
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a short, human-readable description of the action being confirmed.
|
|
26
|
+
* Shown in all confirmation channels (terminal prompt, Telegram message, web dialog).
|
|
27
|
+
*/
|
|
28
|
+
export function buildConfirmDescription(tool, args) {
|
|
29
|
+
const text = (s, max = 150) => String(s || "").slice(0, max);
|
|
30
|
+
|
|
31
|
+
const builders = {
|
|
32
|
+
send_telegram: (a) => `Send Telegram message: "${text(a.text)}"`,
|
|
33
|
+
run_shell: (a) => `Run shell command: \`${text(a.command)}\``,
|
|
34
|
+
write_file: (a) => `Write file: ${a.path || a.file || "(no path)"}`,
|
|
35
|
+
edit_file: (a) => `Edit file: ${a.path || a.file || "(no path)"}`,
|
|
36
|
+
create_task: (a) => `Create task: "${a.title || a.name || "?"}"`,
|
|
37
|
+
add_project: (a) => `Add project: ${a.path || a.name || "?"}`,
|
|
38
|
+
set_identity: (a) => `Change agent identity to: "${a.name || "?"}"`,
|
|
39
|
+
call_runtime: (a) => `Call runtime: ${a.runtime || a.name || "?"}`,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const fn = builders[tool];
|
|
43
|
+
return fn ? fn(args) : `Run tool: \`${tool}\``;
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Pending confirmation store: in-memory map of unresolved confirmations.
|
|
2
|
+
//
|
|
3
|
+
// Each entry holds a Promise's resolve callback and a timeout timer.
|
|
4
|
+
// When the user responds (any channel), resolve() is called and the agent
|
|
5
|
+
// loop that was suspended at `await requestConfirmation(...)` resumes.
|
|
6
|
+
//
|
|
7
|
+
// No persistence needed: confirmations are ephemeral (30–120s window).
|
|
8
|
+
// If the daemon restarts, the agent runs that created them are also dead,
|
|
9
|
+
// so there is nothing to resume. Stale Telegram buttons simply won't find
|
|
10
|
+
// an entry and the handleCallbackQuery path shows "Expired" gracefully.
|
|
11
|
+
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
function generateId() {
|
|
15
|
+
return randomBytes(8).toString("hex"); // 16 hex chars, URL-safe
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ConfirmationPendingStore {
|
|
19
|
+
constructor() {
|
|
20
|
+
// correlationId -> { resolve: (boolean) => void, timer: NodeJS.Timeout }
|
|
21
|
+
this._pending = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register a new pending confirmation.
|
|
26
|
+
*
|
|
27
|
+
* Returns { correlationId, promise } where:
|
|
28
|
+
* - correlationId: embed in the reply (button callback_data, SSE event…)
|
|
29
|
+
* - promise: resolves to true (confirmed) or false (denied / timeout)
|
|
30
|
+
*
|
|
31
|
+
* After timeoutMs with no response the promise auto-resolves to false.
|
|
32
|
+
*/
|
|
33
|
+
create({ timeoutMs = 30_000 } = {}) {
|
|
34
|
+
const correlationId = generateId();
|
|
35
|
+
|
|
36
|
+
const promise = new Promise((resolve) => {
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
this._pending.delete(correlationId);
|
|
39
|
+
resolve(false);
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
this._pending.set(correlationId, { resolve, timer });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return { correlationId, promise };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a pending confirmation.
|
|
49
|
+
* Returns true if found and resolved, false if not found (timed out, already
|
|
50
|
+
* resolved, or stale button after a process restart).
|
|
51
|
+
*/
|
|
52
|
+
resolve(correlationId, value) {
|
|
53
|
+
const entry = this._pending.get(correlationId);
|
|
54
|
+
if (!entry) return false;
|
|
55
|
+
clearTimeout(entry.timer);
|
|
56
|
+
this._pending.delete(correlationId);
|
|
57
|
+
entry.resolve(value);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Singleton — one store per daemon process, shared by all adapters.
|
|
63
|
+
let _store = null;
|
|
64
|
+
|
|
65
|
+
export function getConfirmationStore() {
|
|
66
|
+
if (!_store) _store = new ConfirmationPendingStore();
|
|
67
|
+
return _store;
|
|
68
|
+
}
|
package/src/core/scaffold.js
CHANGED
|
@@ -325,24 +325,60 @@ const AGENTS_MD_TEMPLATE = `# AGENTS.md
|
|
|
325
325
|
<!-- Hard constraints: what agents must always / never do here. -->
|
|
326
326
|
`;
|
|
327
327
|
|
|
328
|
-
const APC_GITIGNORE = `# APC
|
|
329
|
-
#
|
|
330
|
-
|
|
331
|
-
agents
|
|
328
|
+
const APC_GITIGNORE = `# APC repository-safe context only.
|
|
329
|
+
# Runtime state belongs in ~/.apx/projects/<id>/, not in .apc/.
|
|
330
|
+
|
|
331
|
+
# Legacy per-agent runtime dirs (agent definitions are flat: agents/<slug>.md)
|
|
332
|
+
agents/*/
|
|
333
|
+
|
|
334
|
+
# Runtime sessions / conversations / messages
|
|
332
335
|
sessions/
|
|
333
336
|
conversations/
|
|
334
337
|
messages/
|
|
335
338
|
chats/
|
|
339
|
+
threads/
|
|
340
|
+
transcripts/
|
|
341
|
+
runs/
|
|
342
|
+
|
|
343
|
+
# Runtime memory, indexes, databases, and caches
|
|
344
|
+
memory.local.md
|
|
345
|
+
auto-memory.md
|
|
336
346
|
cache/
|
|
337
347
|
tmp/
|
|
338
348
|
private/
|
|
339
349
|
secrets/
|
|
350
|
+
*.db
|
|
351
|
+
*.db-*
|
|
352
|
+
*.sqlite
|
|
353
|
+
*.sqlite3
|
|
354
|
+
project.db
|
|
355
|
+
memory.db
|
|
356
|
+
memory-index.jsonl
|
|
357
|
+
memory-cursor.json
|
|
358
|
+
|
|
359
|
+
# Local config and secrets
|
|
360
|
+
.env
|
|
340
361
|
*.local.json
|
|
341
362
|
*.secret.json
|
|
342
363
|
*.env
|
|
343
364
|
*.env.*
|
|
344
|
-
|
|
365
|
+
*.key
|
|
366
|
+
*.pem
|
|
367
|
+
*.p12
|
|
368
|
+
*.crt
|
|
369
|
+
credentials.json
|
|
370
|
+
service-account*.json
|
|
371
|
+
token*.json
|
|
372
|
+
mcps.local.json
|
|
373
|
+
config.local.json
|
|
374
|
+
|
|
375
|
+
# Scratch planning state
|
|
376
|
+
plans/scratch/
|
|
377
|
+
plans/*.local.md
|
|
378
|
+
|
|
379
|
+
# Migration marker and OS noise
|
|
345
380
|
migrate.md
|
|
381
|
+
.DS_Store
|
|
346
382
|
`;
|
|
347
383
|
|
|
348
384
|
function nowIso() {
|
|
@@ -448,19 +484,8 @@ export function initApf(directory, { name } = {}) {
|
|
|
448
484
|
}
|
|
449
485
|
|
|
450
486
|
export function ensureAgentDir(root, slug) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const memory = path.join(dir, "memory.md");
|
|
454
|
-
if (!fs.existsSync(memory)) {
|
|
455
|
-
fs.writeFileSync(
|
|
456
|
-
memory,
|
|
457
|
-
`# Memory — ${slug}\n\n` +
|
|
458
|
-
`## Identity\n- \n\n` +
|
|
459
|
-
`## Long-term facts\n- \n\n` +
|
|
460
|
-
`## Recent context\n- \n`
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
return dir;
|
|
487
|
+
fs.mkdirSync(path.join(root, ".apc", "agents"), { recursive: true });
|
|
488
|
+
return path.join(root, ".apc", "agents");
|
|
464
489
|
}
|
|
465
490
|
|
|
466
491
|
// Write .apc/agents/<slug>.md — the canonical agent definition file.
|