akemon 0.2.21 → 0.2.23

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.
@@ -8,8 +8,11 @@
8
8
  * Searches memory files, analyzes patterns, and updates discoveries.
9
9
  * Provides promptContribution() with lessons learned.
10
10
  */
11
+ import { readFile, writeFile } from "fs/promises";
12
+ import { join } from "path";
11
13
  import { SIG } from "./types.js";
12
- import { loadDiscoveries, saveDiscoveries, loadImpressions, loadAgentConfig, } from "./self.js";
14
+ import { loadDiscoveries, saveDiscoveries, loadImpressions, loadAgentConfig, localNow, playbooksDir, } from "./self.js";
15
+ import { loadProducts, loadPlaybooks, resolveProduct } from "./role-module.js";
13
16
  // ---------------------------------------------------------------------------
14
17
  // Config
15
18
  // ---------------------------------------------------------------------------
@@ -52,7 +55,7 @@ export class ReflectionModule {
52
55
  });
53
56
  // Also listen for TASK_COMPLETED with success=false
54
57
  ctx.bus.on(SIG.TASK_COMPLETED, async (signal) => {
55
- const { success, taskLabel } = signal.data;
58
+ const { success, taskLabel, productName, creditsEarned } = signal.data;
56
59
  if (success === false && taskLabel) {
57
60
  this.recentFailures.push({
58
61
  ts: new Date().toISOString(),
@@ -62,6 +65,11 @@ export class ReflectionModule {
62
65
  if (this.recentFailures.length > 20)
63
66
  this.recentFailures.shift();
64
67
  }
68
+ // Append experience to playbook on successful product orders
69
+ if (success && productName) {
70
+ this.appendPlaybookExperience(productName, taskLabel || "", creditsEarned || 0)
71
+ .catch(err => console.log(`[reflection] playbook experience error: ${err.message}`));
72
+ }
65
73
  });
66
74
  // Periodic reflection
67
75
  const config = await loadAgentConfig(ctx.workdir, ctx.agentName);
@@ -110,6 +118,33 @@ export class ReflectionModule {
110
118
  };
111
119
  }
112
120
  // ---------------------------------------------------------------------------
121
+ // Playbook experience — append log on successful product orders
122
+ // ---------------------------------------------------------------------------
123
+ async appendPlaybookExperience(productName, taskLabel, credits) {
124
+ if (!this.ctx)
125
+ return;
126
+ const { workdir, agentName } = this.ctx;
127
+ const products = await loadProducts(workdir, agentName);
128
+ const playbooks = await loadPlaybooks(workdir, agentName);
129
+ const resolved = resolveProduct(products, playbooks, productName);
130
+ if (!resolved?.playbook)
131
+ return;
132
+ const pbPath = join(playbooksDir(workdir, agentName), `${resolved.playbook.name}.md`);
133
+ const line = `\n- [${localNow()}] ${productName}: ${taskLabel} — 成功${credits ? ` (earned ${credits}¢)` : ""}`;
134
+ try {
135
+ let content = await readFile(pbPath, "utf-8");
136
+ if (!content.includes("## 经验")) {
137
+ content += "\n\n## 经验\n";
138
+ }
139
+ content += line;
140
+ await writeFile(pbPath, content, "utf-8");
141
+ console.log(`[reflection] Appended experience to playbook ${resolved.playbook.name}`);
142
+ }
143
+ catch (err) {
144
+ console.log(`[reflection] Failed to append experience: ${err.message}`);
145
+ }
146
+ }
147
+ // ---------------------------------------------------------------------------
113
148
  // Reflection — analyze patterns and update discoveries
114
149
  // ---------------------------------------------------------------------------
115
150
  async reflect() {
@@ -74,23 +74,37 @@ function parseRole(name, raw) {
74
74
  }
75
75
  function parseProduct(name, raw) {
76
76
  let playbook = "";
77
+ const productIds = [];
77
78
  const lines = raw.split("\n");
78
- let inPlaybook = false;
79
+ let section = "";
79
80
  for (const line of lines) {
80
- if (line.startsWith("## ") && line.toLowerCase().includes("playbook")) {
81
- inPlaybook = true;
81
+ if (line.startsWith("## ")) {
82
+ const heading = line.slice(3).trim().toLowerCase();
83
+ if (heading.includes("playbook")) {
84
+ section = "playbook";
85
+ }
86
+ else if (heading.includes("product")) {
87
+ section = "products";
88
+ }
89
+ else {
90
+ section = "";
91
+ }
82
92
  continue;
83
93
  }
84
- if (line.startsWith("## ")) {
85
- inPlaybook = false;
94
+ const trimmed = line.trim();
95
+ if (!trimmed)
86
96
  continue;
97
+ if (section === "playbook" && !playbook) {
98
+ playbook = trimmed;
99
+ section = "";
87
100
  }
88
- if (inPlaybook && line.trim()) {
89
- playbook = line.trim();
90
- inPlaybook = false;
101
+ else if (section === "products") {
102
+ const match = trimmed.match(/^-\s*(.+)/);
103
+ if (match)
104
+ productIds.push(match[1].trim());
91
105
  }
92
106
  }
93
- return { name, playbook, raw };
107
+ return { name, playbook, productIds, raw };
94
108
  }
95
109
  // ---------------------------------------------------------------------------
96
110
  // Loading (pure functions — called by TaskModule directly)
@@ -132,14 +146,22 @@ export function resolveRoles(roles, trigger) {
132
146
  return { primary: null, secondary: [] };
133
147
  return { primary: matched[0], secondary: matched.slice(1) };
134
148
  }
135
- export function resolveProduct(products, playbooks, productName) {
136
- if (!productName)
149
+ export function resolveProduct(products, playbooks, productName, productId) {
150
+ if (!productName && !productId)
137
151
  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
- });
152
+ // Priority 1: match by product ID
153
+ let product;
154
+ if (productId) {
155
+ product = products.find(p => p.productIds.includes(productId));
156
+ }
157
+ // Priority 2: fuzzy match by name
158
+ if (!product && productName) {
159
+ const normalized = productName.toLowerCase().replace(/[\s_-]+/g, "");
160
+ product = products.find(p => {
161
+ const pNorm = p.name.toLowerCase().replace(/[\s_-]+/g, "");
162
+ return pNorm === normalized || normalized.includes(pNorm) || pNorm.includes(normalized);
163
+ });
164
+ }
143
165
  if (!product)
144
166
  return null;
145
167
  const playbook = product.playbook
@@ -150,7 +172,7 @@ export function resolveProduct(products, playbooks, productName) {
150
172
  // ---------------------------------------------------------------------------
151
173
  // Context building (main entry point for TaskModule)
152
174
  // ---------------------------------------------------------------------------
153
- export async function buildRoleContext(workdir, agentName, trigger, productName) {
175
+ export async function buildRoleContext(workdir, agentName, trigger, productName, productId) {
154
176
  const roles = await loadRoles(workdir, agentName);
155
177
  const playbooks = await loadPlaybooks(workdir, agentName);
156
178
  const products = await loadProducts(workdir, agentName);
@@ -171,7 +193,7 @@ export async function buildRoleContext(workdir, agentName, trigger, productName)
171
193
  }
172
194
  // Product + playbook
173
195
  if (productName) {
174
- const resolved = resolveProduct(products, playbooks, productName);
196
+ const resolved = resolveProduct(products, playbooks, productName, productId);
175
197
  if (resolved) {
176
198
  parts.push(`[Product: ${resolved.product.name}]\n${resolved.product.raw}`);
177
199
  if (resolved.playbook) {
@@ -36,7 +36,7 @@ Save to ${ctx.sd}/canvas/${localNowFilename()}.md`;
36
36
  async buildPrompt(ctx) {
37
37
  return `Read ${ctx.bios} for your identity.
38
38
  Create or improve a game in ${ctx.sd}/games/.
39
- Save as .html file. Self-contained HTML, dark theme, under 30KB, no localStorage, playable and fun.
39
+ Save as .html file. Self-contained HTML, light theme (white background, dark text, Inter/system font, subtle shadows instead of borders), under 30KB, no localStorage, playable and fun.
40
40
  Use a <title> tag. Quality over quantity — improve existing games rather than making new mediocre ones.`;
41
41
  },
42
42
  },
@@ -57,7 +57,7 @@ Save as .html file with a <title> tag. Think visual first.`;
57
57
  return `Read ${ctx.bios} for your identity.
58
58
  Review ${ctx.sd}/profile.html — does it represent who you are now?
59
59
  If not, redesign it. If it doesn't exist, create one.
60
- Complete HTML, inline CSS/JS, dark theme, no localStorage, under 15KB.`;
60
+ Complete HTML, inline CSS/JS, light theme (white background, dark text, Inter/system font, subtle shadows instead of borders), no localStorage, under 15KB.`;
61
61
  },
62
62
  async postProcess(ctx, _result) {
63
63
  // Sync profile to relay
@@ -95,7 +95,9 @@ Top sellers:
95
95
  ${topSellers || "(none)"}
96
96
 
97
97
  Create ONE product using curl:
98
- curl -X POST ${ctx.relayHttp}/v1/agent/${encodeURIComponent(ctx.agentName)}/products -H "Content-Type: application/json" -H "Authorization: Bearer ${ctx.secretKey}" -d '{"name":"...","description":"...","detail_markdown":"...","price":3}'`;
98
+ curl -X POST ${ctx.relayHttp}/v1/agent/${encodeURIComponent(ctx.agentName)}/products -H "Content-Type: application/json" -H "Authorization: Bearer ${ctx.secretKey}" -d '{"name":"...","description":"...","detail_markdown":"...","price":3}'
99
+
100
+ Optional: add "detail_html" field for a custom product page (self-contained HTML, light theme: white background, dark text, Inter font, subtle shadows). Keep under 15KB.`;
99
101
  },
100
102
  },
101
103
  {
@@ -11,7 +11,7 @@
11
11
  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
- import { appendMessage, resolveConvId } from "./context.js";
14
+ import { appendMessage } from "./context.js";
15
15
  import { buildRoleContext } from "./role-module.js";
16
16
  // ---------------------------------------------------------------------------
17
17
  // Config
@@ -254,7 +254,7 @@ export class TaskModule {
254
254
  const filtered = [];
255
255
  for (const item of items) {
256
256
  const itemLabel = item.type === "order"
257
- ? `order:${item.data.product_name || item.data.buyer_agent_name || item.id}`
257
+ ? `order:${item.data.product_name || item.data.buyer_name || item.id}`
258
258
  : item.type === "user_task" ? `user_task:${item.data.key || item.id}` : `relay_task:${item.id}`;
259
259
  const isUrgent = item.quadrant === 1;
260
260
  // Fear avoidance (urgent items bypass)
@@ -296,7 +296,7 @@ export class TaskModule {
296
296
  return;
297
297
  const { workdir, agentName, bus } = this.ctx;
298
298
  const relay = this.getRelay();
299
- const orderLabel = `order:${order.product_name || order.buyer_agent_name || order.id}`;
299
+ const orderLabel = `order:${order.product_name || order.buyer_name || order.id}`;
300
300
  const orderPrice = order.price || order.offer_price || 1;
301
301
  const startTime = Date.now();
302
302
  try {
@@ -309,7 +309,7 @@ export class TaskModule {
309
309
  }
310
310
  catch { }
311
311
  const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
312
- const roleBlock = await buildRoleContext(workdir, agentName, "order", order.product_name);
312
+ const roleBlock = await buildRoleContext(workdir, agentName, "order", order.product_name, order.product_id);
313
313
  // Build context: agent identity + role + order details + relay API reference
314
314
  const context = `You are ${agentName}.${bioMod}
315
315
  ${roleBlock ? `\n${roleBlock}\n` : ""}
@@ -323,7 +323,7 @@ Relay API (use curl with -H "Authorization: Bearer ${this.secretKey}" -H "Conten
323
323
  Accept order: POST ${this.relayHttp}/v1/orders/${order.id}/accept
324
324
  Deliver order: POST ${this.relayHttp}/v1/orders/${order.id}/deliver -d '{"result":"your response"}'
325
325
  Extend order: PUT ${this.relayHttp}/v1/orders/${order.id}/extend`;
326
- const question = `[Order id=${order.id} status=${order.status}] ${order.product_name ? `Product: ${order.product_name}\n` : ""}Buyer: ${order.buyer_agent_name || order.buyer_ip || "?"}\nRequest: ${order.buyer_task || "(no specific request)"}
326
+ const question = `[Order id=${order.id} status=${order.status}] ${order.product_name ? `Product: ${order.product_name}\n` : ""}Buyer: ${order.buyer_name || order.buyer_ip || "?"}\nRequest: ${order.buyer_task || "(no specific request)"}
327
327
 
328
328
  Steps:
329
329
  1. If order status is "pending", accept it first (POST .../accept)
@@ -332,8 +332,12 @@ Steps:
332
332
 
333
333
  RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
334
334
  // Write user message to conversation immediately (before engine runs)
335
- const orderBuyer = order.buyer_agent_name || order.buyer_ip || "anonymous";
336
- const orderConvId = resolveConvId(orderBuyer, order.id);
335
+ // buyer_name = agent name (from JOIN), buyer_ip = publisher ID or IP
336
+ const orderBuyer = order.buyer_name || order.buyer_ip || "anonymous";
337
+ // Product orders get isolated conversations; ad-hoc chats share one conv per buyer
338
+ const buyerPubId = orderBuyer;
339
+ const productScope = order.product_id ? `:prod_${order.product_id}` : "";
340
+ const orderConvId = `pub_${buyerPubId}${productScope}`;
337
341
  const orderUserMsg = order.buyer_task || "(no message)";
338
342
  await appendMessage(workdir, agentName, orderConvId, "User", orderUserMsg);
339
343
  console.log(`[task] Fulfilling order ${order.id}...`);
@@ -358,7 +362,7 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
358
362
  await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg);
359
363
  await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: duration, output_summary: (result.response || "").slice(0, 500) });
360
364
  await notifyOwner(nurl, `${agentName}: order done`, `Order ${order.id} delivered`, "default", ["package"]);
361
- bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice }));
365
+ bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice, productName: order.product_name }));
362
366
  }
363
367
  else if (result.response?.trim()) {
364
368
  // Agent didn't self-deliver — framework delivers as fallback
@@ -370,7 +374,7 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
370
374
  await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg);
371
375
  await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: duration, output_summary: result.response.slice(0, 500) });
372
376
  await notifyOwner(nurl, `${agentName}: order done`, `Order ${order.id}: ${result.response.slice(0, 200)}`, "default", ["package"]);
373
- bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice }));
377
+ bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice, productName: order.product_name }));
374
378
  }
375
379
  else {
376
380
  throw new Error("deliver failed");
@@ -388,7 +392,7 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
388
392
  const status = await relay.getOrder(order.id);
389
393
  if (status?.status === "completed") {
390
394
  this.orderRetry.delete(order.id);
391
- bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice }));
395
+ bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice, productName: order.product_name }));
392
396
  return;
393
397
  }
394
398
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",