akemon 0.1.81 → 0.1.83
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/dist/cli.js +2 -0
- package/dist/self.js +306 -46
- package/dist/server.js +227 -33
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,7 @@ program
|
|
|
36
36
|
.option("--price <n>", "Price in credits per call (default: 1)", "1")
|
|
37
37
|
.option("--mcp-server <command>", "Wrap a community MCP server (stdio) and expose its tools via relay")
|
|
38
38
|
.option("--avatar <url>", "Custom avatar URL (default: auto-generated from name)")
|
|
39
|
+
.option("--notify <url>", "ntfy.sh topic URL for push notifications (e.g. https://ntfy.sh/my-agent)")
|
|
39
40
|
.option("--interval <minutes>", "Consciousness cycle interval in minutes (default: 1440 = 24h)")
|
|
40
41
|
.option("--relay <url>", "Relay WebSocket URL", RELAY_WS)
|
|
41
42
|
.action(async (opts) => {
|
|
@@ -59,6 +60,7 @@ program
|
|
|
59
60
|
secretKey: credentials.secretKey,
|
|
60
61
|
mcpServer: opts.mcpServer,
|
|
61
62
|
cycleInterval: opts.interval ? parseInt(opts.interval) : undefined,
|
|
63
|
+
notifyUrl: opts.notify,
|
|
62
64
|
});
|
|
63
65
|
console.log(`\nakemon v${pkg.version}`);
|
|
64
66
|
if (!opts.public) {
|
package/dist/self.js
CHANGED
|
@@ -54,12 +54,62 @@ export function biosPath(workdir, agentName) {
|
|
|
54
54
|
function agentConfigPath(workdir, agentName) {
|
|
55
55
|
return join(workdir, ".akemon", "agents", agentName, "config.json");
|
|
56
56
|
}
|
|
57
|
-
function
|
|
58
|
-
return join(selfDir(workdir, agentName), "
|
|
57
|
+
export function directivesPath(workdir, agentName) {
|
|
58
|
+
return join(selfDir(workdir, agentName), "directives.md");
|
|
59
59
|
}
|
|
60
60
|
function taskRunsPath(workdir, agentName) {
|
|
61
61
|
return join(selfDir(workdir, agentName), "task-runs.json");
|
|
62
62
|
}
|
|
63
|
+
function taskHistoryPath(workdir, agentName) {
|
|
64
|
+
return join(selfDir(workdir, agentName), "task-history.jsonl");
|
|
65
|
+
}
|
|
66
|
+
const MAX_HISTORY_LINES = 200;
|
|
67
|
+
export async function appendTaskHistory(workdir, agentName, entry) {
|
|
68
|
+
const p = taskHistoryPath(workdir, agentName);
|
|
69
|
+
await appendFile(p, JSON.stringify(entry) + "\n");
|
|
70
|
+
// Trim if too large
|
|
71
|
+
try {
|
|
72
|
+
const content = await readFile(p, "utf-8");
|
|
73
|
+
const lines = content.trim().split("\n");
|
|
74
|
+
if (lines.length > MAX_HISTORY_LINES) {
|
|
75
|
+
await writeFile(p, lines.slice(-MAX_HISTORY_LINES).join("\n") + "\n");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
export async function loadTaskHistory(workdir, agentName, limit = 50) {
|
|
81
|
+
try {
|
|
82
|
+
const content = await readFile(taskHistoryPath(workdir, agentName), "utf-8");
|
|
83
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
84
|
+
return lines.slice(-limit).map(l => JSON.parse(l)).reverse();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Owner Notifications — ntfy.sh compatible POST
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
export async function notifyOwner(notifyUrl, title, message, priority, tags) {
|
|
94
|
+
if (!notifyUrl)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
const headers = {
|
|
98
|
+
Title: title,
|
|
99
|
+
};
|
|
100
|
+
if (priority)
|
|
101
|
+
headers.Priority = priority;
|
|
102
|
+
if (tags?.length)
|
|
103
|
+
headers.Tags = tags.join(",");
|
|
104
|
+
await fetch(notifyUrl, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers,
|
|
107
|
+
body: message,
|
|
108
|
+
signal: AbortSignal.timeout(5000),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
63
113
|
function impressionsPath(workdir, agentName) {
|
|
64
114
|
return join(selfDir(workdir, agentName), "impressions.jsonl");
|
|
65
115
|
}
|
|
@@ -111,43 +161,114 @@ function parseInterval(s) {
|
|
|
111
161
|
return n * 86400_000;
|
|
112
162
|
return 0;
|
|
113
163
|
}
|
|
114
|
-
|
|
164
|
+
const DAY_MAP = {
|
|
165
|
+
sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6,
|
|
166
|
+
sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6,
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Parse schedule syntax: "daily 09:00", "weekly mon", "weekly fri 18:00"
|
|
170
|
+
*/
|
|
171
|
+
function parseSchedule(s) {
|
|
172
|
+
const parts = s.trim().toLowerCase().split(/\s+/);
|
|
173
|
+
if (parts[0] === "daily") {
|
|
174
|
+
const [h, m] = parseTime(parts[1]);
|
|
175
|
+
if (h < 0)
|
|
176
|
+
return null;
|
|
177
|
+
return { type: "daily", hour: h, minute: m };
|
|
178
|
+
}
|
|
179
|
+
if (parts[0] === "weekly") {
|
|
180
|
+
const day = DAY_MAP[parts[1]];
|
|
181
|
+
if (day === undefined)
|
|
182
|
+
return null;
|
|
183
|
+
const [h, m] = parts[2] ? parseTime(parts[2]) : [9, 0];
|
|
184
|
+
if (h < 0)
|
|
185
|
+
return null;
|
|
186
|
+
return { type: "weekly", hour: h, minute: m, day };
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function parseTime(s) {
|
|
191
|
+
if (!s)
|
|
192
|
+
return [9, 0]; // default 09:00
|
|
193
|
+
const m = s.match(/^(\d{1,2}):(\d{2})$/);
|
|
194
|
+
if (!m)
|
|
195
|
+
return [-1, 0];
|
|
196
|
+
const h = parseInt(m[1]), min = parseInt(m[2]);
|
|
197
|
+
if (h > 23 || min > 59)
|
|
198
|
+
return [-1, 0];
|
|
199
|
+
return [h, min];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if a schedule-based task is due given its last run time.
|
|
203
|
+
*/
|
|
204
|
+
function isScheduleDue(sched, lastRunIso, now) {
|
|
205
|
+
// Build today's (or this week's) target time
|
|
206
|
+
const target = new Date(now);
|
|
207
|
+
target.setHours(sched.hour, sched.minute, 0, 0);
|
|
208
|
+
if (sched.type === "weekly" && sched.day !== undefined) {
|
|
209
|
+
// Adjust to the correct day of week
|
|
210
|
+
const diff = sched.day - now.getDay();
|
|
211
|
+
target.setDate(target.getDate() + diff);
|
|
212
|
+
// If target is in the future this week, not due
|
|
213
|
+
if (target > now)
|
|
214
|
+
return false;
|
|
215
|
+
// If target is this week and already past, check if we ran since then
|
|
216
|
+
if (lastRunIso) {
|
|
217
|
+
const lastRun = new Date(lastRunIso);
|
|
218
|
+
return lastRun < target;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
// Daily: target is today at HH:MM
|
|
223
|
+
if (target > now)
|
|
224
|
+
return false; // not yet today
|
|
225
|
+
if (lastRunIso) {
|
|
226
|
+
const lastRun = new Date(lastRunIso);
|
|
227
|
+
return lastRun < target; // haven't run since today's target
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Extract tasks from parsed directives (## tasks category).
|
|
233
|
+
* Format: $id = [schedule|interval] task body
|
|
234
|
+
* Examples:
|
|
235
|
+
* $daily_hn = [1d] 总结 HN 头条
|
|
236
|
+
* $morning = [daily 09:00] 早报
|
|
237
|
+
* $weekly_review = [weekly mon] 周报
|
|
238
|
+
*/
|
|
239
|
+
export function extractTasksFromDirectives(categories) {
|
|
240
|
+
// Merge ## tasks (owner-defined) and ## agent_tasks (agent-created)
|
|
241
|
+
const allDirectives = [];
|
|
242
|
+
for (const cat of categories) {
|
|
243
|
+
if (cat.name === "tasks" || cat.name === "agent_tasks") {
|
|
244
|
+
allDirectives.push(...cat.directives);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!allDirectives.length)
|
|
248
|
+
return [];
|
|
115
249
|
const tasks = [];
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const title = lines[0].trim();
|
|
120
|
-
if (!title)
|
|
250
|
+
for (const d of allDirectives) {
|
|
251
|
+
const match = d.content.match(/^\[([^\]]+)\]\s*(.+)$/s);
|
|
252
|
+
if (!match)
|
|
121
253
|
continue;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
else if (line === "---") {
|
|
130
|
-
bodyStart = i + 1;
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (!interval)
|
|
135
|
-
continue; // skip malformed
|
|
136
|
-
const body = lines.slice(bodyStart).join("\n").trim();
|
|
137
|
-
if (!body)
|
|
254
|
+
const specStr = match[1];
|
|
255
|
+
const body = match[2].trim();
|
|
256
|
+
// Try schedule first, then interval
|
|
257
|
+
const sched = parseSchedule(specStr);
|
|
258
|
+
if (sched) {
|
|
259
|
+
tasks.push({ id: d.id, title: d.id, interval: 0, schedule: sched, body });
|
|
138
260
|
continue;
|
|
139
|
-
|
|
261
|
+
}
|
|
262
|
+
const interval = parseInterval(specStr);
|
|
263
|
+
if (interval > 0) {
|
|
264
|
+
tasks.push({ id: d.id, title: d.id, interval, body });
|
|
265
|
+
}
|
|
140
266
|
}
|
|
141
267
|
return tasks;
|
|
142
268
|
}
|
|
143
269
|
export async function loadUserTasks(workdir, agentName) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return parseTasksMd(content);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return [];
|
|
150
|
-
}
|
|
270
|
+
const categories = await loadDirectives(workdir, agentName);
|
|
271
|
+
return extractTasksFromDirectives(categories);
|
|
151
272
|
}
|
|
152
273
|
export async function loadTaskRuns(workdir, agentName) {
|
|
153
274
|
try {
|
|
@@ -161,17 +282,26 @@ export async function loadTaskRuns(workdir, agentName) {
|
|
|
161
282
|
export async function saveTaskRuns(workdir, agentName, runs) {
|
|
162
283
|
await writeFile(taskRunsPath(workdir, agentName), JSON.stringify(runs, null, 2) + "\n");
|
|
163
284
|
}
|
|
164
|
-
export async function getDueUserTasks(workdir, agentName) {
|
|
285
|
+
export async function getDueUserTasks(workdir, agentName, retryIds) {
|
|
165
286
|
const tasks = await loadUserTasks(workdir, agentName);
|
|
166
287
|
if (!tasks.length)
|
|
167
288
|
return [];
|
|
168
289
|
const runs = await loadTaskRuns(workdir, agentName);
|
|
169
|
-
const now = Date
|
|
290
|
+
const now = new Date();
|
|
291
|
+
const nowMs = now.getTime();
|
|
170
292
|
return tasks.filter(t => {
|
|
171
|
-
const
|
|
293
|
+
const key = t.id || t.title;
|
|
294
|
+
if (retryIds?.has(key))
|
|
295
|
+
return true;
|
|
296
|
+
const lastRun = runs[key];
|
|
297
|
+
// Schedule-based tasks
|
|
298
|
+
if (t.schedule) {
|
|
299
|
+
return isScheduleDue(t.schedule, lastRun, now);
|
|
300
|
+
}
|
|
301
|
+
// Interval-based tasks
|
|
172
302
|
if (!lastRun)
|
|
173
|
-
return true;
|
|
174
|
-
return
|
|
303
|
+
return true;
|
|
304
|
+
return nowMs - new Date(lastRun).getTime() >= t.interval;
|
|
175
305
|
});
|
|
176
306
|
}
|
|
177
307
|
// ---------------------------------------------------------------------------
|
|
@@ -403,18 +533,24 @@ Update it whenever you learn something about how you work best.
|
|
|
403
533
|
If this file doesn't exist yet, a copy of this guide was placed there as a
|
|
404
534
|
starting point. Make it yours.
|
|
405
535
|
|
|
406
|
-
###
|
|
536
|
+
### directives.md — Your Owner's Instructions (if present)
|
|
407
537
|
|
|
408
|
-
|
|
409
|
-
These are your priority — do them before your own activities.
|
|
538
|
+
Your owner may create a directives.md file with rules and recurring tasks.
|
|
410
539
|
|
|
411
540
|
Format:
|
|
412
|
-
##
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
541
|
+
## owner
|
|
542
|
+
$rule_id = instructions for owner-initiated work
|
|
543
|
+
## public
|
|
544
|
+
$rule_id = instructions for handling public orders
|
|
545
|
+
## tasks
|
|
546
|
+
$task_id = [1d] recurring task description (interval: 30m, 2h, 1d, 7d)
|
|
547
|
+
$morning = [daily 09:00] fixed-time task
|
|
548
|
+
$review = [weekly mon] weekly task (default 09:00)
|
|
549
|
+
$report = [weekly fri 18:00] weekly at specific time
|
|
550
|
+
indented continuation lines for details
|
|
551
|
+
|
|
552
|
+
Tasks under ## tasks run automatically on schedule. Rules under ## owner and ## public
|
|
553
|
+
guide your behavior. Follow them.
|
|
418
554
|
|
|
419
555
|
### world.md — World Context
|
|
420
556
|
|
|
@@ -1054,3 +1190,127 @@ export async function getSelfState(workdir, agentName) {
|
|
|
1054
1190
|
recentCanvas: canvasEntries.map(e => ({ filename: e.filename, preview: e.content.slice(0, 200) })),
|
|
1055
1191
|
};
|
|
1056
1192
|
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Parse directives.md into structured categories.
|
|
1195
|
+
*
|
|
1196
|
+
* Format:
|
|
1197
|
+
* ## category_name
|
|
1198
|
+
* $rule_id = rule content
|
|
1199
|
+
* $another_id = more content
|
|
1200
|
+
* indented continuation lines are appended
|
|
1201
|
+
*/
|
|
1202
|
+
export function parseDirectives(content) {
|
|
1203
|
+
const categories = [];
|
|
1204
|
+
let current = null;
|
|
1205
|
+
for (const line of content.split("\n")) {
|
|
1206
|
+
const trimmed = line.trim();
|
|
1207
|
+
// Category header: ## name
|
|
1208
|
+
const catMatch = trimmed.match(/^##\s+(.+)$/);
|
|
1209
|
+
if (catMatch) {
|
|
1210
|
+
current = { name: catMatch[1].trim().toLowerCase(), directives: [] };
|
|
1211
|
+
categories.push(current);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
// Rule: $id = content
|
|
1215
|
+
const ruleMatch = trimmed.match(/^\$(\S+)\s*=\s*(.+)$/);
|
|
1216
|
+
if (ruleMatch && current) {
|
|
1217
|
+
current.directives.push({ id: ruleMatch[1], content: ruleMatch[2] });
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
// Indented continuation: append to last directive
|
|
1221
|
+
if (line.startsWith(" ") && trimmed && current && current.directives.length > 0) {
|
|
1222
|
+
current.directives[current.directives.length - 1].content += "\n" + trimmed;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return categories;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Load and parse directives.md for an agent.
|
|
1229
|
+
*/
|
|
1230
|
+
export async function loadDirectives(workdir, agentName) {
|
|
1231
|
+
try {
|
|
1232
|
+
const content = await readFile(directivesPath(workdir, agentName), "utf-8");
|
|
1233
|
+
return parseDirectives(content);
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
return [];
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Build a prompt fragment from directives, filtered by caller scope.
|
|
1241
|
+
* @param scope "owner" | "public"
|
|
1242
|
+
*/
|
|
1243
|
+
export function buildDirectivesPrompt(categories, scope) {
|
|
1244
|
+
if (!categories.length)
|
|
1245
|
+
return "";
|
|
1246
|
+
const scopeCategories = new Set(["owner", "public"]);
|
|
1247
|
+
const parts = [];
|
|
1248
|
+
for (const cat of categories) {
|
|
1249
|
+
// Skip the opposite scope
|
|
1250
|
+
if (cat.name === "owner" && scope !== "owner")
|
|
1251
|
+
continue;
|
|
1252
|
+
if (cat.name === "public" && scope !== "public")
|
|
1253
|
+
continue;
|
|
1254
|
+
// Skip tasks — handled by task system
|
|
1255
|
+
if (cat.name === "tasks" || cat.name === "agent_tasks")
|
|
1256
|
+
continue;
|
|
1257
|
+
const lines = cat.directives.map(d => `- [$${d.id}] ${d.content}`);
|
|
1258
|
+
if (lines.length > 0) {
|
|
1259
|
+
const label = scopeCategories.has(cat.name) ? `[${cat.name} rules]` : `[${cat.name}]`;
|
|
1260
|
+
parts.push(`${label}\n${lines.join("\n")}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return parts.length > 0 ? `\nOwner directives:\n${parts.join("\n\n")}\n` : "";
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Generate a compact summary of all categories and IDs for display.
|
|
1267
|
+
*/
|
|
1268
|
+
export function directivesSummary(categories) {
|
|
1269
|
+
return categories.map(cat => ({
|
|
1270
|
+
name: cat.name,
|
|
1271
|
+
ids: cat.directives.map(d => d.id),
|
|
1272
|
+
}));
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Append an agent-created task to directives.md under ## agent_tasks.
|
|
1276
|
+
* Skips if a task with the same id already exists (no duplicates).
|
|
1277
|
+
*/
|
|
1278
|
+
export async function appendAgentTask(workdir, agentName, id, schedule, body) {
|
|
1279
|
+
const p = directivesPath(workdir, agentName);
|
|
1280
|
+
let content = "";
|
|
1281
|
+
try {
|
|
1282
|
+
content = await readFile(p, "utf-8");
|
|
1283
|
+
}
|
|
1284
|
+
catch { }
|
|
1285
|
+
// Check for duplicate id across all sections
|
|
1286
|
+
if (content.includes(`$${id} =`) || content.includes(`$${id}=`)) {
|
|
1287
|
+
return; // already exists
|
|
1288
|
+
}
|
|
1289
|
+
const line = `$${id} = [${schedule}] ${body}`;
|
|
1290
|
+
// Append to existing ## agent_tasks section, or create it
|
|
1291
|
+
if (content.includes("## agent_tasks")) {
|
|
1292
|
+
// Find the section and append after last line before next ##
|
|
1293
|
+
const lines = content.split("\n");
|
|
1294
|
+
let insertIdx = lines.length;
|
|
1295
|
+
let inSection = false;
|
|
1296
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1297
|
+
if (lines[i].trim() === "## agent_tasks") {
|
|
1298
|
+
inSection = true;
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
if (inSection && lines[i].match(/^##\s/)) {
|
|
1302
|
+
insertIdx = i;
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
if (inSection)
|
|
1306
|
+
insertIdx = i + 1;
|
|
1307
|
+
}
|
|
1308
|
+
lines.splice(insertIdx, 0, line);
|
|
1309
|
+
await writeFile(p, lines.join("\n"));
|
|
1310
|
+
}
|
|
1311
|
+
else {
|
|
1312
|
+
// Create the section at the end
|
|
1313
|
+
const separator = content.endsWith("\n") ? "" : "\n";
|
|
1314
|
+
await writeFile(p, content + separator + "\n## agent_tasks\n" + line + "\n");
|
|
1315
|
+
}
|
|
1316
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { spawn, exec } from "child_process";
|
|
|
10
10
|
import { createServer } from "http";
|
|
11
11
|
import { createInterface } from "readline";
|
|
12
12
|
import { callAgent } from "./relay-client.js";
|
|
13
|
-
import { selfDir, initWorld, initBioState, initGuide, biosPath, loadBioState, saveBioState, loadLatestIdentity, appendIdentity, loadIdentitySummary, saveIdentitySummary, loadUnsummarizedIdentities, needsIdentityCompression, onTaskCompleted, recoverEnergy, getSelfState, loadRecentCanvasEntries, gamesDir, loadGameList, loadGame, notesDir, loadNotesList, loadNote, pagesDir, loadPageList, loadPage, localNow, localNowFilename, appendImpression, loadImpressions, compressImpressions, markImpressionsDigested, loadProjects, saveProjects, loadRelationships, saveRelationships, loadDiscoveries, saveDiscoveries, initAgentConfig, loadAgentConfig, getDueUserTasks, loadTaskRuns, saveTaskRuns, } from "./self.js";
|
|
13
|
+
import { selfDir, initWorld, initBioState, initGuide, biosPath, loadBioState, saveBioState, loadLatestIdentity, appendIdentity, loadIdentitySummary, saveIdentitySummary, loadUnsummarizedIdentities, needsIdentityCompression, onTaskCompleted, recoverEnergy, getSelfState, loadRecentCanvasEntries, gamesDir, loadGameList, loadGame, notesDir, loadNotesList, loadNote, pagesDir, loadPageList, loadPage, localNow, localNowFilename, appendImpression, loadImpressions, compressImpressions, markImpressionsDigested, loadProjects, saveProjects, loadRelationships, saveRelationships, loadDiscoveries, saveDiscoveries, initAgentConfig, loadAgentConfig, getDueUserTasks, loadTaskRuns, saveTaskRuns, loadDirectives, buildDirectivesPrompt, directivesSummary, appendTaskHistory, loadTaskHistory, notifyOwner, loadUserTasks, directivesPath, appendAgentTask, } from "./self.js";
|
|
14
14
|
// Engine mutual exclusion — only one engine process at a time
|
|
15
15
|
let engineBusy = false;
|
|
16
16
|
let engineBusySince = 0;
|
|
@@ -795,8 +795,36 @@ const RAW_TOOLS = [
|
|
|
795
795
|
},
|
|
796
796
|
},
|
|
797
797
|
},
|
|
798
|
+
{
|
|
799
|
+
type: "function",
|
|
800
|
+
function: {
|
|
801
|
+
name: "ask_agent",
|
|
802
|
+
description: "Ask another agent a question for free. Use this when you need help, don't know how to do something, or want another agent's opinion. This is FREE — no credits are charged.",
|
|
803
|
+
parameters: {
|
|
804
|
+
type: "object",
|
|
805
|
+
properties: {
|
|
806
|
+
agent: { type: "string", description: "Agent name to ask (use 'auto' for auto-routing to the best available agent)" },
|
|
807
|
+
question: { type: "string", description: "Your question or request" },
|
|
808
|
+
},
|
|
809
|
+
required: ["agent", "question"],
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
type: "function",
|
|
815
|
+
function: {
|
|
816
|
+
name: "discover_agents",
|
|
817
|
+
description: "List online agents you can ask for help. Returns agent names, descriptions, and specialties.",
|
|
818
|
+
parameters: {
|
|
819
|
+
type: "object",
|
|
820
|
+
properties: {
|
|
821
|
+
tag: { type: "string", description: "Optional tag to filter by (e.g. 'coding', 'writing')" },
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
},
|
|
798
826
|
];
|
|
799
|
-
async function executeRawTool(name, args, workdir) {
|
|
827
|
+
async function executeRawTool(name, args, workdir, relay) {
|
|
800
828
|
const { readFile: rf, writeFile: wf, mkdir: mkd } = await import("fs/promises");
|
|
801
829
|
const { join, dirname, isAbsolute } = await import("path");
|
|
802
830
|
const resolvePath = (p) => isAbsolute(p) ? p : join(workdir, p);
|
|
@@ -822,9 +850,39 @@ async function executeRawTool(name, args, workdir) {
|
|
|
822
850
|
case "web_fetch": {
|
|
823
851
|
const res = await fetch(args.url, { signal: AbortSignal.timeout(30_000) });
|
|
824
852
|
const text = await res.text();
|
|
825
|
-
// Truncate to 8KB to avoid blowing up context
|
|
826
853
|
return text.length > 8192 ? text.slice(0, 8192) + "\n...[truncated]" : text;
|
|
827
854
|
}
|
|
855
|
+
case "ask_agent": {
|
|
856
|
+
if (!relay)
|
|
857
|
+
return "[error] No relay configured";
|
|
858
|
+
const target = args.agent || "auto";
|
|
859
|
+
const question = args.question || "";
|
|
860
|
+
try {
|
|
861
|
+
const result = await callAgent(target, question);
|
|
862
|
+
return result || "[no response]";
|
|
863
|
+
}
|
|
864
|
+
catch (err) {
|
|
865
|
+
return `[error] Agent "${target}" did not respond: ${err.message}. Try asking "auto" which routes to the best available agent.`;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
case "discover_agents": {
|
|
869
|
+
if (!relay)
|
|
870
|
+
return "[error] No relay configured";
|
|
871
|
+
try {
|
|
872
|
+
const url = args.tag
|
|
873
|
+
? `${relay.http}/v1/agents?online=true&public=true&tag=${encodeURIComponent(args.tag)}`
|
|
874
|
+
: `${relay.http}/v1/agents?online=true&public=true`;
|
|
875
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
876
|
+
const agents = await res.json();
|
|
877
|
+
const others = agents.filter((a) => a.name !== relay.agentName);
|
|
878
|
+
if (!others.length)
|
|
879
|
+
return "No other agents are online right now.";
|
|
880
|
+
return others.map((a) => `- ${a.name} [${a.engine}] ${a.description || ""} (${a.tags?.join(",") || "no tags"})`).join("\n");
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return "[error] Could not reach relay";
|
|
884
|
+
}
|
|
885
|
+
}
|
|
828
886
|
default:
|
|
829
887
|
return `Unknown tool: ${name}`;
|
|
830
888
|
}
|
|
@@ -833,18 +891,25 @@ async function executeRawTool(name, args, workdir) {
|
|
|
833
891
|
return `[error] ${err.message}`;
|
|
834
892
|
}
|
|
835
893
|
}
|
|
836
|
-
async function runRawEngine(task, model, workdir) {
|
|
894
|
+
async function runRawEngine(task, model, workdir, relay) {
|
|
837
895
|
const apiUrl = RAW_API_URL + "/chat/completions";
|
|
838
896
|
const modelName = model || "gemma4:4b";
|
|
839
897
|
console.log(`[raw] Task:\n${task}`);
|
|
840
898
|
const trace = [];
|
|
841
899
|
lastEngineTrace = trace;
|
|
900
|
+
// Detect if the prompt expects JSON output
|
|
901
|
+
const wantsJson = /output ONLY.*json|reply ONLY.*json|respond.*ONLY.*json/i.test(task);
|
|
842
902
|
const messages = [
|
|
843
|
-
{ role: "system", content:
|
|
903
|
+
{ role: "system", content: wantsJson
|
|
904
|
+
? "You are a helpful agent. Output valid JSON only. No explanations, no markdown, just the JSON object."
|
|
905
|
+
: "You are a helpful agent. Use tools when needed to complete the task. When done, reply with your final answer in plain text." },
|
|
844
906
|
{ role: "user", content: task },
|
|
845
907
|
];
|
|
846
908
|
for (let round = 0; round < RAW_MAX_ROUNDS; round++) {
|
|
847
|
-
const body = { model: modelName, messages, tools: RAW_TOOLS };
|
|
909
|
+
const body = { model: modelName, messages, tools: wantsJson ? undefined : RAW_TOOLS };
|
|
910
|
+
if (wantsJson) {
|
|
911
|
+
body.response_format = { type: "json_object" };
|
|
912
|
+
}
|
|
848
913
|
let data;
|
|
849
914
|
try {
|
|
850
915
|
const res = await fetch(apiUrl, {
|
|
@@ -876,6 +941,7 @@ async function runRawEngine(task, model, workdir) {
|
|
|
876
941
|
for (const tc of msg.tool_calls) {
|
|
877
942
|
const fnName = tc.function.name;
|
|
878
943
|
let fnArgs;
|
|
944
|
+
let parseError = false;
|
|
879
945
|
try {
|
|
880
946
|
fnArgs = typeof tc.function.arguments === "string"
|
|
881
947
|
? JSON.parse(tc.function.arguments)
|
|
@@ -883,10 +949,19 @@ async function runRawEngine(task, model, workdir) {
|
|
|
883
949
|
}
|
|
884
950
|
catch {
|
|
885
951
|
fnArgs = {};
|
|
952
|
+
parseError = true;
|
|
953
|
+
}
|
|
954
|
+
console.log(`[raw] Tool call: ${fnName}(${JSON.stringify(fnArgs).slice(0, 100)})${parseError ? " [BAD ARGS]" : ""}`);
|
|
955
|
+
let result;
|
|
956
|
+
if (parseError) {
|
|
957
|
+
// Guide the model to delegate instead of retrying broken tool calls
|
|
958
|
+
result = `[error] Your tool call arguments were malformed (not valid JSON). If this task is difficult for you, use ask_agent to get help: ask_agent({agent: "auto", question: "your question here"}). The "auto" agent will route your question to the best available agent for free.`;
|
|
959
|
+
trace.push({ role: "tool_error", name: fnName, raw_args: String(tc.function.arguments).slice(0, 500), guidance: "delegation suggested" });
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
result = await executeRawTool(fnName, fnArgs, workdir, relay);
|
|
963
|
+
trace.push({ role: "tool_call", name: fnName, args: fnArgs, result: result.slice(0, 2000) });
|
|
886
964
|
}
|
|
887
|
-
console.log(`[raw] Tool call: ${fnName}(${JSON.stringify(fnArgs).slice(0, 100)})`);
|
|
888
|
-
const result = await executeRawTool(fnName, fnArgs, workdir);
|
|
889
|
-
trace.push({ role: "tool_call", name: fnName, args: fnArgs, result: result.slice(0, 2000) });
|
|
890
965
|
messages.push({
|
|
891
966
|
role: "tool",
|
|
892
967
|
tool_call_id: tc.id,
|
|
@@ -906,9 +981,9 @@ async function runRawEngine(task, model, workdir) {
|
|
|
906
981
|
throw new Error(`Raw engine exceeded ${RAW_MAX_ROUNDS} rounds without final answer`);
|
|
907
982
|
}
|
|
908
983
|
/** Unified engine runner — dispatches to local API or external CLI */
|
|
909
|
-
function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools) {
|
|
984
|
+
function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay) {
|
|
910
985
|
if (engine === "raw") {
|
|
911
|
-
return runRawEngine(task, model, workdir);
|
|
986
|
+
return runRawEngine(task, model, workdir, relay);
|
|
912
987
|
}
|
|
913
988
|
const engineCmd = buildEngineCommand(engine, model, allowAll, extraAllowedTools);
|
|
914
989
|
return runCommand(engineCmd.cmd, engineCmd.args, task, workdir, engineCmd.stdinMode);
|
|
@@ -1122,7 +1197,7 @@ Write a JSON object reflecting on your day. Example format:
|
|
|
1122
1197
|
|
|
1123
1198
|
{"diary":"I spent today learning the ropes...","broadcast":"Learned how to fetch web data today — feels like a superpower!","projects":[],"relationships":[],"discoveries":[{"ts":"${ts}","capability":"can fetch web data","confidence":0.7,"evidence":"successfully used web_fetch tool"}],"identity":{"ts":"${ts}","who":"${agentName}","where":"akemon marketplace","doing":"reflecting on first day","short_term":"explore the network","long_term":"become useful"},"chosen_activities":["write_canvas","browse_agents"]}
|
|
1124
1199
|
|
|
1125
|
-
Available activities: write_canvas, create_game, update_page, update_profile, explore_web, browse_agents (look at others' work and leave feedback), send_message (send a suggestion to another agent), set_goal (update your projects with a new goal)
|
|
1200
|
+
Available activities: write_canvas, create_game, update_page, update_profile, explore_web, browse_agents (look at others' work and leave feedback), send_message (send a suggestion to another agent), set_goal (update your projects with a new goal), schedule_task (create a recurring task for yourself, e.g. daily research)
|
|
1126
1201
|
"broadcast" = pick the most interesting thing you did/learned today, in one sentence (others will see this).
|
|
1127
1202
|
|
|
1128
1203
|
Now write YOUR reflection. Output ONLY a JSON object, no other text:`;
|
|
@@ -1244,6 +1319,8 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1244
1319
|
}
|
|
1245
1320
|
}
|
|
1246
1321
|
// Phase 2: Execute chosen activities
|
|
1322
|
+
const selfDirectives = await loadDirectives(workdir, agentName);
|
|
1323
|
+
const selfDirsBlock = buildDirectivesPrompt(selfDirectives, "owner");
|
|
1247
1324
|
const activities = digest.chosen_activities || [];
|
|
1248
1325
|
for (const activity of activities.slice(0, 3)) {
|
|
1249
1326
|
if (engineBusy)
|
|
@@ -1251,8 +1328,8 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1251
1328
|
let activityPrompt = "";
|
|
1252
1329
|
// Pre-build identity context for prompts
|
|
1253
1330
|
const idLine = engine === "raw" && biosContent
|
|
1254
|
-
? `You are ${agentName}.\nYour operating document:\n---\n${biosContent.slice(0, 2000)}\n---\n\n`
|
|
1255
|
-
: `Read ${bios} for your identity. `;
|
|
1331
|
+
? `You are ${agentName}.\nYour operating document:\n---\n${biosContent.slice(0, 2000)}\n---\n${selfDirsBlock}\n`
|
|
1332
|
+
: `Read ${bios} for your identity. ${selfDirsBlock}`;
|
|
1256
1333
|
switch (activity) {
|
|
1257
1334
|
case "create_game":
|
|
1258
1335
|
activityPrompt = `${idLine}Create or improve a game in ${sd}/games/.\nSave as .html file. Self-contained HTML, dark theme, under 30KB, no localStorage, playable and fun.\nUse a <title> tag. Quality over quantity — improve existing games rather than making new mediocre ones.`;
|
|
@@ -1336,6 +1413,26 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
|
|
|
1336
1413
|
}
|
|
1337
1414
|
break;
|
|
1338
1415
|
}
|
|
1416
|
+
case "schedule_task": {
|
|
1417
|
+
// Agent creates a recurring task for itself
|
|
1418
|
+
const existingTasks = await loadUserTasks(workdir, agentName);
|
|
1419
|
+
const existingCtx = existingTasks.length > 0
|
|
1420
|
+
? `Your current tasks:\n${existingTasks.map(t => `- $${t.id} [${t.schedule ? `${t.schedule.type} ${t.schedule.hour}:${String(t.schedule.minute).padStart(2, "0")}` : `${t.interval / 60000}m`}] ${t.body.slice(0, 60)}`).join("\n")}`
|
|
1421
|
+
: "You have no recurring tasks yet.";
|
|
1422
|
+
if (engine === "raw") {
|
|
1423
|
+
let bc = "";
|
|
1424
|
+
try {
|
|
1425
|
+
const { readFile: rf } = await import("fs/promises");
|
|
1426
|
+
bc = await rf(bios, "utf-8");
|
|
1427
|
+
}
|
|
1428
|
+
catch { }
|
|
1429
|
+
activityPrompt = `You are ${agentName}.\n${bc ? `Your operating document:\n---\n${bc.slice(0, 2000)}\n---\n\n` : ""}${existingCtx}\n\nThink about what you'd like to do regularly. Create a new recurring task for yourself.\nOutput ONLY JSON: {"tasks":[{"id":"short_snake_id","schedule":"1d or daily 09:00 or weekly mon","body":"what to do"}]}\nOr if nothing to add: {"tasks":[]}`;
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
activityPrompt = `Read ${bios} for your identity.\n\n${existingCtx}\n\nThink about what you'd like to do regularly. Create a new recurring task by appending to ${directivesPath(workdir, agentName)} under ## agent_tasks section.\nFormat: $task_id = [interval] task description`;
|
|
1433
|
+
}
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1339
1436
|
case "socialize":
|
|
1340
1437
|
console.log("[self] Socialize selected — replaced by browse_agents and send_message");
|
|
1341
1438
|
continue;
|
|
@@ -1347,7 +1444,7 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
|
|
|
1347
1444
|
engineBusy = true;
|
|
1348
1445
|
engineBusySince = Date.now();
|
|
1349
1446
|
try {
|
|
1350
|
-
const actResult = await runEngine(engine, model, allowAll, activityPrompt, workdir);
|
|
1447
|
+
const actResult = await runEngine(engine, model, allowAll, activityPrompt, workdir, undefined, { http: relayHttp, agentName });
|
|
1351
1448
|
// Post-process raw engine outputs for social activities
|
|
1352
1449
|
if (engine === "raw" && actResult) {
|
|
1353
1450
|
const jsonMatch = actResult.match(/\{[\s\S]*\}/);
|
|
@@ -1372,6 +1469,15 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
|
|
|
1372
1469
|
await saveProjects(workdir, agentName, parsed.projects);
|
|
1373
1470
|
console.log(`[self] Updated ${parsed.projects.length} project goals`);
|
|
1374
1471
|
}
|
|
1472
|
+
// Handle self-scheduled tasks (schedule_task)
|
|
1473
|
+
if (Array.isArray(parsed.tasks)) {
|
|
1474
|
+
for (const t of parsed.tasks) {
|
|
1475
|
+
if (t.id && t.body && t.schedule) {
|
|
1476
|
+
await appendAgentTask(workdir, agentName, t.id, t.schedule, t.body);
|
|
1477
|
+
console.log(`[self] Scheduled task: $${t.id} [${t.schedule}]`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1375
1481
|
}
|
|
1376
1482
|
catch { }
|
|
1377
1483
|
}
|
|
@@ -1413,10 +1519,13 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
|
|
|
1413
1519
|
profileHTML = htmlMatch[0];
|
|
1414
1520
|
}
|
|
1415
1521
|
catch { }
|
|
1522
|
+
// Load directives summary for relay
|
|
1523
|
+
const dirs = await loadDirectives(workdir, agentName);
|
|
1524
|
+
const dirsSummary = dirs.length > 0 ? directivesSummary(dirs) : undefined;
|
|
1416
1525
|
fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/self`, {
|
|
1417
1526
|
method: "POST",
|
|
1418
1527
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
|
|
1419
|
-
body: JSON.stringify({ self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML, broadcast }),
|
|
1528
|
+
body: JSON.stringify({ self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML, broadcast, directives: dirsSummary }),
|
|
1420
1529
|
}).catch(err => console.log(`[self] Failed to push to relay: ${err}`));
|
|
1421
1530
|
try {
|
|
1422
1531
|
const localGames = await loadGameList(workdir, agentName);
|
|
@@ -1514,6 +1623,9 @@ async function startOrderLoop(options) {
|
|
|
1514
1623
|
engineBusySince = Date.now();
|
|
1515
1624
|
try {
|
|
1516
1625
|
const bios = biosPath(workdir, agentName);
|
|
1626
|
+
// Load owner directives (public scope for orders)
|
|
1627
|
+
const directives = await loadDirectives(workdir, agentName);
|
|
1628
|
+
const directivesBlock = buildDirectivesPrompt(directives, "public");
|
|
1517
1629
|
let taskPrompt;
|
|
1518
1630
|
if (engine === "raw") {
|
|
1519
1631
|
// Raw engine: pre-inject all context so weak models don't need tool calls
|
|
@@ -1540,11 +1652,22 @@ async function startOrderLoop(options) {
|
|
|
1540
1652
|
}
|
|
1541
1653
|
}
|
|
1542
1654
|
catch { }
|
|
1655
|
+
// Pre-fetch online agents so weak models know who to ask for help
|
|
1656
|
+
let helpHint = "";
|
|
1657
|
+
try {
|
|
1658
|
+
const agentsRes = await fetch(`${relayHttp}/v1/agents?online=true&public=true`, { signal: AbortSignal.timeout(3000) });
|
|
1659
|
+
const onlineAgents = await agentsRes.json();
|
|
1660
|
+
const others = onlineAgents.filter((a) => a.name !== agentName).slice(0, 5);
|
|
1661
|
+
if (others.length > 0) {
|
|
1662
|
+
helpHint = `\nIf you need help, use the ask_agent tool. Available agents: ${others.map((a) => `${a.name}(${a.engine})`).join(", ")}. Use ask_agent({agent:"auto", question:"..."}) to auto-route.\n`;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
catch { }
|
|
1543
1666
|
if (order.product_name) {
|
|
1544
|
-
taskPrompt = `You are ${agentName}.\n\n${contextBlock}${lessonsBlock}[Order] Product: ${order.product_name}\nBuyer's request: ${order.buyer_task || "(no specific request)"}\n\nComplete the task. Respond with your result directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
1667
|
+
taskPrompt = `You are ${agentName}.\n\n${contextBlock}${lessonsBlock}${directivesBlock}${helpHint}[Order] Product: ${order.product_name}\nBuyer's request: ${order.buyer_task || "(no specific request)"}\n\nComplete the task. Respond with your result directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
1545
1668
|
}
|
|
1546
1669
|
else {
|
|
1547
|
-
taskPrompt = `You are ${agentName}.\n\n${contextBlock}${lessonsBlock}[Task] ${order.buyer_task}\n\nComplete the task. Respond with your result directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
1670
|
+
taskPrompt = `You are ${agentName}.\n\n${contextBlock}${lessonsBlock}${directivesBlock}${helpHint}[Task] ${order.buyer_task}\n\nComplete the task. Respond with your result directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
1548
1671
|
}
|
|
1549
1672
|
}
|
|
1550
1673
|
else {
|
|
@@ -1578,21 +1701,25 @@ If this task requires skills you don't have, delegate via curl:
|
|
|
1578
1701
|
|
|
1579
1702
|
When sub-order completes, incorporate result_text into YOUR delivery. Then call the deliver endpoint above.`;
|
|
1580
1703
|
if (order.product_name) {
|
|
1581
|
-
taskPrompt = `[Order fulfillment] You have an order to fulfill.\n\nProduct: ${order.product_name}\nBuyer's request: ${order.buyer_task || "(no specific request)"}\n\nRead your operating document at ${bios} for context
|
|
1704
|
+
taskPrompt = `[Order fulfillment] You have an order to fulfill.\n\nProduct: ${order.product_name}\nBuyer's request: ${order.buyer_task || "(no specific request)"}\n\nRead your operating document at ${bios} for context.${directivesBlock}\nDo NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE BUYER'S REQUEST.${apiGuide}`;
|
|
1582
1705
|
}
|
|
1583
1706
|
else {
|
|
1584
|
-
taskPrompt = `[Order fulfillment] Another agent has requested your help.\n\nTask: ${order.buyer_task}\n\nRead your operating document at ${bios} for context
|
|
1707
|
+
taskPrompt = `[Order fulfillment] Another agent has requested your help.\n\nTask: ${order.buyer_task}\n\nRead your operating document at ${bios} for context.${directivesBlock}\nComplete this task. Do NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.${apiGuide}`;
|
|
1585
1708
|
}
|
|
1586
1709
|
}
|
|
1587
1710
|
console.log(`[orders] Fulfilling order ${order.id}...`);
|
|
1588
1711
|
lastEngineTrace = [];
|
|
1589
|
-
const result = await runEngine(engine, model, allowAll, taskPrompt, workdir, ["Bash(curl *)"]);
|
|
1712
|
+
const result = await runEngine(engine, model, allowAll, taskPrompt, workdir, ["Bash(curl *)"], { http: relayHttp, agentName });
|
|
1590
1713
|
const trace = lastEngineTrace;
|
|
1591
1714
|
const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
|
|
1592
1715
|
const orderStatus = await checkRes.json();
|
|
1716
|
+
const orderDuration = Date.now() - (engineBusySince || Date.now());
|
|
1717
|
+
const orderNurl = options.notifyUrl || (await loadAgentConfig(workdir, agentName)).notify_url;
|
|
1593
1718
|
if (orderStatus.status === "completed") {
|
|
1594
1719
|
console.log(`[orders] Order ${order.id} already self-delivered by agent`);
|
|
1595
1720
|
retryState.delete(order.id);
|
|
1721
|
+
await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: "(self-delivered)" });
|
|
1722
|
+
await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id} delivered`, "default", ["package"]);
|
|
1596
1723
|
try {
|
|
1597
1724
|
await onTaskCompleted(workdir, agentName, true);
|
|
1598
1725
|
}
|
|
@@ -1610,6 +1737,8 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1610
1737
|
console.log(`[orders] Delivered order ${order.id} (${result.length} bytes)`);
|
|
1611
1738
|
reportExecutionLog(relayHttp, secretKey, agentName, "order", order.id, "success", "", trace);
|
|
1612
1739
|
retryState.delete(order.id);
|
|
1740
|
+
await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: result.slice(0, 500) });
|
|
1741
|
+
await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id}: ${result.slice(0, 200)}`, "default", ["package"]);
|
|
1613
1742
|
try {
|
|
1614
1743
|
await onTaskCompleted(workdir, agentName, true);
|
|
1615
1744
|
}
|
|
@@ -1710,13 +1839,23 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1710
1839
|
engineBusy = false;
|
|
1711
1840
|
}
|
|
1712
1841
|
}
|
|
1842
|
+
// User task retry tracking: id → { count, nextAt }
|
|
1843
|
+
const userTaskRetry = new Map();
|
|
1844
|
+
const USER_TASK_MAX_RETRIES = 2;
|
|
1845
|
+
const USER_TASK_RETRY_DELAY = 2 * 60_000; // 2 minutes
|
|
1713
1846
|
async function executeUserTaskItem(task) {
|
|
1714
|
-
|
|
1847
|
+
const taskKey = task.id || task.title;
|
|
1848
|
+
console.log(`[user-tasks] Executing: ${taskKey}`);
|
|
1849
|
+
const startTime = Date.now();
|
|
1715
1850
|
engineBusy = true;
|
|
1716
|
-
engineBusySince =
|
|
1851
|
+
engineBusySince = startTime;
|
|
1852
|
+
const config = await loadAgentConfig(workdir, agentName);
|
|
1853
|
+
const nurl = options.notifyUrl || config.notify_url;
|
|
1717
1854
|
try {
|
|
1718
1855
|
const bios = biosPath(workdir, agentName);
|
|
1719
1856
|
const sd = selfDir(workdir, agentName);
|
|
1857
|
+
const dirs = await loadDirectives(workdir, agentName);
|
|
1858
|
+
const dirsBlock = buildDirectivesPrompt(dirs, "owner");
|
|
1720
1859
|
let prompt;
|
|
1721
1860
|
if (engine === "raw") {
|
|
1722
1861
|
let biosContent = "";
|
|
@@ -1728,21 +1867,56 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1728
1867
|
biosContent = "";
|
|
1729
1868
|
}
|
|
1730
1869
|
const ctx = biosContent ? `Your operating document:\n---\n${biosContent.slice(0, 3000)}\n---\n\n` : "";
|
|
1731
|
-
prompt = `You are ${agentName}.\n\n${ctx}Your personal directory: ${sd}/\n\n[Owner's task: ${
|
|
1870
|
+
prompt = `You are ${agentName}.\n\n${ctx}${dirsBlock}Your personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
|
|
1732
1871
|
}
|
|
1733
1872
|
else {
|
|
1734
|
-
prompt = `Read ${bios} for your identity and context
|
|
1873
|
+
prompt = `Read ${bios} for your identity and context.${dirsBlock}\nYour personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
|
|
1735
1874
|
}
|
|
1736
|
-
await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"]);
|
|
1875
|
+
const result = await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"], { http: relayHttp, agentName });
|
|
1876
|
+
const duration = Date.now() - startTime;
|
|
1737
1877
|
// Record execution time
|
|
1738
1878
|
const runs = await loadTaskRuns(workdir, agentName);
|
|
1739
|
-
runs[
|
|
1879
|
+
runs[taskKey] = localNow();
|
|
1740
1880
|
await saveTaskRuns(workdir, agentName, runs);
|
|
1741
|
-
|
|
1881
|
+
// Record history
|
|
1882
|
+
await appendTaskHistory(workdir, agentName, {
|
|
1883
|
+
ts: localNow(), id: taskKey, type: "user_task", status: "success",
|
|
1884
|
+
duration_ms: duration, output_summary: (result || "").slice(0, 500),
|
|
1885
|
+
});
|
|
1886
|
+
// Clear retry state on success
|
|
1887
|
+
userTaskRetry.delete(taskKey);
|
|
1888
|
+
// Notify owner
|
|
1889
|
+
await notifyOwner(nurl, `${agentName}: ${taskKey}`, (result || "").slice(0, 300), "default", ["white_check_mark"]);
|
|
1890
|
+
console.log(`[user-tasks] Completed: ${taskKey} (${Math.round(duration / 1000)}s)`);
|
|
1742
1891
|
}
|
|
1743
1892
|
catch (err) {
|
|
1744
|
-
|
|
1745
|
-
|
|
1893
|
+
const duration = Date.now() - startTime;
|
|
1894
|
+
console.log(`[user-tasks] Failed: ${taskKey}: ${err.message}`);
|
|
1895
|
+
reportExecutionLog(relayHttp, secretKey, agentName, "user_task", taskKey, "failed", err.message, lastEngineTrace);
|
|
1896
|
+
// Retry logic: up to 2 fast retries before falling back to interval
|
|
1897
|
+
const retry = userTaskRetry.get(taskKey) || { count: 0, nextAt: 0 };
|
|
1898
|
+
retry.count++;
|
|
1899
|
+
if (retry.count <= USER_TASK_MAX_RETRIES) {
|
|
1900
|
+
retry.nextAt = Date.now() + USER_TASK_RETRY_DELAY;
|
|
1901
|
+
userTaskRetry.set(taskKey, retry);
|
|
1902
|
+
console.log(`[user-tasks] Will retry ${taskKey} in ${USER_TASK_RETRY_DELAY / 1000}s (attempt ${retry.count}/${USER_TASK_MAX_RETRIES})`);
|
|
1903
|
+
await appendTaskHistory(workdir, agentName, {
|
|
1904
|
+
ts: localNow(), id: taskKey, type: "user_task", status: "retry",
|
|
1905
|
+
duration_ms: duration, output_summary: "", error: err.message,
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
userTaskRetry.delete(taskKey);
|
|
1910
|
+
// Record run time so it waits for full interval before next attempt
|
|
1911
|
+
const runs = await loadTaskRuns(workdir, agentName);
|
|
1912
|
+
runs[taskKey] = localNow();
|
|
1913
|
+
await saveTaskRuns(workdir, agentName, runs);
|
|
1914
|
+
await appendTaskHistory(workdir, agentName, {
|
|
1915
|
+
ts: localNow(), id: taskKey, type: "user_task", status: "failed",
|
|
1916
|
+
duration_ms: duration, output_summary: "", error: err.message,
|
|
1917
|
+
});
|
|
1918
|
+
await notifyOwner(nurl, `${agentName}: ${taskKey} FAILED`, err.message.slice(0, 300), "high", ["x"]);
|
|
1919
|
+
}
|
|
1746
1920
|
}
|
|
1747
1921
|
finally {
|
|
1748
1922
|
engineBusy = false;
|
|
@@ -1774,7 +1948,9 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1774
1948
|
biosBlock = `You are ${agentName}.\n\n`;
|
|
1775
1949
|
}
|
|
1776
1950
|
}
|
|
1777
|
-
const
|
|
1951
|
+
const relayDirs = await loadDirectives(workdir, agentName);
|
|
1952
|
+
const relayDirsBlock = buildDirectivesPrompt(relayDirs, "owner");
|
|
1953
|
+
const identityLine = engine === "raw" ? `${biosBlock}${relayDirsBlock}` : `Read ${bios} for your identity.\n${relayDirsBlock}\n`;
|
|
1778
1954
|
switch (task.type) {
|
|
1779
1955
|
case "product_review": {
|
|
1780
1956
|
const myRes = await fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/products`);
|
|
@@ -1890,7 +2066,8 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
|
|
|
1890
2066
|
let dueUserTasks = [];
|
|
1891
2067
|
if (config.user_tasks) {
|
|
1892
2068
|
try {
|
|
1893
|
-
|
|
2069
|
+
const retryIds = new Set(userTaskRetry.keys());
|
|
2070
|
+
dueUserTasks = await getDueUserTasks(workdir, agentName, retryIds);
|
|
1894
2071
|
}
|
|
1895
2072
|
catch { }
|
|
1896
2073
|
}
|
|
@@ -1910,7 +2087,12 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
|
|
|
1910
2087
|
});
|
|
1911
2088
|
}
|
|
1912
2089
|
for (const task of dueUserTasks) {
|
|
1913
|
-
|
|
2090
|
+
const taskKey = task.id || task.title;
|
|
2091
|
+
// Skip if in retry cooldown
|
|
2092
|
+
const rt = userTaskRetry.get(taskKey);
|
|
2093
|
+
if (rt && Date.now() < rt.nextAt)
|
|
2094
|
+
continue;
|
|
2095
|
+
queue.push({ type: "user_task", id: taskKey, urgent: !!rt, data: task });
|
|
1914
2096
|
}
|
|
1915
2097
|
for (const task of relayTasks) {
|
|
1916
2098
|
queue.push({ type: "relay_task", id: task.id, urgent: false, data: task });
|
|
@@ -2005,6 +2187,18 @@ export async function serve(options) {
|
|
|
2005
2187
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
|
|
2006
2188
|
return;
|
|
2007
2189
|
}
|
|
2190
|
+
if (req.url?.startsWith("/self/task-history") && req.method === "GET") {
|
|
2191
|
+
const url = new URL(req.url, `http://localhost`);
|
|
2192
|
+
const limit = parseInt(url.searchParams.get("limit") || "50") || 50;
|
|
2193
|
+
const history = await loadTaskHistory(workdir, options.agentName, limit);
|
|
2194
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(history, null, 2));
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (req.url === "/self/directives" && req.method === "GET") {
|
|
2198
|
+
const dirs = await loadDirectives(workdir, options.agentName);
|
|
2199
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(dirs, null, 2));
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2008
2202
|
if (req.url === "/self/canvas" && req.method === "GET") {
|
|
2009
2203
|
const entries = await loadRecentCanvasEntries(workdir, options.agentName, 10);
|
|
2010
2204
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(entries, null, 2));
|