@agentprojectcontext/apx 1.80.0 → 1.80.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 +1 -1
- package/src/core/agent/tools/handlers/remember-routine.js +79 -0
- package/src/core/agent/tools/names.js +3 -0
- package/src/core/agent/tools/registry.js +6 -0
- package/src/core/profiles/bundled/secretary/routines/day-close.json +1 -1
- package/src/core/profiles/bundled/secretary/routines/day-open.json +1 -1
- package/src/core/profiles/bundled/secretary/routines/watch.json +1 -1
- package/src/core/routines/runner.js +34 -0
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { appendRoutineMemory } from "#core/stores/routine-memory.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A routine writing to its OWN memory.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS TOOL EXISTS. Routine memory is a file, and until now the only way to
|
|
7
|
+
* write it was `write_file` — which is gated as dangerous, correctly, because it
|
|
8
|
+
* can write anywhere. In a routine there is nobody to confirm anything, so
|
|
9
|
+
* `requirePermission` threw "Action requires user confirmation", the model
|
|
10
|
+
* treated it as a dead end, and the evening anchor ended without sending
|
|
11
|
+
* anything. The run still reported `ok`, so the only symptom was silence.
|
|
12
|
+
*
|
|
13
|
+
* An agent recording what it learned is not a dangerous act. It is the most
|
|
14
|
+
* ordinary thing it does, and it must not need a human standing by.
|
|
15
|
+
*
|
|
16
|
+
* WHY IT IS SAFE UNGATED: there is no path argument. The destination comes from
|
|
17
|
+
* the running routine's own context (channelMeta.routineId + the project's
|
|
18
|
+
* storage), so this tool cannot write anywhere else however it is called. That
|
|
19
|
+
* is the whole difference between this and write_file, and it is the reason the
|
|
20
|
+
* permission is unnecessary rather than merely inconvenient.
|
|
21
|
+
*/
|
|
22
|
+
export default {
|
|
23
|
+
name: "remember_routine",
|
|
24
|
+
schema: {
|
|
25
|
+
type: "function",
|
|
26
|
+
function: {
|
|
27
|
+
name: "remember_routine",
|
|
28
|
+
description:
|
|
29
|
+
"Save a durable note to THIS routine's own memory — what you learned about how the " +
|
|
30
|
+
"owner works, what turned out to be worth mentioning, what turned out to be noise. " +
|
|
31
|
+
"Only available while a routine is running. Use this instead of write_file: it is the " +
|
|
32
|
+
"routine's own notebook and needs no permission. For facts about the owner that matter " +
|
|
33
|
+
"on every channel, use `remember` instead.",
|
|
34
|
+
parameters: {
|
|
35
|
+
type: "object",
|
|
36
|
+
required: ["note"],
|
|
37
|
+
properties: {
|
|
38
|
+
note: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "One line. A judgement that will change what you do next time, not a log of what happened.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
makeHandler: ({ channel, channelMeta, projects }) => async ({ note } = {}) => {
|
|
47
|
+
const text = String(note || "").trim();
|
|
48
|
+
if (!text) return { error: "note required" };
|
|
49
|
+
|
|
50
|
+
const routineId = channelMeta?.routineId;
|
|
51
|
+
const routineName = channelMeta?.routineName || "";
|
|
52
|
+
if (!routineId) {
|
|
53
|
+
// Said plainly so the model reaches for `remember` instead of retrying.
|
|
54
|
+
return {
|
|
55
|
+
error:
|
|
56
|
+
"remember_routine only works inside a running routine. " +
|
|
57
|
+
"Use `remember` for a durable fact about the owner.",
|
|
58
|
+
channel: channel || null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The routine's project storage, never a model-supplied path.
|
|
63
|
+
const projectPath = channelMeta?.projectPath || "";
|
|
64
|
+
let storagePath = "";
|
|
65
|
+
for (const entry of projects?.list?.() || []) {
|
|
66
|
+
if (projectPath && entry.path !== projectPath) continue;
|
|
67
|
+
storagePath = projects.get(entry.id)?.storagePath || "";
|
|
68
|
+
if (storagePath) break;
|
|
69
|
+
}
|
|
70
|
+
if (!storagePath) return { error: "could not resolve this routine's storage" };
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const r = appendRoutineMemory(storagePath, routineId, text, { routineName });
|
|
74
|
+
return { saved: true, routine: routineName || routineId, note: text, path: r?.path };
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return { error: e.message };
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -42,6 +42,7 @@ export const TOOLS = Object.freeze({
|
|
|
42
42
|
LIST_TASKS: "list_tasks",
|
|
43
43
|
CREATE_TASK: "create_task",
|
|
44
44
|
RECORD_COMMITMENT: "record_commitment",
|
|
45
|
+
REMEMBER_ROUTINE: "remember_routine",
|
|
45
46
|
LIST_COMMITMENTS: "list_commitments",
|
|
46
47
|
|
|
47
48
|
// Interaction
|
|
@@ -139,6 +140,7 @@ export const NATIVE_TOOL_NAMES = new Set([
|
|
|
139
140
|
TOOLS.CREATE_TASK,
|
|
140
141
|
TOOLS.RECORD_COMMITMENT,
|
|
141
142
|
TOOLS.LIST_COMMITMENTS,
|
|
143
|
+
TOOLS.REMEMBER_ROUTINE,
|
|
142
144
|
TOOLS.ASK_QUESTIONS,
|
|
143
145
|
TOOLS.SEARCH_SESSIONS,
|
|
144
146
|
TOOLS.TRANSCRIBE_AUDIO,
|
|
@@ -228,6 +230,7 @@ export const SIDE_EFFECT_TOOLS = new Set([
|
|
|
228
230
|
TOOLS.SEND_TELEGRAM,
|
|
229
231
|
TOOLS.CREATE_TASK,
|
|
230
232
|
TOOLS.RECORD_COMMITMENT,
|
|
233
|
+
TOOLS.REMEMBER_ROUTINE,
|
|
231
234
|
TOOLS.WRITE_FILE,
|
|
232
235
|
TOOLS.EDIT_FILE,
|
|
233
236
|
TOOLS.RUN_SHELL,
|
|
@@ -30,6 +30,7 @@ import askQuestions from "./handlers/ask-questions.js";
|
|
|
30
30
|
import createTask from "./handlers/create-task.js";
|
|
31
31
|
import recordCommitment from "./handlers/record-commitment.js";
|
|
32
32
|
import listCommitments from "./handlers/list-commitments.js";
|
|
33
|
+
import rememberRoutine from "./handlers/remember-routine.js";
|
|
33
34
|
import listTasks from "./handlers/list-tasks.js";
|
|
34
35
|
import discoverTools from "./handlers/discover-tools.js";
|
|
35
36
|
import gitStatus from "./handlers/git-status.js";
|
|
@@ -85,6 +86,7 @@ const NATIVE_TOOLS = [
|
|
|
85
86
|
listTasks,
|
|
86
87
|
recordCommitment,
|
|
87
88
|
listCommitments,
|
|
89
|
+
rememberRoutine,
|
|
88
90
|
discoverTools,
|
|
89
91
|
gitStatus,
|
|
90
92
|
gitDiff,
|
|
@@ -143,6 +145,10 @@ export const BASE_TOOL_NAMES = new Set([
|
|
|
143
145
|
TOOLS.READ_AGENT_MEMORY,
|
|
144
146
|
TOOLS.READ_SELF_MEMORY,
|
|
145
147
|
TOOLS.REMEMBER,
|
|
148
|
+
// NOT here: remember_routine. It only works inside a routine, and the routine
|
|
149
|
+
// channel is in FULL_CHANNELS — it already receives the whole registry. Adding
|
|
150
|
+
// it to the base set would spend tokens on every Telegram and desktop turn for
|
|
151
|
+
// a tool that returns an error on those channels.
|
|
146
152
|
TOOLS.SET_IDENTITY,
|
|
147
153
|
// Sessions + messages (self-recall + channel history).
|
|
148
154
|
TOOLS.SEARCH_SESSIONS,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"permission_mode": "permiso",
|
|
6
6
|
"enabled_by_default": true,
|
|
7
7
|
"spec": {
|
|
8
|
-
"prompt": "Close the day.
|
|
8
|
+
"prompt": "Close the day, across every registered project.\n\nHOW TO SPEND YOUR STEPS. You get a small, fixed number of tool calls, and the LAST one is taken away from you to write your closing prose — so a turn spent exploring ends with nothing delivered. Gather with list_tasks and list_commitments (once each, no project argument) and NOTHING ELSE. Do not search sessions, do not read files, do not run shell commands: none of that is what {{owner_name}} asked for. Then send, immediately. Report what you actually have; an honest short message beats a complete one that never arrives.\n\nSend ONE short message with three things: what moved today, what is still stuck, and what carries over. Keep promises to people separate from ordinary tasks — if a commitment came due today, ask whether it was kept rather than marking it yourself. Note anything that has gone quiet longer than the staleness threshold. If the day was quiet, say that plainly in a line.\n\nDELIVERY: send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. One message, then stop.\n\nAfter sending, record with remember_routine anything durable you learned about how this person works — one line. Never write_file for this: a scheduled run has nobody to approve it and the whole turn dies there.",
|
|
9
9
|
"anchor": true
|
|
10
10
|
},
|
|
11
11
|
"allowed_tools": [
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"permission_mode": "permiso",
|
|
6
6
|
"enabled_by_default": true,
|
|
7
7
|
"spec": {
|
|
8
|
-
"prompt": "Open the day. Look at what is due today, what is overdue, what is blocked, and what is promised, across every registered project.
|
|
8
|
+
"prompt": "Open the day. Look at what is due today, what is overdue, what is blocked, and what is promised, across every registered project. HOW TO SPEND YOUR STEPS. You get a small, fixed number of tool calls, and the LAST one is taken away from you to write your closing prose — so a turn spent exploring ends with nothing delivered. Gather with list_tasks and list_commitments (once each, no project argument) and NOTHING ELSE. Do not search sessions, do not read files, do not run shell commands: none of that is what {{owner_name}} asked for. Then send, immediately. Report what you actually have; an honest short message beats a complete one that never arrives. Lead with promises: a broken promise costs more than a late task, so it goes first and is named as a promise to a person, never folded into the task list.\n\nThen send ONE short message: promises coming due or already past, what is due today, anything on the calendar, and the single thing that most deserves attention. Not an inventory.\n\nIf there is genuinely nothing pressing, say so in one line — and then do the thing a chief of staff does with an empty morning: ask what should go on today's list, or name the one stale project worth picking up. An empty day is a prompt, not a silence.\n\nDELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. Send exactly one message, then stop; do not end the turn by asking whether to keep looking.",
|
|
9
9
|
"anchor": true
|
|
10
10
|
},
|
|
11
11
|
"allowed_tools": [
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"permission_mode": "permiso",
|
|
6
6
|
"enabled_by_default": true,
|
|
7
7
|
"spec": {
|
|
8
|
-
"prompt": "You are watching {{owner_name}}'s projects between the anchors. The signals below were detected deterministically — they are facts, already gathered; you do not need to look them up again.\n\nDecide whether any of it is worth interrupting for RIGHT NOW rather than waiting for the next anchor. Staying quiet is the normal outcome and costs you nothing. A message that could have waited until tomorrow morning costs you the next one, because that is the one they will not open.\n\nIf you do speak: send ONE line naming the single most important thing and what you suggest doing about it, with send_telegram. DELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. If you decide to stay quiet, just say so in your reply and call nothing."
|
|
8
|
+
"prompt": "You are watching {{owner_name}}'s projects between the anchors. The signals below were detected deterministically — they are facts, already gathered; you do not need to look them up again.\n\nDecide whether any of it is worth interrupting for RIGHT NOW rather than waiting for the next anchor. Staying quiet is the normal outcome and costs you nothing. A message that could have waited until tomorrow morning costs you the next one, because that is the one they will not open.\n\nIf you do speak: send ONE line naming the single most important thing and what you suggest doing about it, with send_telegram. DELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. If you decide to stay quiet, just say so in your reply and call nothing.\n\nWhichever you choose, note what you learned with remember_routine — especially when you stayed quiet, because next time you need to know this kind of signal was not worth it."
|
|
9
9
|
},
|
|
10
10
|
"allowed_tools": [
|
|
11
11
|
"send_telegram"
|
|
@@ -145,6 +145,12 @@ async function handleSuperAgent(ctx, routine, extraChannelMeta = {}) {
|
|
|
145
145
|
suppressTools: suppressTools.length > 0 ? suppressTools : null,
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
// A tool that needed a human in a run with no human is a DEAD END, not a
|
|
149
|
+
// hiccup: nobody is ever going to confirm it. Reporting the run as "ok"
|
|
150
|
+
// meant the only symptom was silence — the evening anchor produced nothing
|
|
151
|
+
// and it took twenty-one shell commands to work out why. Name it.
|
|
152
|
+
const blocked = blockedForPermission(result.trace);
|
|
153
|
+
|
|
148
154
|
project.logMessage({
|
|
149
155
|
channel: CHANNELS.ROUTINE,
|
|
150
156
|
direction: "out",
|
|
@@ -161,9 +167,37 @@ async function handleSuperAgent(ctx, routine, extraChannelMeta = {}) {
|
|
|
161
167
|
usage: result.usage,
|
|
162
168
|
},
|
|
163
169
|
});
|
|
170
|
+
if (blocked.length) {
|
|
171
|
+
return {
|
|
172
|
+
status: "error",
|
|
173
|
+
error:
|
|
174
|
+
`blocked waiting for a confirmation nobody can give: ${blocked.join(", ")}. ` +
|
|
175
|
+
`A scheduled run has no one to approve a dangerous tool — either allow it on ` +
|
|
176
|
+
`this routine (allowed_tools) or use a tool that does not need approval.`,
|
|
177
|
+
blocked_tools: blocked,
|
|
178
|
+
reply: result.text,
|
|
179
|
+
trace: result.trace,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
164
182
|
return { status: "ok", reply: result.text, trace: result.trace };
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Tools whose result was "Action requires user confirmation" — the message
|
|
187
|
+
* createPermissionGuard throws when there is no confirmation channel wired
|
|
188
|
+
* (tools/helpers.js). Distinct from a tool that merely failed.
|
|
189
|
+
*/
|
|
190
|
+
export function blockedForPermission(trace) {
|
|
191
|
+
const names = new Set();
|
|
192
|
+
for (const item of Array.isArray(trace) ? trace : []) {
|
|
193
|
+
const err = item?.result?.error;
|
|
194
|
+
if (typeof err === "string" && /requires user confirmation/i.test(err)) {
|
|
195
|
+
names.add(item.tool || "unknown");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return [...names];
|
|
199
|
+
}
|
|
200
|
+
|
|
167
201
|
async function handleTelegram(ctx, routine) {
|
|
168
202
|
const { plugins } = ctx;
|
|
169
203
|
const tg = plugins?.get("telegram");
|