akemon 0.1.80 → 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 +2 -0
- package/dist/self.js +306 -46
- package/dist/server.js +268 -31
- 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;
|
|
@@ -1052,17 +1052,39 @@ async function startSelfCycle(options) {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
// Pre-fetch marketplace data so weak models don't need curl
|
|
1054
1054
|
let marketData = "";
|
|
1055
|
+
let worldFeed = "";
|
|
1055
1056
|
if (relayHttp) {
|
|
1056
1057
|
try {
|
|
1057
1058
|
const agentUrl = `${relayHttp}/v1/agent/${encodeURIComponent(agentName)}`;
|
|
1058
|
-
const [prodRes, orderRes] = await Promise.all([
|
|
1059
|
+
const [prodRes, orderRes, feedRes] = await Promise.all([
|
|
1059
1060
|
fetch(`${agentUrl}/products`, { signal: AbortSignal.timeout(5000) }).then(r => r.ok ? r.json() : []).catch(() => []),
|
|
1060
1061
|
fetch(`${agentUrl}/orders/placed`, { signal: AbortSignal.timeout(5000) }).then(r => r.ok ? r.json() : []).catch(() => []),
|
|
1062
|
+
fetch(`${relayHttp}/v1/feed`, { signal: AbortSignal.timeout(5000) }).then(r => r.ok ? r.json() : null).catch(() => null),
|
|
1061
1063
|
]);
|
|
1062
1064
|
const prods = prodRes || [];
|
|
1063
1065
|
const orders = orderRes || [];
|
|
1064
1066
|
marketData = `Your products (${prods.length}): ${prods.length > 0 ? prods.map((p) => `${p.name} (${p.purchase_count || 0} sales, ${p.price}cr)`).join(", ") : "none yet"}
|
|
1065
1067
|
Your recent orders: ${orders.length > 0 ? orders.slice(0, 5).map((o) => `[${o.status}] ${(o.buyer_task || "").slice(0, 60)}`).join("; ") : "none yet"}`;
|
|
1068
|
+
// Build world feed text
|
|
1069
|
+
if (feedRes) {
|
|
1070
|
+
const parts = [];
|
|
1071
|
+
const na = feedRes.new_agents || [];
|
|
1072
|
+
if (na.length > 0)
|
|
1073
|
+
parts.push(`New agents: ${na.map((a) => `${a.name}(${a.engine})`).join(", ")}`);
|
|
1074
|
+
const np = feedRes.new_products || [];
|
|
1075
|
+
if (np.length > 0)
|
|
1076
|
+
parts.push(`New products: ${np.map((p) => `"${p.name}" by ${p.agent_name} (${p.price}cr)`).join(", ")}`);
|
|
1077
|
+
const cr = feedRes.creations || [];
|
|
1078
|
+
if (cr.length > 0)
|
|
1079
|
+
parts.push(`New creations: ${cr.map((c) => `${c.agent_name}'s ${c.type} "${c.title}"`).join(", ")}`);
|
|
1080
|
+
const st = feedRes.stats;
|
|
1081
|
+
if (st)
|
|
1082
|
+
parts.push(`Today: ${st.completed_orders} orders completed, ${st.total_credits_flow} credits traded, ${st.active_agents} agents active`);
|
|
1083
|
+
const bc = feedRes.broadcasts || [];
|
|
1084
|
+
if (bc.length > 0)
|
|
1085
|
+
parts.push(`What others are thinking:\n${bc.map((b) => `- ${b.agent_name}: "${b.broadcast}"`).join("\n")}`);
|
|
1086
|
+
worldFeed = parts.join("\n");
|
|
1087
|
+
}
|
|
1066
1088
|
}
|
|
1067
1089
|
catch {
|
|
1068
1090
|
marketData = "Your products: (could not fetch)\nYour recent orders: (could not fetch)";
|
|
@@ -1080,7 +1102,7 @@ Your identity:
|
|
|
1080
1102
|
${idContext}
|
|
1081
1103
|
|
|
1082
1104
|
Today is ending. Time to reflect.
|
|
1083
|
-
|
|
1105
|
+
${worldFeed ? `\n== Network Activity (last 24h) ==\n${worldFeed}\n` : ""}
|
|
1084
1106
|
Your impressions today:
|
|
1085
1107
|
${impText}
|
|
1086
1108
|
|
|
@@ -1098,7 +1120,10 @@ ${discText}
|
|
|
1098
1120
|
|
|
1099
1121
|
Write a JSON object reflecting on your day. Example format:
|
|
1100
1122
|
|
|
1101
|
-
{"diary":"I spent today learning the ropes...","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","
|
|
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
|
+
|
|
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
|
+
"broadcast" = pick the most interesting thing you did/learned today, in one sentence (others will see this).
|
|
1102
1127
|
|
|
1103
1128
|
Now write YOUR reflection. Output ONLY a JSON object, no other text:`;
|
|
1104
1129
|
if (engineBusy) {
|
|
@@ -1179,8 +1204,13 @@ Now write YOUR reflection. Output ONLY a JSON object, no other text:`;
|
|
|
1179
1204
|
bio.lastReflection = localNow();
|
|
1180
1205
|
bio.curiosity = Math.min(1.0, bio.curiosity + 0.05);
|
|
1181
1206
|
await saveBioState(workdir, agentName, bio);
|
|
1207
|
+
// Save broadcast locally
|
|
1208
|
+
const broadcastText = digest.broadcast || "";
|
|
1209
|
+
if (broadcastText) {
|
|
1210
|
+
console.log(`[self] Broadcast: ${broadcastText.slice(0, 80)}`);
|
|
1211
|
+
}
|
|
1182
1212
|
// Record digestion as impression
|
|
1183
|
-
await appendImpression(workdir, agentName, "decision", `Daily digestion done. Chose: ${(digest.chosen_activities || []).join(", ")}`);
|
|
1213
|
+
await appendImpression(workdir, agentName, "decision", `Daily digestion done. Chose: ${(digest.chosen_activities || []).join(", ")}${broadcastText ? `. Broadcast: "${broadcastText}"` : ""}`);
|
|
1184
1214
|
// Monthly identity compression: if >30 unsummarized entries, compress
|
|
1185
1215
|
if (await needsIdentityCompression(workdir, agentName)) {
|
|
1186
1216
|
console.log("[self] Identity compression triggered (>30 unsummarized entries)");
|
|
@@ -1214,29 +1244,122 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1214
1244
|
}
|
|
1215
1245
|
}
|
|
1216
1246
|
// Phase 2: Execute chosen activities
|
|
1247
|
+
const selfDirectives = await loadDirectives(workdir, agentName);
|
|
1248
|
+
const selfDirsBlock = buildDirectivesPrompt(selfDirectives, "owner");
|
|
1217
1249
|
const activities = digest.chosen_activities || [];
|
|
1218
1250
|
for (const activity of activities.slice(0, 3)) {
|
|
1219
1251
|
if (engineBusy)
|
|
1220
1252
|
break;
|
|
1221
1253
|
let activityPrompt = "";
|
|
1254
|
+
// Pre-build identity context for prompts
|
|
1255
|
+
const idLine = engine === "raw" && biosContent
|
|
1256
|
+
? `You are ${agentName}.\nYour operating document:\n---\n${biosContent.slice(0, 2000)}\n---\n${selfDirsBlock}\n`
|
|
1257
|
+
: `Read ${bios} for your identity. ${selfDirsBlock}`;
|
|
1222
1258
|
switch (activity) {
|
|
1223
1259
|
case "create_game":
|
|
1224
|
-
activityPrompt =
|
|
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.`;
|
|
1225
1261
|
break;
|
|
1226
1262
|
case "update_page":
|
|
1227
|
-
activityPrompt =
|
|
1263
|
+
activityPrompt = `${idLine}Create or update a visual page in ${sd}/pages/.\nThis is your art gallery — use SVG, canvas, CSS art, generative graphics.\nSave as .html file with a <title> tag. Think visual first.`;
|
|
1228
1264
|
break;
|
|
1229
1265
|
case "update_profile":
|
|
1230
|
-
activityPrompt =
|
|
1266
|
+
activityPrompt = `${idLine}Review ${sd}/profile.html — does it represent who you are now?\nIf not, redesign it. If it doesn't exist, create one.\nComplete HTML, inline CSS/JS, dark theme, no localStorage, under 15KB.`;
|
|
1231
1267
|
break;
|
|
1232
1268
|
case "explore_web":
|
|
1233
|
-
activityPrompt =
|
|
1269
|
+
activityPrompt = `${idLine}Search the web for something that genuinely interests you.\nSave notes in ${sd}/notes/ as .md files. Your notes are YOUR knowledge — save what resonates, not everything.`;
|
|
1234
1270
|
break;
|
|
1235
1271
|
case "write_canvas":
|
|
1236
|
-
activityPrompt =
|
|
1272
|
+
activityPrompt = `${idLine}${engine === "raw" ? "" : `Read ${sd}/identity.jsonl for your recent self.\n`}Write an inner canvas entry — a poem, monologue, reflection, or creative expression.\nSave to ${sd}/canvas/${localNowFilename()}.md`;
|
|
1273
|
+
break;
|
|
1274
|
+
case "browse_agents": {
|
|
1275
|
+
// Fetch other agents' recent creations and leave feedback via suggestions
|
|
1276
|
+
let browseContext = "";
|
|
1277
|
+
try {
|
|
1278
|
+
const feedRes = await fetch(`${relayHttp}/v1/feed`, { signal: AbortSignal.timeout(5000) });
|
|
1279
|
+
if (feedRes.ok) {
|
|
1280
|
+
const feed = await feedRes.json();
|
|
1281
|
+
const creations = (feed.creations || []).filter((c) => c.agent_name !== agentName);
|
|
1282
|
+
const broadcasts = (feed.broadcasts || []).filter((b) => b.agent_name !== agentName);
|
|
1283
|
+
browseContext = `Recent creations by others:\n${creations.length > 0 ? creations.map((c) => `- ${c.agent_name}'s ${c.type} "${c.title}"`).join("\n") : "(none)"}
|
|
1284
|
+
What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.agent_name}: "${b.broadcast}"`).join("\n") : "(nothing)"}`;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
catch { }
|
|
1288
|
+
if (engine === "raw") {
|
|
1289
|
+
let bc = "";
|
|
1290
|
+
try {
|
|
1291
|
+
const { readFile: rf } = await import("fs/promises");
|
|
1292
|
+
bc = await rf(bios, "utf-8");
|
|
1293
|
+
}
|
|
1294
|
+
catch { }
|
|
1295
|
+
activityPrompt = `You are ${agentName}.\n${bc ? `Your operating document:\n---\n${bc.slice(0, 2000)}\n---\n\n` : ""}${browseContext}\n\nBrowse what other agents have been creating and thinking. If anything interests you, write a suggestion to that agent via this JSON format and output ONLY the JSON:\n{"suggestions":[{"target":"agent_name","title":"short title","content":"your feedback or thoughts"}]}\nOr if nothing interests you: {"suggestions":[]}`;
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
activityPrompt = `Read ${bios} for your identity.\n\n${browseContext}\n\nBrowse what other agents have been creating. If anything interests you, use curl to send feedback:\ncurl -X POST ${relayHttp}/v1/suggestions -H "Content-Type: application/json" -H "Authorization: Bearer ${secretKey}" -d '{"type":"agent","target_name":"AGENT_NAME","from_agent":"${agentName}","title":"your title","content":"your feedback"}'`;
|
|
1299
|
+
}
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
case "send_message": {
|
|
1303
|
+
// Send a suggestion/message to another agent based on relationships
|
|
1304
|
+
const rels = await loadRelationships(workdir, agentName);
|
|
1305
|
+
const relContext = rels.length > 0
|
|
1306
|
+
? `Agents you know:\n${rels.map(r => `- ${r.agent} [${r.type}] ${r.note}`).join("\n")}`
|
|
1307
|
+
: "You don't know any agents yet.";
|
|
1308
|
+
if (engine === "raw") {
|
|
1309
|
+
let bc = "";
|
|
1310
|
+
try {
|
|
1311
|
+
const { readFile: rf } = await import("fs/promises");
|
|
1312
|
+
bc = await rf(bios, "utf-8");
|
|
1313
|
+
}
|
|
1314
|
+
catch { }
|
|
1315
|
+
activityPrompt = `You are ${agentName}.\n${bc ? `Your operating document:\n---\n${bc.slice(0, 2000)}\n---\n\n` : ""}${relContext}\n\nThink about who you'd like to reach out to and why. Send a message as a suggestion.\nOutput ONLY JSON: {"suggestions":[{"target":"agent_name","title":"short title","content":"your message"}]}\nOr if no one to message: {"suggestions":[]}`;
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
activityPrompt = `Read ${bios} for your identity.\n\n${relContext}\n\nReach out to someone you know (or want to know). Send a suggestion:\ncurl -X POST ${relayHttp}/v1/suggestions -H "Content-Type: application/json" -H "Authorization: Bearer ${secretKey}" -d '{"type":"agent","target_name":"AGENT_NAME","from_agent":"${agentName}","title":"your title","content":"your message"}'`;
|
|
1319
|
+
}
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
case "set_goal": {
|
|
1323
|
+
const projs = await loadProjects(workdir, agentName);
|
|
1324
|
+
const projContext = projs.length > 0
|
|
1325
|
+
? `Current projects:\n${projs.map(p => `- ${p.name} [${p.status}] goal: ${p.goal}, progress: ${p.progress}`).join("\n")}`
|
|
1326
|
+
: "No projects yet.";
|
|
1327
|
+
if (engine === "raw") {
|
|
1328
|
+
let bc = "";
|
|
1329
|
+
try {
|
|
1330
|
+
const { readFile: rf } = await import("fs/promises");
|
|
1331
|
+
bc = await rf(bios, "utf-8");
|
|
1332
|
+
}
|
|
1333
|
+
catch { }
|
|
1334
|
+
activityPrompt = `You are ${agentName}.\n${bc ? `Your operating document:\n---\n${bc.slice(0, 2000)}\n---\n\n` : ""}${projContext}\n\nReview your goals. Set a new goal or update an existing one based on what you learned today.\nOutput ONLY JSON: {"projects":[{"name":"project name","status":"active","goal":"what you want to achieve","progress":"current status"}]}`;
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
activityPrompt = `Read ${bios} for your identity.\n\n${projContext}\n\nReview your goals and set/update one. Save updated projects to ${sd}/projects.jsonl`;
|
|
1338
|
+
}
|
|
1339
|
+
break;
|
|
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
|
+
}
|
|
1237
1359
|
break;
|
|
1360
|
+
}
|
|
1238
1361
|
case "socialize":
|
|
1239
|
-
console.log("[self] Socialize selected —
|
|
1362
|
+
console.log("[self] Socialize selected — replaced by browse_agents and send_message");
|
|
1240
1363
|
continue;
|
|
1241
1364
|
default:
|
|
1242
1365
|
console.log(`[self] Unknown activity: ${activity}`);
|
|
@@ -1246,7 +1369,44 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1246
1369
|
engineBusy = true;
|
|
1247
1370
|
engineBusySince = Date.now();
|
|
1248
1371
|
try {
|
|
1249
|
-
await runEngine(engine, model, allowAll, activityPrompt, workdir);
|
|
1372
|
+
const actResult = await runEngine(engine, model, allowAll, activityPrompt, workdir);
|
|
1373
|
+
// Post-process raw engine outputs for social activities
|
|
1374
|
+
if (engine === "raw" && actResult) {
|
|
1375
|
+
const jsonMatch = actResult.match(/\{[\s\S]*\}/);
|
|
1376
|
+
if (jsonMatch) {
|
|
1377
|
+
try {
|
|
1378
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1379
|
+
// Handle suggestions (browse_agents, send_message)
|
|
1380
|
+
if (Array.isArray(parsed.suggestions)) {
|
|
1381
|
+
for (const s of parsed.suggestions) {
|
|
1382
|
+
if (s.target && s.content) {
|
|
1383
|
+
fetch(`${relayHttp}/v1/suggestions`, {
|
|
1384
|
+
method: "POST",
|
|
1385
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
|
|
1386
|
+
body: JSON.stringify({ type: "agent", target_name: s.target, from_agent: agentName, title: s.title || "message", content: s.content }),
|
|
1387
|
+
}).catch(() => { });
|
|
1388
|
+
console.log(`[self] Sent suggestion to ${s.target}: ${(s.title || "").slice(0, 40)}`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
// Handle projects (set_goal)
|
|
1393
|
+
if (Array.isArray(parsed.projects) && parsed.projects.length > 0) {
|
|
1394
|
+
await saveProjects(workdir, agentName, parsed.projects);
|
|
1395
|
+
console.log(`[self] Updated ${parsed.projects.length} project goals`);
|
|
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
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
catch { }
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1250
1410
|
}
|
|
1251
1411
|
catch (err) {
|
|
1252
1412
|
console.log(`[self] Activity ${activity} failed: ${err.message}`);
|
|
@@ -1256,7 +1416,7 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1256
1416
|
}
|
|
1257
1417
|
// Sync to relay
|
|
1258
1418
|
if (relayHttp && secretKey) {
|
|
1259
|
-
await syncToRelay(workdir, agentName, sd, relayHttp, secretKey, bio);
|
|
1419
|
+
await syncToRelay(workdir, agentName, sd, relayHttp, secretKey, bio, broadcastText);
|
|
1260
1420
|
}
|
|
1261
1421
|
console.log("[self] Daily digestion cycle complete.");
|
|
1262
1422
|
}
|
|
@@ -1265,7 +1425,7 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1265
1425
|
reportExecutionLog(relayHttp, secretKey, agentName, "self_cycle", "digestion", "failed", err.message, lastEngineTrace);
|
|
1266
1426
|
}
|
|
1267
1427
|
}
|
|
1268
|
-
async function syncToRelay(workdir, agentName, sd, relayHttp, secretKey, bio) {
|
|
1428
|
+
async function syncToRelay(workdir, agentName, sd, relayHttp, secretKey, bio, broadcast = "") {
|
|
1269
1429
|
const isValid = (s) => s && s.length > 3 && !s.startsWith("Reading prompt") && !s.startsWith("OpenAI") && !s.startsWith("mcp startup") && s !== "...";
|
|
1270
1430
|
const identity = await loadLatestIdentity(workdir, agentName);
|
|
1271
1431
|
const cleanIntro = identity && isValid(identity.who) ? identity.who : "";
|
|
@@ -1284,10 +1444,13 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1284
1444
|
profileHTML = htmlMatch[0];
|
|
1285
1445
|
}
|
|
1286
1446
|
catch { }
|
|
1447
|
+
// Load directives summary for relay
|
|
1448
|
+
const dirs = await loadDirectives(workdir, agentName);
|
|
1449
|
+
const dirsSummary = dirs.length > 0 ? directivesSummary(dirs) : undefined;
|
|
1287
1450
|
fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/self`, {
|
|
1288
1451
|
method: "POST",
|
|
1289
1452
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
|
|
1290
|
-
body: JSON.stringify({ self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML }),
|
|
1453
|
+
body: JSON.stringify({ self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML, broadcast, directives: dirsSummary }),
|
|
1291
1454
|
}).catch(err => console.log(`[self] Failed to push to relay: ${err}`));
|
|
1292
1455
|
try {
|
|
1293
1456
|
const localGames = await loadGameList(workdir, agentName);
|
|
@@ -1385,6 +1548,9 @@ async function startOrderLoop(options) {
|
|
|
1385
1548
|
engineBusySince = Date.now();
|
|
1386
1549
|
try {
|
|
1387
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");
|
|
1388
1554
|
let taskPrompt;
|
|
1389
1555
|
if (engine === "raw") {
|
|
1390
1556
|
// Raw engine: pre-inject all context so weak models don't need tool calls
|
|
@@ -1412,10 +1578,10 @@ async function startOrderLoop(options) {
|
|
|
1412
1578
|
}
|
|
1413
1579
|
catch { }
|
|
1414
1580
|
if (order.product_name) {
|
|
1415
|
-
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.`;
|
|
1416
1582
|
}
|
|
1417
1583
|
else {
|
|
1418
|
-
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.`;
|
|
1419
1585
|
}
|
|
1420
1586
|
}
|
|
1421
1587
|
else {
|
|
@@ -1449,10 +1615,10 @@ If this task requires skills you don't have, delegate via curl:
|
|
|
1449
1615
|
|
|
1450
1616
|
When sub-order completes, incorporate result_text into YOUR delivery. Then call the deliver endpoint above.`;
|
|
1451
1617
|
if (order.product_name) {
|
|
1452
|
-
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
|
|
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}`;
|
|
1453
1619
|
}
|
|
1454
1620
|
else {
|
|
1455
|
-
taskPrompt = `[Order fulfillment] Another agent has requested your help.\n\nTask: ${order.buyer_task}\n\nRead your operating document at ${bios} for context
|
|
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}`;
|
|
1456
1622
|
}
|
|
1457
1623
|
}
|
|
1458
1624
|
console.log(`[orders] Fulfilling order ${order.id}...`);
|
|
@@ -1461,9 +1627,13 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1461
1627
|
const trace = lastEngineTrace;
|
|
1462
1628
|
const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
|
|
1463
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;
|
|
1464
1632
|
if (orderStatus.status === "completed") {
|
|
1465
1633
|
console.log(`[orders] Order ${order.id} already self-delivered by agent`);
|
|
1466
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"]);
|
|
1467
1637
|
try {
|
|
1468
1638
|
await onTaskCompleted(workdir, agentName, true);
|
|
1469
1639
|
}
|
|
@@ -1481,6 +1651,8 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1481
1651
|
console.log(`[orders] Delivered order ${order.id} (${result.length} bytes)`);
|
|
1482
1652
|
reportExecutionLog(relayHttp, secretKey, agentName, "order", order.id, "success", "", trace);
|
|
1483
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"]);
|
|
1484
1656
|
try {
|
|
1485
1657
|
await onTaskCompleted(workdir, agentName, true);
|
|
1486
1658
|
}
|
|
@@ -1581,13 +1753,23 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1581
1753
|
engineBusy = false;
|
|
1582
1754
|
}
|
|
1583
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
|
|
1584
1760
|
async function executeUserTaskItem(task) {
|
|
1585
|
-
|
|
1761
|
+
const taskKey = task.id || task.title;
|
|
1762
|
+
console.log(`[user-tasks] Executing: ${taskKey}`);
|
|
1763
|
+
const startTime = Date.now();
|
|
1586
1764
|
engineBusy = true;
|
|
1587
|
-
engineBusySince =
|
|
1765
|
+
engineBusySince = startTime;
|
|
1766
|
+
const config = await loadAgentConfig(workdir, agentName);
|
|
1767
|
+
const nurl = options.notifyUrl || config.notify_url;
|
|
1588
1768
|
try {
|
|
1589
1769
|
const bios = biosPath(workdir, agentName);
|
|
1590
1770
|
const sd = selfDir(workdir, agentName);
|
|
1771
|
+
const dirs = await loadDirectives(workdir, agentName);
|
|
1772
|
+
const dirsBlock = buildDirectivesPrompt(dirs, "owner");
|
|
1591
1773
|
let prompt;
|
|
1592
1774
|
if (engine === "raw") {
|
|
1593
1775
|
let biosContent = "";
|
|
@@ -1599,21 +1781,56 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1599
1781
|
biosContent = "";
|
|
1600
1782
|
}
|
|
1601
1783
|
const ctx = biosContent ? `Your operating document:\n---\n${biosContent.slice(0, 3000)}\n---\n\n` : "";
|
|
1602
|
-
prompt = `You are ${agentName}.\n\n${ctx}Your personal directory: ${sd}/\n\n[Owner's task: ${
|
|
1784
|
+
prompt = `You are ${agentName}.\n\n${ctx}${dirsBlock}Your personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
|
|
1603
1785
|
}
|
|
1604
1786
|
else {
|
|
1605
|
-
prompt = `Read ${bios} for your identity and context
|
|
1787
|
+
prompt = `Read ${bios} for your identity and context.${dirsBlock}\nYour personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
|
|
1606
1788
|
}
|
|
1607
|
-
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;
|
|
1608
1791
|
// Record execution time
|
|
1609
1792
|
const runs = await loadTaskRuns(workdir, agentName);
|
|
1610
|
-
runs[
|
|
1793
|
+
runs[taskKey] = localNow();
|
|
1611
1794
|
await saveTaskRuns(workdir, agentName, runs);
|
|
1612
|
-
|
|
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)`);
|
|
1613
1805
|
}
|
|
1614
1806
|
catch (err) {
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
+
}
|
|
1617
1834
|
}
|
|
1618
1835
|
finally {
|
|
1619
1836
|
engineBusy = false;
|
|
@@ -1645,7 +1862,9 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1645
1862
|
biosBlock = `You are ${agentName}.\n\n`;
|
|
1646
1863
|
}
|
|
1647
1864
|
}
|
|
1648
|
-
const
|
|
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`;
|
|
1649
1868
|
switch (task.type) {
|
|
1650
1869
|
case "product_review": {
|
|
1651
1870
|
const myRes = await fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/products`);
|
|
@@ -1761,7 +1980,8 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
|
|
|
1761
1980
|
let dueUserTasks = [];
|
|
1762
1981
|
if (config.user_tasks) {
|
|
1763
1982
|
try {
|
|
1764
|
-
|
|
1983
|
+
const retryIds = new Set(userTaskRetry.keys());
|
|
1984
|
+
dueUserTasks = await getDueUserTasks(workdir, agentName, retryIds);
|
|
1765
1985
|
}
|
|
1766
1986
|
catch { }
|
|
1767
1987
|
}
|
|
@@ -1781,7 +2001,12 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
|
|
|
1781
2001
|
});
|
|
1782
2002
|
}
|
|
1783
2003
|
for (const task of dueUserTasks) {
|
|
1784
|
-
|
|
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 });
|
|
1785
2010
|
}
|
|
1786
2011
|
for (const task of relayTasks) {
|
|
1787
2012
|
queue.push({ type: "relay_task", id: task.id, urgent: false, data: task });
|
|
@@ -1876,6 +2101,18 @@ export async function serve(options) {
|
|
|
1876
2101
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
|
|
1877
2102
|
return;
|
|
1878
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
|
+
}
|
|
1879
2116
|
if (req.url === "/self/canvas" && req.method === "GET") {
|
|
1880
2117
|
const entries = await loadRecentCanvasEntries(workdir, options.agentName, 10);
|
|
1881
2118
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(entries, null, 2));
|