akemon 0.3.1 → 0.3.3
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/README.md +18 -4
- package/dist/cli.js +95 -0
- package/dist/server.js +161 -6
- package/dist/software-agent-peripheral.js +382 -0
- package/dist/types.js +3 -0
- package/package.json +4 -4
- package/dist/context.test.js +0 -90
- package/dist/dashboard.html +0 -552
- package/dist/engine-queue.test.js +0 -99
- package/dist/engine-routing.test.js +0 -122
- package/dist/engine-stream.test.js +0 -103
- package/dist/orphan-scan.test.js +0 -81
- package/dist/reflection-module.integration.test.js +0 -180
- package/dist/reflection-module.test.js +0 -66
- package/dist/role-module.test.js +0 -208
- package/dist/task-helpers.test.js +0 -88
package/dist/role-module.test.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { parseRole, parseProduct, resolveRoles, resolveProduct, buildRoleContext, } from "./role-module.js";
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Helpers
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
function makeRole(name, triggers = [], extras = {}) {
|
|
11
|
-
return { name, description: "", triggers, include: [], exclude: [], customRules: "", raw: "", ...extras };
|
|
12
|
-
}
|
|
13
|
-
function makeProduct(name, extras = {}) {
|
|
14
|
-
return { name, playbook: "", productIds: [], raw: "", ...extras };
|
|
15
|
-
}
|
|
16
|
-
function makePlaybook(name, raw = "") {
|
|
17
|
-
return { name, raw };
|
|
18
|
-
}
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Pure parsing — parseRole
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
describe("parseRole — pure parsing", () => {
|
|
23
|
-
it("extracts description, triggers, include, exclude from standard role md", () => {
|
|
24
|
-
const raw = `# Sales Rep
|
|
25
|
-
|
|
26
|
-
Expert in closing deals and building client relationships.
|
|
27
|
-
|
|
28
|
-
## 激活
|
|
29
|
-
- trigger:order
|
|
30
|
-
- trigger:chat:public
|
|
31
|
-
|
|
32
|
-
## 上下文范围
|
|
33
|
-
include: buyer history, product catalog
|
|
34
|
-
exclude: owner notes, private diary
|
|
35
|
-
`;
|
|
36
|
-
const role = parseRole("sales-rep", raw);
|
|
37
|
-
assert.equal(role.name, "sales-rep");
|
|
38
|
-
assert.equal(role.description, "Expert in closing deals and building client relationships.");
|
|
39
|
-
assert.deepEqual(role.triggers, ["trigger:order", "trigger:chat:public"]);
|
|
40
|
-
assert.deepEqual(role.include, ["buyer history", "product catalog"]);
|
|
41
|
-
assert.deepEqual(role.exclude, ["owner notes", "private diary"]);
|
|
42
|
-
});
|
|
43
|
-
it("returns empty arrays for include/exclude/triggers when sections are missing", () => {
|
|
44
|
-
const raw = `# Simple Role
|
|
45
|
-
|
|
46
|
-
A minimal role with no activation or context sections.
|
|
47
|
-
`;
|
|
48
|
-
const role = parseRole("simple", raw);
|
|
49
|
-
assert.deepEqual(role.triggers, []);
|
|
50
|
-
assert.deepEqual(role.include, []);
|
|
51
|
-
assert.deepEqual(role.exclude, []);
|
|
52
|
-
assert.equal(role.description, "A minimal role with no activation or context sections.");
|
|
53
|
-
});
|
|
54
|
-
it("collects content outside 激活 and 上下文范围 into customRules", () => {
|
|
55
|
-
const raw = `# Worker
|
|
56
|
-
|
|
57
|
-
Efficient task executor.
|
|
58
|
-
|
|
59
|
-
## 激活
|
|
60
|
-
- trigger:agent_call
|
|
61
|
-
|
|
62
|
-
## Operating Principles
|
|
63
|
-
Always reply in the language the user writes in.
|
|
64
|
-
Be concise and accurate.
|
|
65
|
-
|
|
66
|
-
## Tone
|
|
67
|
-
Professional and neutral.
|
|
68
|
-
`;
|
|
69
|
-
const role = parseRole("worker", raw);
|
|
70
|
-
assert.ok(role.customRules.includes("Always reply in the language the user writes in."), "should include content from ## Operating Principles");
|
|
71
|
-
assert.ok(role.customRules.includes("Professional and neutral."), "should include content from ## Tone");
|
|
72
|
-
// triggers and standard sections should not bleed in
|
|
73
|
-
assert.deepEqual(role.triggers, ["trigger:agent_call"]);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Pure parsing — parseProduct
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
describe("parseProduct — pure parsing", () => {
|
|
80
|
-
it("extracts playbook and multiple productIds correctly", () => {
|
|
81
|
-
const raw = `# Widget Pro
|
|
82
|
-
|
|
83
|
-
## playbook
|
|
84
|
-
widget-strategy
|
|
85
|
-
|
|
86
|
-
## products
|
|
87
|
-
- p_abc123
|
|
88
|
-
- p_def456
|
|
89
|
-
- p_ghi789
|
|
90
|
-
`;
|
|
91
|
-
const product = parseProduct("widget-pro", raw);
|
|
92
|
-
assert.equal(product.name, "widget-pro");
|
|
93
|
-
assert.equal(product.playbook, "widget-strategy");
|
|
94
|
-
assert.deepEqual(product.productIds, ["p_abc123", "p_def456", "p_ghi789"]);
|
|
95
|
-
});
|
|
96
|
-
it("returns empty productIds when ## products section is absent", () => {
|
|
97
|
-
const raw = `# No Products
|
|
98
|
-
|
|
99
|
-
## playbook
|
|
100
|
-
some-playbook
|
|
101
|
-
`;
|
|
102
|
-
const product = parseProduct("no-products", raw);
|
|
103
|
-
assert.deepEqual(product.productIds, []);
|
|
104
|
-
assert.equal(product.playbook, "some-playbook");
|
|
105
|
-
});
|
|
106
|
-
it("returns empty string for playbook when ## playbook section is absent", () => {
|
|
107
|
-
const raw = `# No Playbook
|
|
108
|
-
|
|
109
|
-
## products
|
|
110
|
-
- p_xyz
|
|
111
|
-
`;
|
|
112
|
-
const product = parseProduct("no-playbook", raw);
|
|
113
|
-
assert.equal(product.playbook, "");
|
|
114
|
-
assert.deepEqual(product.productIds, ["p_xyz"]);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Resolution — resolveRoles
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
describe("resolveRoles — resolution", () => {
|
|
121
|
-
it("partial trigger match: first match is primary, remaining are secondary", () => {
|
|
122
|
-
const roles = [
|
|
123
|
-
makeRole("general-chat", ["trigger:chat"]),
|
|
124
|
-
makeRole("owner-companion", ["trigger:chat:owner"]),
|
|
125
|
-
];
|
|
126
|
-
// "trigger:chat:owner" includes "trigger:chat" → both match
|
|
127
|
-
const { primary, secondary } = resolveRoles(roles, "trigger:chat:owner");
|
|
128
|
-
assert.equal(primary?.name, "general-chat");
|
|
129
|
-
assert.equal(secondary.length, 1);
|
|
130
|
-
assert.equal(secondary[0].name, "owner-companion");
|
|
131
|
-
});
|
|
132
|
-
it("returns primary=null and empty secondary when no trigger matches", () => {
|
|
133
|
-
const roles = [
|
|
134
|
-
makeRole("merchant", ["trigger:order"]),
|
|
135
|
-
makeRole("companion", ["trigger:chat:owner"]),
|
|
136
|
-
];
|
|
137
|
-
const { primary, secondary } = resolveRoles(roles, "trigger:nonexistent");
|
|
138
|
-
assert.equal(primary, null);
|
|
139
|
-
assert.deepEqual(secondary, []);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Resolution — resolveProduct
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
describe("resolveProduct — resolution", () => {
|
|
146
|
-
const products = [
|
|
147
|
-
makeProduct("Alpha Product", { productIds: ["p_alpha"], playbook: "alpha-pb" }),
|
|
148
|
-
makeProduct("Beta Service", { productIds: ["p_beta"], playbook: "beta-pb" }),
|
|
149
|
-
];
|
|
150
|
-
const playbooks = [
|
|
151
|
-
makePlaybook("alpha-pb", "# Alpha Playbook\nContent."),
|
|
152
|
-
makePlaybook("beta-pb", "# Beta Playbook\nContent."),
|
|
153
|
-
];
|
|
154
|
-
it("productId takes priority over productName when both are provided", () => {
|
|
155
|
-
// productId matches "Alpha Product", productName matches "Beta Service"
|
|
156
|
-
const result = resolveProduct(products, playbooks, "Beta Service", "p_alpha");
|
|
157
|
-
assert.ok(result !== null);
|
|
158
|
-
assert.equal(result.product.name, "Alpha Product");
|
|
159
|
-
});
|
|
160
|
-
it("fuzzy name match is case- and separator-insensitive", () => {
|
|
161
|
-
// "alpha_product" → normalized "alphaproduct"
|
|
162
|
-
// "Alpha Product" → normalized "alphaproduct" → exact match
|
|
163
|
-
const result = resolveProduct(products, playbooks, "alpha_product");
|
|
164
|
-
assert.ok(result !== null);
|
|
165
|
-
assert.equal(result.product.name, "Alpha Product");
|
|
166
|
-
});
|
|
167
|
-
it("returns null when neither productId nor productName matches", () => {
|
|
168
|
-
const result = resolveProduct(products, playbooks, "Completely Unknown");
|
|
169
|
-
assert.equal(result, null);
|
|
170
|
-
});
|
|
171
|
-
it("playbook lookup is case-insensitive", () => {
|
|
172
|
-
const prods = [makeProduct("my-widget", { playbook: "My-Playbook", productIds: [] })];
|
|
173
|
-
const pbs = [makePlaybook("my-playbook", "# PB Content")];
|
|
174
|
-
const result = resolveProduct(prods, pbs, "my-widget");
|
|
175
|
-
assert.ok(result !== null);
|
|
176
|
-
assert.ok(result.playbook !== null);
|
|
177
|
-
assert.equal(result.playbook?.name, "my-playbook");
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
// buildRoleContext — real fs with tmp dir
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
describe("buildRoleContext — fs integration", () => {
|
|
184
|
-
let tmpDir;
|
|
185
|
-
const agentName = "test-agent";
|
|
186
|
-
before(async () => {
|
|
187
|
-
tmpDir = await mkdtemp(join(tmpdir(), "akemon-test-"));
|
|
188
|
-
const selfBase = join(tmpDir, ".akemon", "agents", agentName, "self");
|
|
189
|
-
await mkdir(join(selfBase, "roles"), { recursive: true });
|
|
190
|
-
await mkdir(join(selfBase, "playbooks"), { recursive: true });
|
|
191
|
-
await mkdir(join(selfBase, "products"), { recursive: true });
|
|
192
|
-
await writeFile(join(selfBase, "roles", "worker.md"), `# 工人\n执行任务的专业角色。\n\n## 激活\n- trigger:order\n`);
|
|
193
|
-
await writeFile(join(selfBase, "playbooks", "strategy.md"), `# Strategy\n核心策略内容。\n`);
|
|
194
|
-
await writeFile(join(selfBase, "products", "widget.md"), `# Widget\n\n## playbook\nstrategy\n\n## products\n- p_1234\n`);
|
|
195
|
-
});
|
|
196
|
-
after(async () => {
|
|
197
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
198
|
-
});
|
|
199
|
-
it("matching trigger produces output with [Active role: ...]", async () => {
|
|
200
|
-
const result = await buildRoleContext(tmpDir, agentName, "trigger:order:001");
|
|
201
|
-
assert.ok(result.includes("[Active role: worker]"), `expected '[Active role: worker]' in:\n${result}`);
|
|
202
|
-
});
|
|
203
|
-
it("passing productName produces output with [Product: ...] and [Playbook: ...]", async () => {
|
|
204
|
-
const result = await buildRoleContext(tmpDir, agentName, "trigger:other", "widget");
|
|
205
|
-
assert.ok(result.includes("[Product: widget]"), `expected '[Product: widget]' in:\n${result}`);
|
|
206
|
-
assert.ok(result.includes("[Playbook: strategy]"), `expected '[Playbook: strategy]' in:\n${result}`);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { sortByQuadrant, dedupeWorkItems, computeRetryDelay, RETRY_INTERVALS } from "./task-helpers.js";
|
|
4
|
-
describe("sortByQuadrant", () => {
|
|
5
|
-
it("empty array returns empty array", () => {
|
|
6
|
-
assert.deepStrictEqual(sortByQuadrant([]), []);
|
|
7
|
-
});
|
|
8
|
-
it("does not modify the input array", () => {
|
|
9
|
-
const items = [
|
|
10
|
-
{ quadrant: 3, id: "a" },
|
|
11
|
-
{ quadrant: 1, id: "b" },
|
|
12
|
-
];
|
|
13
|
-
const original = [...items];
|
|
14
|
-
sortByQuadrant(items);
|
|
15
|
-
assert.deepStrictEqual(items, original, "input array must not be mutated");
|
|
16
|
-
});
|
|
17
|
-
it("sorts ascending by quadrant", () => {
|
|
18
|
-
const items = [
|
|
19
|
-
{ quadrant: 4, id: "d" },
|
|
20
|
-
{ quadrant: 2, id: "b" },
|
|
21
|
-
{ quadrant: 3, id: "c" },
|
|
22
|
-
{ quadrant: 1, id: "a" },
|
|
23
|
-
];
|
|
24
|
-
const result = sortByQuadrant(items);
|
|
25
|
-
assert.deepStrictEqual(result.map(i => i.quadrant), [1, 2, 3, 4]);
|
|
26
|
-
});
|
|
27
|
-
it("is stable: equal-quadrant items preserve original order", () => {
|
|
28
|
-
const items = [
|
|
29
|
-
{ quadrant: 2, id: "first" },
|
|
30
|
-
{ quadrant: 1, id: "solo" },
|
|
31
|
-
{ quadrant: 2, id: "second" },
|
|
32
|
-
];
|
|
33
|
-
const result = sortByQuadrant(items);
|
|
34
|
-
assert.strictEqual(result[0].id, "solo");
|
|
35
|
-
assert.strictEqual(result[1].id, "first", "first Q2 item should come before second Q2 item");
|
|
36
|
-
assert.strictEqual(result[2].id, "second");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describe("dedupeWorkItems", () => {
|
|
40
|
-
it("different type, same id are kept (not considered duplicates)", () => {
|
|
41
|
-
const items = [
|
|
42
|
-
{ type: "order", id: "abc" },
|
|
43
|
-
{ type: "user_task", id: "abc" },
|
|
44
|
-
];
|
|
45
|
-
const result = dedupeWorkItems(items);
|
|
46
|
-
assert.strictEqual(result.length, 2, "order:abc and user_task:abc must both survive");
|
|
47
|
-
});
|
|
48
|
-
it("same type+id: only first occurrence is kept", () => {
|
|
49
|
-
const items = [
|
|
50
|
-
{ type: "order", id: "x", extra: 1 },
|
|
51
|
-
{ type: "order", id: "x", extra: 2 },
|
|
52
|
-
];
|
|
53
|
-
const result = dedupeWorkItems(items);
|
|
54
|
-
assert.strictEqual(result.length, 1);
|
|
55
|
-
assert.strictEqual(result[0].extra, 1, "first occurrence must be preserved");
|
|
56
|
-
});
|
|
57
|
-
it("returns empty array for empty input", () => {
|
|
58
|
-
assert.deepStrictEqual(dedupeWorkItems([]), []);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
describe("computeRetryDelay", () => {
|
|
62
|
-
it("count=0 returns 0 (immediate first retry)", () => {
|
|
63
|
-
assert.strictEqual(computeRetryDelay(0), 0);
|
|
64
|
-
});
|
|
65
|
-
it("count=1 returns 30_000", () => {
|
|
66
|
-
assert.strictEqual(computeRetryDelay(1), 30_000);
|
|
67
|
-
});
|
|
68
|
-
it("count=4 returns 7_200_000 (2h)", () => {
|
|
69
|
-
assert.strictEqual(computeRetryDelay(4), 2 * 3600_000);
|
|
70
|
-
});
|
|
71
|
-
it("count=5 returns null (exhausted)", () => {
|
|
72
|
-
assert.strictEqual(computeRetryDelay(5), null);
|
|
73
|
-
});
|
|
74
|
-
it("count=-1 returns null (negative treated as exhausted)", () => {
|
|
75
|
-
assert.strictEqual(computeRetryDelay(-1), null);
|
|
76
|
-
});
|
|
77
|
-
it("uses RETRY_INTERVALS as default intervals", () => {
|
|
78
|
-
for (let i = 0; i < RETRY_INTERVALS.length; i++) {
|
|
79
|
-
assert.strictEqual(computeRetryDelay(i), RETRY_INTERVALS[i]);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
it("accepts a custom intervals array", () => {
|
|
83
|
-
const custom = [100, 200, 300];
|
|
84
|
-
assert.strictEqual(computeRetryDelay(0, custom), 100);
|
|
85
|
-
assert.strictEqual(computeRetryDelay(2, custom), 300);
|
|
86
|
-
assert.strictEqual(computeRetryDelay(3, custom), null);
|
|
87
|
-
});
|
|
88
|
-
});
|