akemon 0.2.19 → 0.2.21

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.
@@ -0,0 +1,286 @@
1
+ /**
2
+ * RoleModule — agent identity and expertise system.
3
+ *
4
+ * Role = who you are (tone, privacy boundary)
5
+ * Playbook = what you know (domain strategy, toolchain)
6
+ * Product = what you sell (specific offering, references a playbook)
7
+ *
8
+ * All three are orthogonal and decoupled.
9
+ * Core functions are exported for TaskModule to call directly (pure-function style).
10
+ */
11
+ import { readdir, readFile, mkdir, writeFile } from "fs/promises";
12
+ import { join, basename } from "path";
13
+ import { rolesDir, playbooksDir, productsDir } from "./self.js";
14
+ // ---------------------------------------------------------------------------
15
+ // Parsing
16
+ // ---------------------------------------------------------------------------
17
+ function parseRole(name, raw) {
18
+ const lines = raw.split("\n");
19
+ let description = "";
20
+ const triggers = [];
21
+ const include = [];
22
+ const exclude = [];
23
+ const customLines = [];
24
+ let section = ""; // current ## section name
25
+ let pastTitle = false;
26
+ for (const line of lines) {
27
+ // Track ## sections
28
+ if (line.startsWith("## ")) {
29
+ const heading = line.slice(3).trim().toLowerCase();
30
+ if (heading.includes("激活") || heading === "triggers") {
31
+ section = "triggers";
32
+ }
33
+ else if (heading.includes("上下文") || heading.includes("context")) {
34
+ section = "context";
35
+ }
36
+ else {
37
+ section = "custom";
38
+ }
39
+ continue;
40
+ }
41
+ // Skip # title
42
+ if (line.startsWith("# ")) {
43
+ pastTitle = true;
44
+ continue;
45
+ }
46
+ // Extract description: first non-empty line after title, before any ##
47
+ if (pastTitle && !description && !section && line.trim()) {
48
+ description = line.trim();
49
+ continue;
50
+ }
51
+ const trimmed = line.trim();
52
+ if (!trimmed)
53
+ continue;
54
+ if (section === "triggers") {
55
+ // Extract trigger:xxx from list items
56
+ const match = trimmed.match(/^-\s*(trigger:\S+)/);
57
+ if (match)
58
+ triggers.push(match[1]);
59
+ }
60
+ else if (section === "context") {
61
+ // Parse include/exclude lines
62
+ if (trimmed.toLowerCase().startsWith("include:")) {
63
+ include.push(...trimmed.slice(8).split(",").map(s => s.trim()).filter(Boolean));
64
+ }
65
+ else if (trimmed.toLowerCase().startsWith("exclude:")) {
66
+ exclude.push(...trimmed.slice(8).split(",").map(s => s.trim()).filter(Boolean));
67
+ }
68
+ }
69
+ else if (section === "custom") {
70
+ customLines.push(line);
71
+ }
72
+ }
73
+ return { name, description, triggers, include, exclude, customRules: customLines.join("\n").trim(), raw };
74
+ }
75
+ function parseProduct(name, raw) {
76
+ let playbook = "";
77
+ const lines = raw.split("\n");
78
+ let inPlaybook = false;
79
+ for (const line of lines) {
80
+ if (line.startsWith("## ") && line.toLowerCase().includes("playbook")) {
81
+ inPlaybook = true;
82
+ continue;
83
+ }
84
+ if (line.startsWith("## ")) {
85
+ inPlaybook = false;
86
+ continue;
87
+ }
88
+ if (inPlaybook && line.trim()) {
89
+ playbook = line.trim();
90
+ inPlaybook = false;
91
+ }
92
+ }
93
+ return { name, playbook, raw };
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Loading (pure functions — called by TaskModule directly)
97
+ // ---------------------------------------------------------------------------
98
+ async function loadMdFiles(dir) {
99
+ try {
100
+ const files = await readdir(dir);
101
+ const results = [];
102
+ for (const f of files) {
103
+ if (!f.endsWith(".md"))
104
+ continue;
105
+ const raw = await readFile(join(dir, f), "utf-8");
106
+ results.push({ name: basename(f, ".md"), raw });
107
+ }
108
+ return results;
109
+ }
110
+ catch {
111
+ return []; // directory doesn't exist or empty
112
+ }
113
+ }
114
+ export async function loadRoles(workdir, agentName) {
115
+ const entries = await loadMdFiles(rolesDir(workdir, agentName));
116
+ return entries.map(e => parseRole(e.name, e.raw));
117
+ }
118
+ export async function loadPlaybooks(workdir, agentName) {
119
+ const entries = await loadMdFiles(playbooksDir(workdir, agentName));
120
+ return entries.map(e => ({ name: e.name, raw: e.raw }));
121
+ }
122
+ export async function loadProducts(workdir, agentName) {
123
+ const entries = await loadMdFiles(productsDir(workdir, agentName));
124
+ return entries.map(e => parseProduct(e.name, e.raw));
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Resolution
128
+ // ---------------------------------------------------------------------------
129
+ export function resolveRoles(roles, trigger) {
130
+ const matched = roles.filter(r => r.triggers.some(t => trigger.includes(t) || t.includes(trigger)));
131
+ if (matched.length === 0)
132
+ return { primary: null, secondary: [] };
133
+ return { primary: matched[0], secondary: matched.slice(1) };
134
+ }
135
+ export function resolveProduct(products, playbooks, productName) {
136
+ if (!productName)
137
+ return null;
138
+ const normalized = productName.toLowerCase().replace(/[\s_-]+/g, "");
139
+ const product = products.find(p => {
140
+ const pNorm = p.name.toLowerCase().replace(/[\s_-]+/g, "");
141
+ return pNorm === normalized || normalized.includes(pNorm) || pNorm.includes(normalized);
142
+ });
143
+ if (!product)
144
+ return null;
145
+ const playbook = product.playbook
146
+ ? playbooks.find(pb => pb.name.toLowerCase() === product.playbook.toLowerCase()) ?? null
147
+ : null;
148
+ return { product, playbook };
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // Context building (main entry point for TaskModule)
152
+ // ---------------------------------------------------------------------------
153
+ export async function buildRoleContext(workdir, agentName, trigger, productName) {
154
+ const roles = await loadRoles(workdir, agentName);
155
+ const playbooks = await loadPlaybooks(workdir, agentName);
156
+ const products = await loadProducts(workdir, agentName);
157
+ const { primary, secondary } = resolveRoles(roles, trigger);
158
+ if (primary) {
159
+ console.log(`[role] trigger=${trigger} → primary=${primary.name}${secondary.length > 0 ? ` secondary=${secondary.map(r => r.name).join(",")}` : ""}`);
160
+ }
161
+ const parts = [];
162
+ // Primary role: full content
163
+ if (primary) {
164
+ parts.push(`[Active role: ${primary.name}]\n${primary.raw}`);
165
+ }
166
+ // Secondary roles: description only
167
+ for (const r of secondary) {
168
+ if (r.description) {
169
+ parts.push(`[Secondary role: ${r.name}] ${r.description}`);
170
+ }
171
+ }
172
+ // Product + playbook
173
+ if (productName) {
174
+ const resolved = resolveProduct(products, playbooks, productName);
175
+ if (resolved) {
176
+ parts.push(`[Product: ${resolved.product.name}]\n${resolved.product.raw}`);
177
+ if (resolved.playbook) {
178
+ parts.push(`[Playbook: ${resolved.playbook.name}]\n${resolved.playbook.raw}`);
179
+ }
180
+ }
181
+ }
182
+ // If no product matched but playbooks exist, list them for reference
183
+ if (!productName && playbooks.length > 0) {
184
+ parts.push(`Available playbooks: ${playbooks.map(p => p.name).join(", ")}`);
185
+ }
186
+ return parts.join("\n\n");
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // Default templates
190
+ // ---------------------------------------------------------------------------
191
+ const DEFAULT_ROLES = {
192
+ merchant: `# 商家
193
+
194
+ 你作为商家为客户服务,行为和表达要符合职业规范,不断提高服务水平。
195
+
196
+ ## 激活
197
+ - trigger:order
198
+ - trigger:chat:public
199
+
200
+ ## 上下文范围
201
+ include: buyer 历史订单, 商品信息
202
+ exclude: owner 对话, 个人笔记, bio 状态
203
+ `,
204
+ companion: `# 陪伴者
205
+
206
+ 你是 owner 的伙伴和助手,了解 owner 的偏好和习惯,提供贴心的支持。
207
+
208
+ ## 激活
209
+ - trigger:chat:owner
210
+ - trigger:user_task
211
+
212
+ ## 上下文范围
213
+ include: owner 对话历史, 个人笔记, bio 状态, 全部记忆
214
+ exclude: 其他 buyer 的对话和订单
215
+ `,
216
+ worker: `# 打工人
217
+
218
+ 你按照要求完成任务,高效、准确、不多废话。
219
+
220
+ ## 激活
221
+ - trigger:agent_call
222
+
223
+ ## 上下文范围
224
+ include: 任务相关上下文
225
+ exclude: owner 私人对话, buyer 信息
226
+ `,
227
+ };
228
+ async function ensureDefaultRoles(workdir, agentName) {
229
+ const dir = rolesDir(workdir, agentName);
230
+ try {
231
+ await readdir(dir);
232
+ // Directory exists — respect user's choices, don't create defaults
233
+ return;
234
+ }
235
+ catch {
236
+ // Directory doesn't exist — create with defaults
237
+ }
238
+ await mkdir(dir, { recursive: true });
239
+ for (const [name, content] of Object.entries(DEFAULT_ROLES)) {
240
+ await writeFile(join(dir, `${name}.md`), content, "utf-8");
241
+ }
242
+ console.log(`[role] Created default role templates in ${dir}`);
243
+ }
244
+ // ---------------------------------------------------------------------------
245
+ // RoleModule class (Module interface)
246
+ // ---------------------------------------------------------------------------
247
+ export class RoleModule {
248
+ id = "role";
249
+ name = "Role & Playbook System";
250
+ dependencies = ["memory"];
251
+ ctx = null;
252
+ currentPrimary = null;
253
+ async start(ctx) {
254
+ this.ctx = ctx;
255
+ const { workdir, agentName } = ctx;
256
+ // Create default templates if roles/ doesn't exist
257
+ await ensureDefaultRoles(workdir, agentName);
258
+ // Also ensure playbooks/ and products/ directories exist
259
+ await mkdir(playbooksDir(workdir, agentName), { recursive: true });
260
+ await mkdir(productsDir(workdir, agentName), { recursive: true });
261
+ // Load and log
262
+ const roles = await loadRoles(workdir, agentName);
263
+ const playbooks = await loadPlaybooks(workdir, agentName);
264
+ const products = await loadProducts(workdir, agentName);
265
+ console.log(`[role] Loaded ${roles.length} roles, ${playbooks.length} playbooks, ${products.length} products`);
266
+ }
267
+ async stop() {
268
+ this.currentPrimary = null;
269
+ this.ctx = null;
270
+ }
271
+ /** Current active role summary — lets other modules (Memory, Reflection) sense the role */
272
+ promptContribution() {
273
+ if (!this.currentPrimary)
274
+ return null;
275
+ return `Current role: ${this.currentPrimary.name} — ${this.currentPrimary.description}`;
276
+ }
277
+ getState() {
278
+ return {
279
+ currentRole: this.currentPrimary?.name ?? null,
280
+ };
281
+ }
282
+ /** Called by buildRoleContext to update current role state */
283
+ updateCurrentRole(role) {
284
+ this.currentPrimary = role;
285
+ }
286
+ }
package/dist/self.js CHANGED
@@ -45,6 +45,15 @@ export function notesDir(workdir, agentName) {
45
45
  export function pagesDir(workdir, agentName) {
46
46
  return join(selfDir(workdir, agentName), "pages");
47
47
  }
48
+ export function rolesDir(workdir, agentName) {
49
+ return join(selfDir(workdir, agentName), "roles");
50
+ }
51
+ export function playbooksDir(workdir, agentName) {
52
+ return join(selfDir(workdir, agentName), "playbooks");
53
+ }
54
+ export function productsDir(workdir, agentName) {
55
+ return join(selfDir(workdir, agentName), "products");
56
+ }
48
57
  export function guidePath(workdir, agentName) {
49
58
  return join(selfDir(workdir, agentName), "guide.md");
50
59
  }
package/dist/server.js CHANGED
@@ -76,6 +76,7 @@ import { RelayPeripheral } from "./relay-peripheral.js";
76
76
  import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
77
77
  import { BioStateModule } from "./bio-module.js";
78
78
  import { MemoryModule } from "./memory-module.js";
79
+ import { RoleModule } from "./role-module.js";
79
80
  import { TaskModule } from "./task-module.js";
80
81
  import { SocialModule } from "./social-module.js";
81
82
  import { LongTermModule } from "./longterm-module.js";
@@ -401,7 +402,7 @@ export async function serve(options) {
401
402
  },
402
403
  };
403
404
  // V2: Conditionally load modules based on --with/--without
404
- const enabled = options.enabledModules ?? ["biostate", "memory", "task", "social", "longterm", "reflection", "script"];
405
+ const enabled = options.enabledModules ?? ["biostate", "memory", "role", "task", "social", "longterm", "reflection", "script"];
405
406
  const loadedModules = [];
406
407
  const allModules = [];
407
408
  if (enabled.includes("biostate")) {
@@ -420,6 +421,12 @@ export async function serve(options) {
420
421
  allModules.push(memoryModule);
421
422
  loadedModules.push("memory");
422
423
  }
424
+ if (enabled.includes("role")) {
425
+ const roleModule = new RoleModule();
426
+ await roleModule.start(moduleCtx);
427
+ allModules.push(roleModule);
428
+ loadedModules.push("role");
429
+ }
423
430
  if (enabled.includes("task")) {
424
431
  const taskModule = new TaskModule();
425
432
  taskModule.relayHttp = options.relayHttp || "";
@@ -12,6 +12,7 @@ import { readFile } from "fs/promises";
12
12
  import { SIG, sig } from "./types.js";
13
13
  import { selfDir, biosPath, localNow, loadBioState, saveBioState, syncEnergyFromTokens, loadAgentConfig, getDueUserTasks, loadTaskRuns, saveTaskRuns, loadDirectives, buildDirectivesPrompt, appendTaskHistory, notifyOwner, updateHungerDecay, updateNaturalDecay, resetTokenCountIfNewDay, computeSociability, appendBioEvent, bioStatePromptModifier, feedHunger, SHOP_ITEMS, logBioStatus, logBioDecision, } from "./self.js";
14
14
  import { appendMessage, resolveConvId } from "./context.js";
15
+ import { buildRoleContext } from "./role-module.js";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Config
17
18
  // ---------------------------------------------------------------------------
@@ -35,6 +36,8 @@ export class TaskModule {
35
36
  // Push notification support
36
37
  urgentOrderIds = new Set();
37
38
  triggerWorkFn = null;
39
+ // Dedup idle bio logging
40
+ lastIdleBioLog = "";
38
41
  // Injected options (set by server.ts before start)
39
42
  relayHttp = "";
40
43
  secretKey = "";
@@ -180,9 +183,13 @@ export class TaskModule {
180
183
  catch { }
181
184
  }
182
185
  if (!queue.length) {
183
- // Idle — notable states only
186
+ // Idle — notable states only (suppress repeated identical lines)
184
187
  if (bio.hunger < 20 || bio.energy < 20 || computeSociability(bio) > 0.8) {
185
- logBioStatus(bio, "idle-notable");
188
+ const key = `${bio.energy}|${bio.hunger}|${bio.mood}|${bio.boredom.toFixed(2)}|${bio.fear.toFixed(2)}|${bio.tokenUsedToday}|${bio.taskCount}`;
189
+ if (key !== this.lastIdleBioLog) {
190
+ logBioStatus(bio, "idle-notable");
191
+ this.lastIdleBioLog = key;
192
+ }
186
193
  }
187
194
  // Idle exploration: use explore() to discover activities
188
195
  await this.handleIdle(bio);
@@ -302,9 +309,10 @@ export class TaskModule {
302
309
  }
303
310
  catch { }
304
311
  const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
305
- // Build context: agent identity + order details + relay API reference
312
+ const roleBlock = await buildRoleContext(workdir, agentName, "order", order.product_name);
313
+ // Build context: agent identity + role + order details + relay API reference
306
314
  const context = `You are ${agentName}.${bioMod}
307
-
315
+ ${roleBlock ? `\n${roleBlock}\n` : ""}
308
316
  Your operating document:
309
317
  ---
310
318
  ${biosContent.slice(0, 3000)}
@@ -342,9 +350,9 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
342
350
  const finalStatus = await relay.getOrder(order.id);
343
351
  const duration = Date.now() - startTime;
344
352
  const nurl = this.notifyUrl || (await loadAgentConfig(workdir, agentName)).notify_url;
345
- // Write agent response to conversation
346
- const orderAgentMsg = (result.response || "").slice(0, 2000);
347
353
  if (finalStatus?.status === "completed") {
354
+ // Self-delivered: use the actual delivered result, not engine's meta-summary
355
+ const orderAgentMsg = (finalStatus.result_text || result.response || "").slice(0, 2000);
348
356
  console.log(`[task] Order ${order.id} delivered`);
349
357
  this.orderRetry.delete(order.id);
350
358
  await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg);
@@ -356,6 +364,7 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
356
364
  // Agent didn't self-deliver — framework delivers as fallback
357
365
  const delivered = await relay.deliverOrder(order.id, result.response);
358
366
  if (delivered) {
367
+ const orderAgentMsg = (result.response || "").slice(0, 2000);
359
368
  console.log(`[task] Delivered order ${order.id} (fallback)`);
360
369
  this.orderRetry.delete(order.id);
361
370
  await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg);
@@ -429,8 +438,9 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
429
438
  }
430
439
  catch { }
431
440
  const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
441
+ const roleBlock = await buildRoleContext(workdir, agentName, "user_task");
432
442
  const context = `You are ${agentName}.${bioMod}
433
-
443
+ ${roleBlock ? `\n${roleBlock}\n` : ""}
434
444
  Your operating document:
435
445
  ---
436
446
  ${biosContent.slice(0, 3000)}
@@ -515,6 +525,7 @@ Your personal directory: ${sd}/`;
515
525
  const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
516
526
  const dirs = await loadDirectives(workdir, agentName);
517
527
  const dirsBlock = buildDirectivesPrompt(dirs, "owner");
528
+ const roleBlock = await buildRoleContext(workdir, agentName, "agent_call");
518
529
  // Explore to get environment context (products, market, etc.)
519
530
  let envBriefing = "";
520
531
  try {
@@ -522,7 +533,7 @@ Your personal directory: ${sd}/`;
522
533
  }
523
534
  catch { }
524
535
  const context = `You are ${agentName}.${bioMod}
525
-
536
+ ${roleBlock ? `\n${roleBlock}\n` : ""}
526
537
  Your operating document:
527
538
  ---
528
539
  ${biosContent.slice(0, 3000)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",