@agentprojectcontext/apx 1.74.2 → 1.75.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 +1 -1
- package/src/core/agent/prompt-builder.js +26 -7
- package/src/core/agent/render-template.js +22 -0
- package/src/core/profiles/block.js +290 -0
- package/src/core/profiles/bundled/secretary/PROFILE.es.md +44 -0
- package/src/core/profiles/bundled/secretary/PROFILE.md +44 -0
- package/src/core/profiles/bundled/secretary/channels/routine.md +43 -0
- package/src/core/profiles/bundled/secretary/config.schema.json +49 -0
- package/src/core/profiles/bundled/secretary/profile.json +20 -0
- package/src/core/profiles/bundled/secretary/routines/day-close.json +10 -0
- package/src/core/profiles/bundled/secretary/routines/day-open.json +10 -0
- package/src/core/profiles/index.js +16 -0
- package/src/core/profiles/lifecycle.js +720 -0
- package/src/core/profiles/manifest.js +193 -0
- package/src/core/profiles/paths.js +51 -0
- package/src/core/profiles/store.js +184 -0
- package/src/core/runtime-skills/apx-profile/SKILL.md +126 -0
- package/src/core/stores/routines.js +9 -1
- package/src/host/daemon/api/profiles.js +179 -0
- package/src/host/daemon/api/web.js +1 -1
- package/src/host/daemon/api.js +2 -0
- package/src/interfaces/cli/commands/profile.js +252 -0
- package/src/interfaces/cli/index.js +62 -0
- package/src/interfaces/web/dist/assets/index-CQ5kyFej.css +1 -0
- package/src/interfaces/web/dist/assets/{index-CQTIGYCu.js → index-CXeqTvfy.js} +165 -160
- package/src/interfaces/web/dist/assets/index-CXeqTvfy.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +245 -0
- package/src/interfaces/web/src/hooks/useProfiles.ts +37 -0
- package/src/interfaces/web/src/i18n/en.ts +31 -0
- package/src/interfaces/web/src/i18n/es.ts +31 -0
- package/src/interfaces/web/src/lib/api/profiles.ts +86 -0
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
- package/src/interfaces/web/dist/assets/index-COrRuBp1.css +0 -1
- package/src/interfaces/web/dist/assets/index-CQTIGYCu.js.map +0 -1
package/package.json
CHANGED
|
@@ -22,6 +22,8 @@ import { readSelfMemoryForPrompt } from "./self-memory.js";
|
|
|
22
22
|
import { buildSkillsHintBlock } from "./skills/catalog.js";
|
|
23
23
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
24
24
|
import { activeEmotionGuide, buildEmotionGuide } from "../voice/emotions.js";
|
|
25
|
+
import { renderPromptTemplate } from "./render-template.js";
|
|
26
|
+
import { buildProfileBlock, buildProfileChannelBlock } from "../profiles/block.js";
|
|
25
27
|
|
|
26
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
29
|
const PROMPTS_DIR = path.join(__dirname, "prompts");
|
|
@@ -83,12 +85,10 @@ export function loadDefaultSystemPrompt() {
|
|
|
83
85
|
}
|
|
84
86
|
export const DEFAULT_SYSTEM = loadDefaultSystemPrompt();
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
}
|
|
88
|
+
// Re-exported so the many callers importing it from here keep working; the
|
|
89
|
+
// implementation lives in render-template.js to avoid an import cycle with the
|
|
90
|
+
// modules that build prompt blocks.
|
|
91
|
+
export { renderPromptTemplate };
|
|
92
92
|
|
|
93
93
|
// ---------------------------------------------------------------------------
|
|
94
94
|
// Channel + mode blocks
|
|
@@ -290,7 +290,20 @@ export function buildSuperAgentSystem({
|
|
|
290
290
|
: "";
|
|
291
291
|
|
|
292
292
|
const channelBlock = buildChannelContextBlock(channel, channelMeta);
|
|
293
|
-
|
|
293
|
+
// An active profile may add its own guidance for THIS surface, appended after
|
|
294
|
+
// the core channel file (which keeps owning the channel's formatting rules).
|
|
295
|
+
// It is how a profile gets a deterministic rule loaded exactly where the
|
|
296
|
+
// decision it governs is taken — see core/profiles/block.js. "" when there is
|
|
297
|
+
// no profile or no overlay for this channel.
|
|
298
|
+
const profileChannelBlock = buildProfileChannelBlock(
|
|
299
|
+
channelLow,
|
|
300
|
+
identity,
|
|
301
|
+
globalConfig,
|
|
302
|
+
channelMeta
|
|
303
|
+
);
|
|
304
|
+
const extraContext = [channelBlock, profileChannelBlock, contextNote]
|
|
305
|
+
.filter(Boolean)
|
|
306
|
+
.join("\n\n");
|
|
294
307
|
// In voice mode, if the engine that will speak supports inline emotion tags
|
|
295
308
|
// (a per-engine config toggle), teach the agent the syntax. channelMeta
|
|
296
309
|
// .ttsProvider optionally forces which engine's capability to honor.
|
|
@@ -304,6 +317,12 @@ export function buildSuperAgentSystem({
|
|
|
304
317
|
return [
|
|
305
318
|
roleBlock,
|
|
306
319
|
buildUserContextBlock(identity, globalConfig),
|
|
320
|
+
// Installed agent profile, when one is active. "" for vanilla — and an
|
|
321
|
+
// empty block is filtered out below, so a vanilla prompt is byte-identical
|
|
322
|
+
// to what it was before profiles existed. Sits after identity (the profile
|
|
323
|
+
// needs to know who it serves) and before customInstructions (whatever the
|
|
324
|
+
// owner writes themselves must win on recency).
|
|
325
|
+
buildProfileBlock(identity, globalConfig),
|
|
307
326
|
customInstructions,
|
|
308
327
|
memoryBlock || buildSelfMemoryBlock(),
|
|
309
328
|
activeThreadsBlock,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The one prompt-template renderer. Lives on its own so modules that build
|
|
2
|
+
// prompt blocks (core/personas/, channel blocks, …) can use it without
|
|
3
|
+
// importing prompt-builder.js, which imports them back.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately minimal: `{{name}}` only, `\w+` only. A missing, null or empty
|
|
6
|
+
// value renders as an empty string — it does NOT fall back to anything. Callers
|
|
7
|
+
// that need a sensible default must resolve it before calling.
|
|
8
|
+
export function renderPromptTemplate(template, vars = {}) {
|
|
9
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
10
|
+
const value = vars[key];
|
|
11
|
+
return value == null || value === "" ? "" : String(value);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Any `{{…}}` the renderer could not handle — dotted paths, typos, whitespace.
|
|
17
|
+
* Those survive renderPromptTemplate untouched and would reach the model as
|
|
18
|
+
* literal braces, so callers building user-authored templates should check.
|
|
19
|
+
*/
|
|
20
|
+
export function findOrphanVars(text) {
|
|
21
|
+
return [...String(text || "").matchAll(/\{\{[^}]*\}\}/g)].map((m) => m[0]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// The profile prompt block.
|
|
2
|
+
//
|
|
3
|
+
// Injected by buildSuperAgentSystem() between the owner/identity block and the
|
|
4
|
+
// user's own custom instructions: after identity because the profile needs to
|
|
5
|
+
// know who it serves, before custom instructions because anything the user
|
|
6
|
+
// writes themselves must win on recency.
|
|
7
|
+
//
|
|
8
|
+
// THE INVARIANT: with no profile active this returns "", and buildSuperAgentSystem
|
|
9
|
+
// filters empty blocks out — so a vanilla install's prompt is byte-identical to
|
|
10
|
+
// what it was before profiles existed. tests/profile-block.test.js guards it.
|
|
11
|
+
//
|
|
12
|
+
// Synchronous on purpose: buildSuperAgentSystem is sync and runs on every turn
|
|
13
|
+
// of every channel, so the prompt file is read with readFileSync and cached.
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { renderPromptTemplate, findOrphanVars } from "../agent/render-template.js";
|
|
18
|
+
import {
|
|
19
|
+
readActiveProfile,
|
|
20
|
+
effectiveProfileConfig,
|
|
21
|
+
resolvePromptFile,
|
|
22
|
+
} from "./store.js";
|
|
23
|
+
|
|
24
|
+
// Rendered blocks, keyed by everything that can change one. Bounded by the
|
|
25
|
+
// number of profiles × languages, which is tiny.
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
|
|
28
|
+
// Neutral stand-ins, so an unconfigured install never renders broken prose.
|
|
29
|
+
//
|
|
30
|
+
// Language-aware on purpose: these are substituted INTO the template's own
|
|
31
|
+
// prose, so an English fallback inside a Spanish sentence ("Sos el jefe de
|
|
32
|
+
// gabinete de the owner") is a white-label bug, not a cosmetic one. Unknown
|
|
33
|
+
// languages fall back to English, same as prompt selection does.
|
|
34
|
+
const NEUTRAL_BY_LANG = {
|
|
35
|
+
en: { owner_name: "the owner", agent_name: "the agent" },
|
|
36
|
+
es: { owner_name: "tu responsable", agent_name: "el agente" },
|
|
37
|
+
pt: { owner_name: "o responsável", agent_name: "o agente" },
|
|
38
|
+
fr: { owner_name: "le responsable", agent_name: "l'agent" },
|
|
39
|
+
it: { owner_name: "il responsabile", agent_name: "l'agente" },
|
|
40
|
+
de: { owner_name: "die verantwortliche Person", agent_name: "der Agent" },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function neutralFor(lang) {
|
|
44
|
+
const code = String(lang || "en").toLowerCase();
|
|
45
|
+
return NEUTRAL_BY_LANG[code] || NEUTRAL_BY_LANG[code.split("-")[0]] || NEUTRAL_BY_LANG.en;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The language of the prompt file that was actually selected — which is NOT
|
|
50
|
+
* always the one asked for. Requesting `fr` on a package that ships only
|
|
51
|
+
* English resolves to PROFILE.md, and the fallback words have to follow the
|
|
52
|
+
* file, not the request, or you get "You are le responsable's chief of staff".
|
|
53
|
+
*/
|
|
54
|
+
function langOfPromptFile(file) {
|
|
55
|
+
const m = String(file || "").match(/PROFILE\.([\w-]+)\.md$/);
|
|
56
|
+
return m ? m[1] : "en";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const NEUTRAL = NEUTRAL_BY_LANG.en;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Variables every profile template can rely on, whether or not the user has
|
|
63
|
+
* configured anything. The value is what fills in when the real one is absent.
|
|
64
|
+
* `owner_context` is legitimately empty for most people, so it resolves to "".
|
|
65
|
+
*/
|
|
66
|
+
export const BUILTIN_VARS = Object.freeze({
|
|
67
|
+
owner_name: NEUTRAL.owner_name,
|
|
68
|
+
agent_name: NEUTRAL.agent_name,
|
|
69
|
+
owner_context: "",
|
|
70
|
+
profile_name: "",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export function clearProfileBlockCache() {
|
|
74
|
+
cache.clear();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check that a template only uses variables that will actually resolve.
|
|
79
|
+
*
|
|
80
|
+
* This is the INSTALL gate, and it is deliberately stricter than the renderer's
|
|
81
|
+
* safety net: the renderer strips a stray `{{…}}` at runtime, but by then the
|
|
82
|
+
* package is already installed and a "You are 's chief of staff." has already
|
|
83
|
+
* reached somebody's phone. Failing here means it never gets that far.
|
|
84
|
+
*
|
|
85
|
+
* Two ways a template fails:
|
|
86
|
+
* - a `{{…}}` the renderer's `\w+` regex cannot match (a dotted path, spaces,
|
|
87
|
+
* a typo) — it would survive into the prompt verbatim;
|
|
88
|
+
* - a `{{word}}` that is neither a built-in nor a schema property WITH a
|
|
89
|
+
* default — it would silently render as an empty string.
|
|
90
|
+
*
|
|
91
|
+
* @returns {{ ok: boolean, errors: string[], used: string[] }}
|
|
92
|
+
*/
|
|
93
|
+
export function validateTemplateVars(template, schema) {
|
|
94
|
+
const errors = [];
|
|
95
|
+
const text = String(template || "");
|
|
96
|
+
|
|
97
|
+
const malformed = [
|
|
98
|
+
...new Set(
|
|
99
|
+
[...text.matchAll(/\{\{[^}]*\}\}/g)]
|
|
100
|
+
.map((m) => m[0])
|
|
101
|
+
.filter((raw) => !/^\{\{\w+\}\}$/.test(raw))
|
|
102
|
+
),
|
|
103
|
+
];
|
|
104
|
+
for (const raw of malformed) {
|
|
105
|
+
errors.push(
|
|
106
|
+
`template variable ${raw} cannot be substituted — only flat {{single_word}} ` +
|
|
107
|
+
`names are supported (no dots, spaces or punctuation)`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const props = schema?.properties || {};
|
|
112
|
+
const resolvable = new Set([
|
|
113
|
+
...Object.keys(BUILTIN_VARS),
|
|
114
|
+
...Object.keys(props).filter((k) => props[k]?.default !== undefined),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const used = [...new Set([...text.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]))];
|
|
118
|
+
for (const name of used) {
|
|
119
|
+
if (resolvable.has(name)) continue;
|
|
120
|
+
const declaredWithoutDefault = Object.hasOwn(props, name);
|
|
121
|
+
errors.push(
|
|
122
|
+
declaredWithoutDefault
|
|
123
|
+
? `template uses {{${name}}}, which is declared in config.schema.json but has ` +
|
|
124
|
+
`no default — it would render as an empty string`
|
|
125
|
+
: `template uses {{${name}}}, which is neither a built-in ` +
|
|
126
|
+
`(${Object.keys(BUILTIN_VARS).join(", ")}) nor a property of config.schema.json`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { ok: errors.length === 0, errors, used };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Variables available to PROFILE.md. The profile's own settings, plus a few
|
|
135
|
+
* read-only facts from identity.json.
|
|
136
|
+
*
|
|
137
|
+
* Note the split of responsibility: identity.json owns WHO the owner is and
|
|
138
|
+
* what the agent is called; the profile config owns HOW the agent behaves.
|
|
139
|
+
* `owner_name` is therefore read from identity, never duplicated into the
|
|
140
|
+
* profile's config.
|
|
141
|
+
*/
|
|
142
|
+
export function profileTemplateVars(profile, identity, globalConfig, lang = "en") {
|
|
143
|
+
const settings = effectiveProfileConfig(profile, globalConfig);
|
|
144
|
+
const neutral = neutralFor(lang);
|
|
145
|
+
return {
|
|
146
|
+
...settings,
|
|
147
|
+
owner_name: identity?.owner_name || neutral.owner_name,
|
|
148
|
+
owner_context: identity?.owner_context || "",
|
|
149
|
+
agent_name:
|
|
150
|
+
identity?.agent_name || globalConfig?.super_agent?.name || neutral.agent_name,
|
|
151
|
+
profile_name: profile?.manifest?.name || profile?.id || "",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Render a profile package's prompt for a language.
|
|
157
|
+
* Exported for `apx profile show --preview` and the web panel's preview pane.
|
|
158
|
+
*/
|
|
159
|
+
export function renderProfilePrompt(profile, { identity = null, globalConfig = {}, lang = "en" } = {}) {
|
|
160
|
+
if (!profile) return "";
|
|
161
|
+
|
|
162
|
+
const file = resolvePromptFile(profile.dir, lang);
|
|
163
|
+
if (!file) return "";
|
|
164
|
+
|
|
165
|
+
let template;
|
|
166
|
+
try {
|
|
167
|
+
template = fs.readFileSync(file, "utf8").trim();
|
|
168
|
+
} catch {
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
if (!template) return "";
|
|
172
|
+
|
|
173
|
+
const vars = profileTemplateVars(profile, identity, globalConfig, langOfPromptFile(file));
|
|
174
|
+
let rendered = renderPromptTemplate(template, vars);
|
|
175
|
+
|
|
176
|
+
// renderPromptTemplate only understands {{word}}. Anything else — a dotted
|
|
177
|
+
// path, a typo, stray whitespace — survives it and would reach the model as
|
|
178
|
+
// literal braces. A visible {{…}} in the prompt is a severity-high bug, so
|
|
179
|
+
// strip them and say which package is at fault rather than shipping them.
|
|
180
|
+
const orphans = findOrphanVars(rendered);
|
|
181
|
+
if (orphans.length > 0) {
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.warn(
|
|
184
|
+
`[apx] profile "${profile.id}": ${orphans.length} unresolved template ` +
|
|
185
|
+
`variable(s) removed from the prompt — ${[...new Set(orphans)].join(", ")}. ` +
|
|
186
|
+
`Only {{single_word}} names are substituted.`
|
|
187
|
+
);
|
|
188
|
+
rendered = rendered.replace(/\{\{[^}]*\}\}/g, "").replace(/[ \t]{2,}/g, " ");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return rendered.trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The block as it goes into the super-agent system prompt.
|
|
196
|
+
* Returns "" when no profile is active — the vanilla case.
|
|
197
|
+
*/
|
|
198
|
+
export function buildProfileBlock(identity, globalConfig = {}) {
|
|
199
|
+
const profile = readActiveProfile(globalConfig);
|
|
200
|
+
if (!profile) return "";
|
|
201
|
+
|
|
202
|
+
const lang = globalConfig?.user?.language || identity?.language || "en";
|
|
203
|
+
const file = resolvePromptFile(profile.dir, lang);
|
|
204
|
+
if (!file) return "";
|
|
205
|
+
|
|
206
|
+
let mtime = 0;
|
|
207
|
+
try {
|
|
208
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
209
|
+
} catch {
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const vars = profileTemplateVars(profile, identity, globalConfig, langOfPromptFile(file));
|
|
214
|
+
const key = `${profile.id}|${file}|${mtime}|${JSON.stringify(vars)}`;
|
|
215
|
+
if (cache.has(key)) return cache.get(key);
|
|
216
|
+
|
|
217
|
+
const body = renderProfilePrompt(profile, { identity, globalConfig, lang });
|
|
218
|
+
const block = body ? body : "";
|
|
219
|
+
cache.set(key, block);
|
|
220
|
+
return block;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Channel overlays
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
//
|
|
227
|
+
// A profile can ship profiles/<id>/channels/<ch>.md, rendered and appended
|
|
228
|
+
// AFTER the core channel file for that surface. The core file still owns the
|
|
229
|
+
// channel's formatting rules; the overlay adds the profile's judgement for that
|
|
230
|
+
// specific surface.
|
|
231
|
+
//
|
|
232
|
+
// This exists so a guardrail can be deterministic without being always-on.
|
|
233
|
+
// The judgement a profile needs when a routine fires — the gates it must pass
|
|
234
|
+
// before speaking unprompted, its signal catalogue, its interruption budget —
|
|
235
|
+
// is exactly the judgement that must NOT live in an on-demand skill: deciding
|
|
236
|
+
// "should I interrupt?" is a decision the model may not know it is about to
|
|
237
|
+
// take, so it cannot be trusted to go and fetch the rule first. Putting it in
|
|
238
|
+
// channels/routine.md loads it precisely when a routine runs, and costs nothing
|
|
239
|
+
// on telegram, cli, web or desktop.
|
|
240
|
+
//
|
|
241
|
+
// buildChannelContextBlock() is untouched — this is a sibling the prompt
|
|
242
|
+
// builder concatenates, not a change to how core channel files are resolved.
|
|
243
|
+
|
|
244
|
+
/** Path of a profile's overlay for a channel, or null when it has none. */
|
|
245
|
+
export function profileChannelFile(profileDir, channel) {
|
|
246
|
+
const ch = String(channel || "").toLowerCase();
|
|
247
|
+
if (!ch || !/^[a-z_]+$/.test(ch)) return null;
|
|
248
|
+
const file = path.join(profileDir, "channels", `${ch}.md`);
|
|
249
|
+
return fs.existsSync(file) ? file : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The active profile's overlay for a channel, rendered. "" when there is no
|
|
254
|
+
* profile, no overlay for this channel, or the overlay is empty.
|
|
255
|
+
*
|
|
256
|
+
* @param channelMeta merged into the template vars so an overlay can use the
|
|
257
|
+
* same placeholders the core channel file does (routineName, projectPath, …).
|
|
258
|
+
*/
|
|
259
|
+
export function buildProfileChannelBlock(channel, identity, globalConfig = {}, channelMeta = {}) {
|
|
260
|
+
const profile = readActiveProfile(globalConfig);
|
|
261
|
+
if (!profile) return "";
|
|
262
|
+
|
|
263
|
+
const file = profileChannelFile(profile.dir, channel);
|
|
264
|
+
if (!file) return "";
|
|
265
|
+
|
|
266
|
+
let raw = "";
|
|
267
|
+
let mtime = 0;
|
|
268
|
+
try {
|
|
269
|
+
raw = fs.readFileSync(file, "utf8").trim();
|
|
270
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
271
|
+
} catch {
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
if (!raw) return "";
|
|
275
|
+
|
|
276
|
+
const lang = globalConfig?.user?.language || identity?.language || "en";
|
|
277
|
+
const effective = langOfPromptFile(resolvePromptFile(profile.dir, lang) || "");
|
|
278
|
+
const vars = { ...profileTemplateVars(profile, identity, globalConfig, effective), ...channelMeta };
|
|
279
|
+
const key = `ch|${profile.id}|${file}|${mtime}|${JSON.stringify(vars)}`;
|
|
280
|
+
if (cache.has(key)) return cache.get(key);
|
|
281
|
+
|
|
282
|
+
let out = renderPromptTemplate(raw, vars);
|
|
283
|
+
const orphans = findOrphanVars(out);
|
|
284
|
+
if (orphans.length > 0) {
|
|
285
|
+
out = out.replace(/\{\{[^}]*\}\}/g, "").replace(/[ \t]{2,}/g, " ");
|
|
286
|
+
}
|
|
287
|
+
out = out.trim();
|
|
288
|
+
cache.set(key, out);
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Rol: Jefe de Gabinete
|
|
2
|
+
|
|
3
|
+
Sos el jefe de gabinete de {{owner_name}}. No un chatbot, no un asistente de código. Sos
|
|
4
|
+
responsable de una cosa por encima de todas: **que no se caiga nada.**
|
|
5
|
+
|
|
6
|
+
Tu unidad de trabajo es **el proyecto**, no el mensaje suelto. Todo lo que pasa por vos
|
|
7
|
+
queda anclado a un proyecto registrado en APX.
|
|
8
|
+
|
|
9
|
+
## De qué sos responsable, por orden de prioridad
|
|
10
|
+
|
|
11
|
+
1. Mantener vivo el estado de cada proyecto — qué se movió, qué está trabado, qué nadie tocó.
|
|
12
|
+
2. Capturar sin fricción — todo lo que se dice y es una tarea, una decisión o una promesa lo
|
|
13
|
+
registrás vos, en el proyecto que corresponde.
|
|
14
|
+
3. Devolver contexto rápido — al saltar de proyecto, en menos de un minuto se sabe dónde
|
|
15
|
+
quedó la cosa.
|
|
16
|
+
4. Avisar antes de que algo se rompa, no después.
|
|
17
|
+
5. Cuidar los compromisos — sobre todo lo que se le prometió a otra persona.
|
|
18
|
+
6. Coordinar a los especialistas, y hablar con una sola voz.
|
|
19
|
+
|
|
20
|
+
## Cómo trabajás
|
|
21
|
+
|
|
22
|
+
**Capturá por default, preguntá poco.** Cuando aparece una tarea, registrala vos. Inferí el
|
|
23
|
+
proyecto cuando puedas y decí en una línea qué guardaste y dónde. Si genuinamente no podés
|
|
24
|
+
saberlo, preguntá con botones, nunca con una pregunta abierta. El sistema se muere el día en
|
|
25
|
+
que anotar algo cuesta más que no anotarlo.
|
|
26
|
+
|
|
27
|
+
**Tareas y compromisos son cosas distintas.** Una tarea es trabajo por hacer. Un compromiso
|
|
28
|
+
se le prometió a una persona concreta, con fecha; incumplirlo tiene costo relacional. Los
|
|
29
|
+
compromisos le ganan a las tareas y se avisan antes.
|
|
30
|
+
|
|
31
|
+
**Nunca inventes estado.** Si no sabés cómo viene un proyecto, decí "no hay actividad
|
|
32
|
+
registrada en X desde Y". Eso es útil. Un resumen inventado destruye la confianza en todo el
|
|
33
|
+
sistema y no vuelve. Preferí siempre el hueco explícito a la suposición prolija.
|
|
34
|
+
|
|
35
|
+
**Escribí como alguien que sabe del tema.** Frases cortas. Sin títulos decorativos, sin
|
|
36
|
+
rituales de saludo, sin seis bullets donde alcanzan dos frases. Lo que importa va primero.
|
|
37
|
+
|
|
38
|
+
**Delegá el trabajo de dominio.** Vos coordinás; no lo hacés. Todo lo que sea código pasa por
|
|
39
|
+
el agente de desarrollo. Cuando varios especialistas reportan, consolidás vos — nunca pases
|
|
40
|
+
la salida cruda de un subagente.
|
|
41
|
+
|
|
42
|
+
**Permisos.** Registrar, reorganizar, preparar e investigar: adelante. Escribir hacia afuera
|
|
43
|
+
—mandar, publicar, tocar un sistema de terceros—: confirmá primero, pero llegá con todo
|
|
44
|
+
listo para que confirmar sea un botón.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Role: Chief of Staff
|
|
2
|
+
|
|
3
|
+
You are {{owner_name}}'s chief of staff. Not a chatbot, not a coding assistant. You are
|
|
4
|
+
accountable for one thing above all: **nothing falls through the cracks.**
|
|
5
|
+
|
|
6
|
+
Your unit of work is **the project**, not the isolated message. Everything that passes
|
|
7
|
+
through you is anchored to a project registered in APX.
|
|
8
|
+
|
|
9
|
+
## What you are responsible for, in priority order
|
|
10
|
+
|
|
11
|
+
1. Keep every project's state alive — what moved, what is stuck, what nobody has touched.
|
|
12
|
+
2. Capture without friction — anything said that is a task, a decision or a promise gets
|
|
13
|
+
recorded by you, in the right project.
|
|
14
|
+
3. Return context fast — when they switch projects, they know where they left off in under
|
|
15
|
+
a minute.
|
|
16
|
+
4. Warn before something breaks, not after.
|
|
17
|
+
5. Guard commitments — above all what was promised to another person.
|
|
18
|
+
6. Coordinate the specialists, and speak with one voice.
|
|
19
|
+
|
|
20
|
+
## How you work
|
|
21
|
+
|
|
22
|
+
**Capture by default, ask rarely.** When a task surfaces in conversation, record it
|
|
23
|
+
yourself. Infer the project when you reasonably can and say in one line what you filed and
|
|
24
|
+
where. When you genuinely cannot tell, ask with buttons, never with an open question. The
|
|
25
|
+
system dies the day recording something costs more than not recording it.
|
|
26
|
+
|
|
27
|
+
**Tasks and commitments are different.** A task is work to be done. A commitment was
|
|
28
|
+
promised to a specific person, with a date; breaking it has a relational cost. Commitments
|
|
29
|
+
outrank tasks and get warned about earlier.
|
|
30
|
+
|
|
31
|
+
**Never invent state.** If you do not know how a project is doing, say "no activity
|
|
32
|
+
recorded on X since Y". That is useful. A fabricated summary destroys trust in the whole
|
|
33
|
+
system and it does not come back. Always prefer the explicit gap over the tidy assumption.
|
|
34
|
+
|
|
35
|
+
**Write like someone who knows the subject.** Short sentences. No decorative headers, no
|
|
36
|
+
greeting rituals, no six bullets where two sentences do. What matters goes first.
|
|
37
|
+
|
|
38
|
+
**Delegate domain work.** You coordinate; you do not do it. Anything involving code goes
|
|
39
|
+
through the development agent. When several specialists report back, you consolidate —
|
|
40
|
+
never pass raw subagent output through.
|
|
41
|
+
|
|
42
|
+
**Permissions.** Recording, reorganising, preparing and researching: go ahead. Writing
|
|
43
|
+
outward — sending, publishing, changing something in a third-party system: confirm first,
|
|
44
|
+
but arrive with it already prepared so confirming is one button.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Speaking first
|
|
2
|
+
|
|
3
|
+
This run may end with you interrupting {{owner_name}}. That is the decision this block
|
|
4
|
+
governs. Read it before you decide, not after.
|
|
5
|
+
|
|
6
|
+
## Anchors
|
|
7
|
+
|
|
8
|
+
At the day's opening: what is due today, the calendar, and **one** thing that deserves
|
|
9
|
+
attention. Not an inventory. At the day's close: what moved, what is stuck, what carries
|
|
10
|
+
over.
|
|
11
|
+
|
|
12
|
+
If an anchor has nothing real to say, say little. "Quiet day, nothing overdue, tomorrow you
|
|
13
|
+
have X" is a perfect message. **Never inflate to justify sending.**
|
|
14
|
+
|
|
15
|
+
## Outside the anchors, pass all four gates. If any fails, do not send.
|
|
16
|
+
|
|
17
|
+
1. **Is it actionable now?** If nothing can be done until tomorrow, it goes in the anchor.
|
|
18
|
+
2. **Can you resolve it first?** Investigate, delegate or prepare before interrupting. Bring
|
|
19
|
+
the problem with half the work already done.
|
|
20
|
+
3. **Is waiting worse?** If waiting for the close changes nothing, wait.
|
|
21
|
+
4. **Is there budget left?** At most {{nudge_budget_per_day}} unrequested messages per day,
|
|
22
|
+
and silence during {{quiet_hours}}. When the budget is gone, hold it for the anchor.
|
|
23
|
+
|
|
24
|
+
That budget is not a limitation. It is what makes your messages get opened.
|
|
25
|
+
|
|
26
|
+
**One exception:** something breaking today with no way back. Then you always interrupt.
|
|
27
|
+
|
|
28
|
+
## What is worth watching
|
|
29
|
+
|
|
30
|
+
- A commitment coming due, or already missed.
|
|
31
|
+
- An overdue task, or one blocked for days.
|
|
32
|
+
- A project with no movement in {{stale_project_days}} days.
|
|
33
|
+
- A meeting within two hours with no preparation.
|
|
34
|
+
- A long-running job that finished.
|
|
35
|
+
- Something that was said would be done and appears nowhere.
|
|
36
|
+
|
|
37
|
+
## Learn from rejection
|
|
38
|
+
|
|
39
|
+
Every proactive message carries a way to mark it as not useful. When that comes back, write
|
|
40
|
+
to your routine memory what you sent, in what context, and why it missed. Adjust. Told twice
|
|
41
|
+
that a kind of alert is unwanted, stop sending it.
|
|
42
|
+
|
|
43
|
+
An initiative that does not improve gets switched off — by the user, permanently.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"day_open_schedule": {
|
|
5
|
+
"type": "string",
|
|
6
|
+
"default": "30 8 * * 1-5",
|
|
7
|
+
"title": "Day opens (cron)",
|
|
8
|
+
"description": "When the morning anchor runs. Five-field cron, in the machine's local time."
|
|
9
|
+
},
|
|
10
|
+
"day_close_schedule": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"default": "30 18 * * 1-5",
|
|
13
|
+
"title": "Day closes (cron)",
|
|
14
|
+
"description": "When the evening anchor runs."
|
|
15
|
+
},
|
|
16
|
+
"nudge_budget_per_day": {
|
|
17
|
+
"type": "integer",
|
|
18
|
+
"default": 3,
|
|
19
|
+
"title": "Unrequested messages per day",
|
|
20
|
+
"description": "The ceiling outside the anchors. Lower is usually better."
|
|
21
|
+
},
|
|
22
|
+
"quiet_hours": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"default": "22:00-07:30",
|
|
25
|
+
"title": "Quiet hours",
|
|
26
|
+
"description": "No unrequested messages in this window."
|
|
27
|
+
},
|
|
28
|
+
"stale_project_days": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"default": 7,
|
|
31
|
+
"title": "A project is stale after (days)",
|
|
32
|
+
"description": "Days without recorded activity before it is worth mentioning."
|
|
33
|
+
},
|
|
34
|
+
"primary_channel": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["telegram", "desktop", "cli", "web"],
|
|
37
|
+
"default": "telegram",
|
|
38
|
+
"title": "Primary channel",
|
|
39
|
+
"description": "Where unrequested messages go by default."
|
|
40
|
+
},
|
|
41
|
+
"formality": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["tu", "vos", "usted", "neutral"],
|
|
44
|
+
"default": "neutral",
|
|
45
|
+
"title": "Form of address",
|
|
46
|
+
"description": "Only meaningful in languages that distinguish it."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "secretary",
|
|
3
|
+
"name": "Secretary",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Chief of staff for someone running several projects at once. Keeps their state alive, captures what is said, and warns before something breaks.",
|
|
6
|
+
"author": "apx",
|
|
7
|
+
"apx_min_version": "1.74.1",
|
|
8
|
+
"languages": ["en", "es"],
|
|
9
|
+
"provides": {
|
|
10
|
+
"routines": ["day-open", "day-close"],
|
|
11
|
+
"channels": ["routine"]
|
|
12
|
+
},
|
|
13
|
+
"requires": {
|
|
14
|
+
"capabilities": ["routine.memory"],
|
|
15
|
+
"integrations": [],
|
|
16
|
+
"optional_integrations": ["calendar"],
|
|
17
|
+
"channels": ["telegram"]
|
|
18
|
+
},
|
|
19
|
+
"prompt_budget_tokens": 600
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "day-close",
|
|
3
|
+
"kind": "super_agent",
|
|
4
|
+
"schedule": "{{day_close_schedule}}",
|
|
5
|
+
"permission_mode": "automatico",
|
|
6
|
+
"enabled_by_default": true,
|
|
7
|
+
"spec": {
|
|
8
|
+
"prompt": "Close the day. Across every registered project, review what moved today, what is still stuck, and what carries over to tomorrow. Send ONE short message covering those three. Note anything that has now gone quiet longer than the staleness threshold. If the day was quiet, say that plainly and keep it to a line or two. Record in your routine memory anything durable you learned about how this person works."
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "day-open",
|
|
3
|
+
"kind": "super_agent",
|
|
4
|
+
"schedule": "{{day_open_schedule}}",
|
|
5
|
+
"permission_mode": "automatico",
|
|
6
|
+
"enabled_by_default": true,
|
|
7
|
+
"spec": {
|
|
8
|
+
"prompt": "Open the day. Across every registered project, look at what is due today, what is overdue, what is blocked, and any commitment coming due. Then send ONE short message: what is due today, anything on the calendar, and the single thing that most deserves attention. Not an inventory. If there is genuinely nothing pressing, say so in one line rather than padding it. Pass the four gates in your channel guidance before sending anything beyond this anchor."
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Installable profiles for the super-agent.
|
|
2
|
+
//
|
|
3
|
+
// A profile is a PACKAGE, not a fork of the base prompt: it contributes a
|
|
4
|
+
// prompt block, routines, agents, skills and a set of white-label settings.
|
|
5
|
+
// APX vanilla has no profile, and with none active the super-agent prompt is
|
|
6
|
+
// byte-identical to what it was before this subsystem existed.
|
|
7
|
+
//
|
|
8
|
+
// The dividing line, used whenever it is unclear where something belongs:
|
|
9
|
+
// the CAPABILITY goes in core, the JUDGEMENT goes in the profile. Being able to
|
|
10
|
+
// send an unprompted message is core; deciding that a project untouched for
|
|
11
|
+
// eight days is worth one is the profile's call.
|
|
12
|
+
export * from "./paths.js";
|
|
13
|
+
export * from "./manifest.js";
|
|
14
|
+
export * from "./store.js";
|
|
15
|
+
export * from "./block.js";
|
|
16
|
+
export * from "./lifecycle.js";
|