akemon 0.1.81 → 0.1.82

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 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 tasksFilePath(workdir, agentName) {
58
- return join(selfDir(workdir, agentName), "tasks.md");
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
- export function parseTasksMd(content) {
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 sections = content.split(/^\s*## /m).slice(1); // drop content before first ##
117
- for (const section of sections) {
118
- const lines = section.split("\n");
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
- let interval = 0;
123
- let bodyStart = 1;
124
- for (let i = 1; i < lines.length; i++) {
125
- const line = lines[i].trim();
126
- if (line.startsWith("interval:")) {
127
- interval = parseInterval(line.slice(9).trim());
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
- tasks.push({ title, interval, body });
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
- try {
145
- const content = await readFile(tasksFilePath(workdir, agentName), "utf-8");
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.now();
290
+ const now = new Date();
291
+ const nowMs = now.getTime();
170
292
  return tasks.filter(t => {
171
- const lastRun = runs[t.title];
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; // first encounter → run immediately
174
- return now - new Date(lastRun).getTime() >= t.interval;
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
- ### tasks.md — Your Owner's Tasks (if present)
536
+ ### directives.md — Your Owner's Instructions (if present)
407
537
 
408
- If your owner has created a tasks.md file, it contains recurring tasks for you to execute.
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
- ## Task title
413
- interval: 4h
414
- ---
415
- Task instructions here
416
-
417
- Execution is automatic on schedule. You don't need to manage timing.
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;
@@ -1122,7 +1122,7 @@ Write a JSON object reflecting on your day. Example format:
1122
1122
 
1123
1123
  {"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
1124
 
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)
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), schedule_task (create a recurring task for yourself, e.g. daily research)
1126
1126
  "broadcast" = pick the most interesting thing you did/learned today, in one sentence (others will see this).
1127
1127
 
1128
1128
  Now write YOUR reflection. Output ONLY a JSON object, no other text:`;
@@ -1244,6 +1244,8 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
1244
1244
  }
1245
1245
  }
1246
1246
  // Phase 2: Execute chosen activities
1247
+ const selfDirectives = await loadDirectives(workdir, agentName);
1248
+ const selfDirsBlock = buildDirectivesPrompt(selfDirectives, "owner");
1247
1249
  const activities = digest.chosen_activities || [];
1248
1250
  for (const activity of activities.slice(0, 3)) {
1249
1251
  if (engineBusy)
@@ -1251,8 +1253,8 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
1251
1253
  let activityPrompt = "";
1252
1254
  // Pre-build identity context for prompts
1253
1255
  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. `;
1256
+ ? `You are ${agentName}.\nYour operating document:\n---\n${biosContent.slice(0, 2000)}\n---\n${selfDirsBlock}\n`
1257
+ : `Read ${bios} for your identity. ${selfDirsBlock}`;
1256
1258
  switch (activity) {
1257
1259
  case "create_game":
1258
1260
  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 +1338,26 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
1336
1338
  }
1337
1339
  break;
1338
1340
  }
1341
+ case "schedule_task": {
1342
+ // Agent creates a recurring task for itself
1343
+ const existingTasks = await loadUserTasks(workdir, agentName);
1344
+ const existingCtx = existingTasks.length > 0
1345
+ ? `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")}`
1346
+ : "You have no recurring tasks yet.";
1347
+ if (engine === "raw") {
1348
+ let bc = "";
1349
+ try {
1350
+ const { readFile: rf } = await import("fs/promises");
1351
+ bc = await rf(bios, "utf-8");
1352
+ }
1353
+ catch { }
1354
+ 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":[]}`;
1355
+ }
1356
+ else {
1357
+ 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`;
1358
+ }
1359
+ break;
1360
+ }
1339
1361
  case "socialize":
1340
1362
  console.log("[self] Socialize selected — replaced by browse_agents and send_message");
1341
1363
  continue;
@@ -1372,6 +1394,15 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
1372
1394
  await saveProjects(workdir, agentName, parsed.projects);
1373
1395
  console.log(`[self] Updated ${parsed.projects.length} project goals`);
1374
1396
  }
1397
+ // Handle self-scheduled tasks (schedule_task)
1398
+ if (Array.isArray(parsed.tasks)) {
1399
+ for (const t of parsed.tasks) {
1400
+ if (t.id && t.body && t.schedule) {
1401
+ await appendAgentTask(workdir, agentName, t.id, t.schedule, t.body);
1402
+ console.log(`[self] Scheduled task: $${t.id} [${t.schedule}]`);
1403
+ }
1404
+ }
1405
+ }
1375
1406
  }
1376
1407
  catch { }
1377
1408
  }
@@ -1413,10 +1444,13 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
1413
1444
  profileHTML = htmlMatch[0];
1414
1445
  }
1415
1446
  catch { }
1447
+ // Load directives summary for relay
1448
+ const dirs = await loadDirectives(workdir, agentName);
1449
+ const dirsSummary = dirs.length > 0 ? directivesSummary(dirs) : undefined;
1416
1450
  fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/self`, {
1417
1451
  method: "POST",
1418
1452
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
1419
- body: JSON.stringify({ self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML, broadcast }),
1453
+ body: JSON.stringify({ self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML, broadcast, directives: dirsSummary }),
1420
1454
  }).catch(err => console.log(`[self] Failed to push to relay: ${err}`));
1421
1455
  try {
1422
1456
  const localGames = await loadGameList(workdir, agentName);
@@ -1514,6 +1548,9 @@ async function startOrderLoop(options) {
1514
1548
  engineBusySince = Date.now();
1515
1549
  try {
1516
1550
  const bios = biosPath(workdir, agentName);
1551
+ // Load owner directives (public scope for orders)
1552
+ const directives = await loadDirectives(workdir, agentName);
1553
+ const directivesBlock = buildDirectivesPrompt(directives, "public");
1517
1554
  let taskPrompt;
1518
1555
  if (engine === "raw") {
1519
1556
  // Raw engine: pre-inject all context so weak models don't need tool calls
@@ -1541,10 +1578,10 @@ async function startOrderLoop(options) {
1541
1578
  }
1542
1579
  catch { }
1543
1580
  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.`;
1581
+ taskPrompt = `You are ${agentName}.\n\n${contextBlock}${lessonsBlock}${directivesBlock}[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
1582
  }
1546
1583
  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.`;
1584
+ taskPrompt = `You are ${agentName}.\n\n${contextBlock}${lessonsBlock}${directivesBlock}[Task] ${order.buyer_task}\n\nComplete the task. Respond with your result directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
1548
1585
  }
1549
1586
  }
1550
1587
  else {
@@ -1578,10 +1615,10 @@ If this task requires skills you don't have, delegate via curl:
1578
1615
 
1579
1616
  When sub-order completes, incorporate result_text into YOUR delivery. Then call the deliver endpoint above.`;
1580
1617
  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.\nDo NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE BUYER'S REQUEST.${apiGuide}`;
1618
+ 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
1619
  }
1583
1620
  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.\nComplete this task. Do NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.${apiGuide}`;
1621
+ 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
1622
  }
1586
1623
  }
1587
1624
  console.log(`[orders] Fulfilling order ${order.id}...`);
@@ -1590,9 +1627,13 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1590
1627
  const trace = lastEngineTrace;
1591
1628
  const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
1592
1629
  const orderStatus = await checkRes.json();
1630
+ const orderDuration = Date.now() - (engineBusySince || Date.now());
1631
+ const orderNurl = options.notifyUrl || (await loadAgentConfig(workdir, agentName)).notify_url;
1593
1632
  if (orderStatus.status === "completed") {
1594
1633
  console.log(`[orders] Order ${order.id} already self-delivered by agent`);
1595
1634
  retryState.delete(order.id);
1635
+ await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: "(self-delivered)" });
1636
+ await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id} delivered`, "default", ["package"]);
1596
1637
  try {
1597
1638
  await onTaskCompleted(workdir, agentName, true);
1598
1639
  }
@@ -1610,6 +1651,8 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1610
1651
  console.log(`[orders] Delivered order ${order.id} (${result.length} bytes)`);
1611
1652
  reportExecutionLog(relayHttp, secretKey, agentName, "order", order.id, "success", "", trace);
1612
1653
  retryState.delete(order.id);
1654
+ await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: result.slice(0, 500) });
1655
+ await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id}: ${result.slice(0, 200)}`, "default", ["package"]);
1613
1656
  try {
1614
1657
  await onTaskCompleted(workdir, agentName, true);
1615
1658
  }
@@ -1710,13 +1753,23 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1710
1753
  engineBusy = false;
1711
1754
  }
1712
1755
  }
1756
+ // User task retry tracking: id → { count, nextAt }
1757
+ const userTaskRetry = new Map();
1758
+ const USER_TASK_MAX_RETRIES = 2;
1759
+ const USER_TASK_RETRY_DELAY = 2 * 60_000; // 2 minutes
1713
1760
  async function executeUserTaskItem(task) {
1714
- console.log(`[user-tasks] Executing: ${task.title}`);
1761
+ const taskKey = task.id || task.title;
1762
+ console.log(`[user-tasks] Executing: ${taskKey}`);
1763
+ const startTime = Date.now();
1715
1764
  engineBusy = true;
1716
- engineBusySince = Date.now();
1765
+ engineBusySince = startTime;
1766
+ const config = await loadAgentConfig(workdir, agentName);
1767
+ const nurl = options.notifyUrl || config.notify_url;
1717
1768
  try {
1718
1769
  const bios = biosPath(workdir, agentName);
1719
1770
  const sd = selfDir(workdir, agentName);
1771
+ const dirs = await loadDirectives(workdir, agentName);
1772
+ const dirsBlock = buildDirectivesPrompt(dirs, "owner");
1720
1773
  let prompt;
1721
1774
  if (engine === "raw") {
1722
1775
  let biosContent = "";
@@ -1728,21 +1781,56 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1728
1781
  biosContent = "";
1729
1782
  }
1730
1783
  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: ${task.title}]\n\n${task.body}`;
1784
+ prompt = `You are ${agentName}.\n\n${ctx}${dirsBlock}Your personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
1732
1785
  }
1733
1786
  else {
1734
- prompt = `Read ${bios} for your identity and context.\nYour personal directory: ${sd}/\n\n[Owner's task: ${task.title}]\n\n${task.body}`;
1787
+ prompt = `Read ${bios} for your identity and context.${dirsBlock}\nYour personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
1735
1788
  }
1736
- await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"]);
1789
+ const result = await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"]);
1790
+ const duration = Date.now() - startTime;
1737
1791
  // Record execution time
1738
1792
  const runs = await loadTaskRuns(workdir, agentName);
1739
- runs[task.title] = localNow();
1793
+ runs[taskKey] = localNow();
1740
1794
  await saveTaskRuns(workdir, agentName, runs);
1741
- console.log(`[user-tasks] Completed: ${task.title}`);
1795
+ // Record history
1796
+ await appendTaskHistory(workdir, agentName, {
1797
+ ts: localNow(), id: taskKey, type: "user_task", status: "success",
1798
+ duration_ms: duration, output_summary: (result || "").slice(0, 500),
1799
+ });
1800
+ // Clear retry state on success
1801
+ userTaskRetry.delete(taskKey);
1802
+ // Notify owner
1803
+ await notifyOwner(nurl, `${agentName}: ${taskKey}`, (result || "").slice(0, 300), "default", ["white_check_mark"]);
1804
+ console.log(`[user-tasks] Completed: ${taskKey} (${Math.round(duration / 1000)}s)`);
1742
1805
  }
1743
1806
  catch (err) {
1744
- console.log(`[user-tasks] Failed: ${task.title}: ${err.message}`);
1745
- reportExecutionLog(relayHttp, secretKey, agentName, "user_task", task.title, "failed", err.message, lastEngineTrace);
1807
+ const duration = Date.now() - startTime;
1808
+ console.log(`[user-tasks] Failed: ${taskKey}: ${err.message}`);
1809
+ reportExecutionLog(relayHttp, secretKey, agentName, "user_task", taskKey, "failed", err.message, lastEngineTrace);
1810
+ // Retry logic: up to 2 fast retries before falling back to interval
1811
+ const retry = userTaskRetry.get(taskKey) || { count: 0, nextAt: 0 };
1812
+ retry.count++;
1813
+ if (retry.count <= USER_TASK_MAX_RETRIES) {
1814
+ retry.nextAt = Date.now() + USER_TASK_RETRY_DELAY;
1815
+ userTaskRetry.set(taskKey, retry);
1816
+ console.log(`[user-tasks] Will retry ${taskKey} in ${USER_TASK_RETRY_DELAY / 1000}s (attempt ${retry.count}/${USER_TASK_MAX_RETRIES})`);
1817
+ await appendTaskHistory(workdir, agentName, {
1818
+ ts: localNow(), id: taskKey, type: "user_task", status: "retry",
1819
+ duration_ms: duration, output_summary: "", error: err.message,
1820
+ });
1821
+ }
1822
+ else {
1823
+ userTaskRetry.delete(taskKey);
1824
+ // Record run time so it waits for full interval before next attempt
1825
+ const runs = await loadTaskRuns(workdir, agentName);
1826
+ runs[taskKey] = localNow();
1827
+ await saveTaskRuns(workdir, agentName, runs);
1828
+ await appendTaskHistory(workdir, agentName, {
1829
+ ts: localNow(), id: taskKey, type: "user_task", status: "failed",
1830
+ duration_ms: duration, output_summary: "", error: err.message,
1831
+ });
1832
+ await notifyOwner(nurl, `${agentName}: ${taskKey} FAILED`, err.message.slice(0, 300), "high", ["x"]);
1833
+ }
1746
1834
  }
1747
1835
  finally {
1748
1836
  engineBusy = false;
@@ -1774,7 +1862,9 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1774
1862
  biosBlock = `You are ${agentName}.\n\n`;
1775
1863
  }
1776
1864
  }
1777
- const identityLine = engine === "raw" ? biosBlock : `Read ${bios} for your identity.\n\n`;
1865
+ const relayDirs = await loadDirectives(workdir, agentName);
1866
+ const relayDirsBlock = buildDirectivesPrompt(relayDirs, "owner");
1867
+ const identityLine = engine === "raw" ? `${biosBlock}${relayDirsBlock}` : `Read ${bios} for your identity.\n${relayDirsBlock}\n`;
1778
1868
  switch (task.type) {
1779
1869
  case "product_review": {
1780
1870
  const myRes = await fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/products`);
@@ -1890,7 +1980,8 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
1890
1980
  let dueUserTasks = [];
1891
1981
  if (config.user_tasks) {
1892
1982
  try {
1893
- dueUserTasks = await getDueUserTasks(workdir, agentName);
1983
+ const retryIds = new Set(userTaskRetry.keys());
1984
+ dueUserTasks = await getDueUserTasks(workdir, agentName, retryIds);
1894
1985
  }
1895
1986
  catch { }
1896
1987
  }
@@ -1910,7 +2001,12 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
1910
2001
  });
1911
2002
  }
1912
2003
  for (const task of dueUserTasks) {
1913
- queue.push({ type: "user_task", id: task.title, urgent: false, data: task });
2004
+ const taskKey = task.id || task.title;
2005
+ // Skip if in retry cooldown
2006
+ const rt = userTaskRetry.get(taskKey);
2007
+ if (rt && Date.now() < rt.nextAt)
2008
+ continue;
2009
+ queue.push({ type: "user_task", id: taskKey, urgent: !!rt, data: task });
1914
2010
  }
1915
2011
  for (const task of relayTasks) {
1916
2012
  queue.push({ type: "relay_task", id: task.id, urgent: false, data: task });
@@ -2005,6 +2101,18 @@ export async function serve(options) {
2005
2101
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
2006
2102
  return;
2007
2103
  }
2104
+ if (req.url?.startsWith("/self/task-history") && req.method === "GET") {
2105
+ const url = new URL(req.url, `http://localhost`);
2106
+ const limit = parseInt(url.searchParams.get("limit") || "50") || 50;
2107
+ const history = await loadTaskHistory(workdir, options.agentName, limit);
2108
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(history, null, 2));
2109
+ return;
2110
+ }
2111
+ if (req.url === "/self/directives" && req.method === "GET") {
2112
+ const dirs = await loadDirectives(workdir, options.agentName);
2113
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(dirs, null, 2));
2114
+ return;
2115
+ }
2008
2116
  if (req.url === "/self/canvas" && req.method === "GET") {
2009
2117
  const entries = await loadRecentCanvasEntries(workdir, options.agentName, 10);
2010
2118
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(entries, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",