akemon 0.3.2 → 0.3.4
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 +6 -1
- package/dist/cli.js +209 -19
- package/dist/server.js +236 -2
- package/dist/software-agent-memory.js +141 -0
- package/dist/software-agent-peripheral.js +297 -24
- 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/event-bus.test.js +0 -51
- 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/software-agent-http.test.js +0 -108
- package/dist/software-agent-peripheral.test.js +0 -187
- package/dist/task-helpers.test.js +0 -88
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { buildLLMContext, loadConversation } from "./context.js";
|
|
2
|
+
import { buildRoleContext, loadRoles, resolveRoles } from "./role-module.js";
|
|
3
|
+
const DEFAULT_CONTEXT_BUDGET = 6000;
|
|
4
|
+
export async function buildSoftwareAgentMemorySummary(opts) {
|
|
5
|
+
const budget = opts.contextBudget ?? DEFAULT_CONTEXT_BUDGET;
|
|
6
|
+
const parts = [
|
|
7
|
+
"[Akemon memory boundary]",
|
|
8
|
+
`Role scope: ${opts.envelope.roleScope}`,
|
|
9
|
+
`Memory scope: ${opts.envelope.memoryScope}`,
|
|
10
|
+
boundaryDescription(opts.envelope.roleScope, opts.envelope.memoryScope),
|
|
11
|
+
];
|
|
12
|
+
if (opts.envelope.memoryScope === "none") {
|
|
13
|
+
parts.push("No Akemon memory/context is included for this task.");
|
|
14
|
+
return parts.join("\n");
|
|
15
|
+
}
|
|
16
|
+
const request = normalizeRequest(opts.request);
|
|
17
|
+
const roleTrigger = readRequestString(request, "roleTrigger") || triggerForRoleScope(opts.envelope.roleScope);
|
|
18
|
+
const productName = readRequestString(request, "productName");
|
|
19
|
+
const productId = readRequestString(request, "productId");
|
|
20
|
+
const rolePolicy = await resolveRoleMemoryPolicy(opts.workdir, opts.agentName, roleTrigger);
|
|
21
|
+
if (rolePolicy.exclude.length) {
|
|
22
|
+
parts.push(`Active role exclusions: ${rolePolicy.exclude.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
const roleContext = await buildRoleContext(opts.workdir, opts.agentName, roleTrigger, productName, productId);
|
|
25
|
+
if (roleContext.trim()) {
|
|
26
|
+
parts.push("");
|
|
27
|
+
parts.push("[Role/product context]");
|
|
28
|
+
parts.push(limitText(roleContext.trim(), Math.floor(budget * 0.55)));
|
|
29
|
+
}
|
|
30
|
+
const taskContext = readRequestString(request, "taskContext");
|
|
31
|
+
if (taskContext) {
|
|
32
|
+
parts.push("");
|
|
33
|
+
parts.push("[Task-provided context]");
|
|
34
|
+
parts.push(limitText(taskContext, Math.floor(budget * 0.25)));
|
|
35
|
+
}
|
|
36
|
+
const conversationId = readRequestString(request, "conversationId");
|
|
37
|
+
if (conversationId && canIncludeConversation(opts.envelope.roleScope, opts.envelope.memoryScope)) {
|
|
38
|
+
const conv = await loadConversation(opts.workdir, opts.agentName, conversationId);
|
|
39
|
+
const { text } = buildLLMContext(conv, Math.floor(budget * 0.3));
|
|
40
|
+
if (text.trim()) {
|
|
41
|
+
parts.push("");
|
|
42
|
+
parts.push("[Conversation context]");
|
|
43
|
+
parts.push(text.trim());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (conversationId) {
|
|
47
|
+
parts.push("");
|
|
48
|
+
parts.push("[Excluded conversation context]");
|
|
49
|
+
parts.push("A conversationId was supplied, but conversation memory is only included for owner-scoped software-agent tasks in v1.");
|
|
50
|
+
}
|
|
51
|
+
const ownerMemory = readRequestString(request, "memorySummary");
|
|
52
|
+
if (ownerMemory && canIncludeOwnerMemory(opts.envelope.roleScope, opts.envelope.memoryScope)) {
|
|
53
|
+
if (roleExcludesOwnerMemory(rolePolicy)) {
|
|
54
|
+
parts.push("");
|
|
55
|
+
parts.push("[Role-excluded owner memory]");
|
|
56
|
+
parts.push(`The active role (${rolePolicy.roleName || "unknown"}) excludes ${rolePolicy.exclude.join(", ")}, so owner-provided memory was not included.`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
parts.push("");
|
|
60
|
+
parts.push("[Owner-visible memory]");
|
|
61
|
+
parts.push(limitText(ownerMemory, Math.floor(budget * 0.35)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (ownerMemory) {
|
|
65
|
+
parts.push("");
|
|
66
|
+
parts.push("[Excluded owner memory]");
|
|
67
|
+
parts.push("A memorySummary was supplied, but it was not included because this envelope is not owner/owner scoped.");
|
|
68
|
+
}
|
|
69
|
+
return limitText(parts.join("\n"), budget);
|
|
70
|
+
}
|
|
71
|
+
export function canIncludeOwnerMemory(roleScope, memoryScope) {
|
|
72
|
+
return roleScope === "owner" && memoryScope === "owner";
|
|
73
|
+
}
|
|
74
|
+
function canIncludeConversation(roleScope, memoryScope) {
|
|
75
|
+
return roleScope === "owner" && (memoryScope === "owner" || memoryScope === "task");
|
|
76
|
+
}
|
|
77
|
+
function triggerForRoleScope(roleScope) {
|
|
78
|
+
switch (roleScope) {
|
|
79
|
+
case "owner": return "trigger:chat:owner";
|
|
80
|
+
case "public": return "trigger:chat:public";
|
|
81
|
+
case "order": return "trigger:order";
|
|
82
|
+
case "agent": return "trigger:agent_call";
|
|
83
|
+
case "system": return "trigger:system";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function boundaryDescription(roleScope, memoryScope) {
|
|
87
|
+
if (roleScope === "owner" && memoryScope === "owner") {
|
|
88
|
+
return "Owner-scoped task: owner-visible memory may be included after Akemon-side selection.";
|
|
89
|
+
}
|
|
90
|
+
if (memoryScope === "none") {
|
|
91
|
+
return "No-memory task: do not use Akemon private memory, conversation history, or subjective state.";
|
|
92
|
+
}
|
|
93
|
+
return "Non-owner task: exclude owner private conversations, personal notes, bio state, diary, subjective impressions, and owner-only memory.";
|
|
94
|
+
}
|
|
95
|
+
async function resolveRoleMemoryPolicy(workdir, agentName, roleTrigger) {
|
|
96
|
+
const roles = await loadRoles(workdir, agentName);
|
|
97
|
+
const { primary } = resolveRoles(roles, roleTrigger);
|
|
98
|
+
return {
|
|
99
|
+
roleName: primary?.name || null,
|
|
100
|
+
exclude: primary?.exclude || [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function roleExcludesOwnerMemory(policy) {
|
|
104
|
+
return policy.exclude.some((item) => {
|
|
105
|
+
const normalized = item.toLowerCase();
|
|
106
|
+
return normalized.includes("owner")
|
|
107
|
+
|| normalized.includes("private")
|
|
108
|
+
|| normalized.includes("personal")
|
|
109
|
+
|| normalized.includes("note")
|
|
110
|
+
|| normalized.includes("diary")
|
|
111
|
+
|| normalized.includes("bio")
|
|
112
|
+
|| normalized.includes("全部记忆")
|
|
113
|
+
|| normalized.includes("个人")
|
|
114
|
+
|| normalized.includes("笔记")
|
|
115
|
+
|| normalized.includes("日记")
|
|
116
|
+
|| normalized.includes("状态");
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function normalizeRequest(value) {
|
|
120
|
+
if (value === undefined || value === null)
|
|
121
|
+
return {};
|
|
122
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
123
|
+
throw new Error("Invalid request: expected object");
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function readRequestString(request, field) {
|
|
128
|
+
const value = request[field];
|
|
129
|
+
if (value === undefined || value === null)
|
|
130
|
+
return "";
|
|
131
|
+
if (typeof value !== "string")
|
|
132
|
+
throw new Error(`Invalid ${field}: expected string`);
|
|
133
|
+
return value.trim();
|
|
134
|
+
}
|
|
135
|
+
function limitText(text, maxChars) {
|
|
136
|
+
if (text.length <= maxChars)
|
|
137
|
+
return text;
|
|
138
|
+
const head = Math.floor(maxChars * 0.45);
|
|
139
|
+
const tail = Math.max(0, maxChars - head - 40);
|
|
140
|
+
return `${text.slice(0, head)}\n[truncated ${text.length - head - tail} chars]\n${text.slice(-tail)}`;
|
|
141
|
+
}
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* transport to app-server or a true persistent interactive session.
|
|
12
12
|
*/
|
|
13
13
|
import { randomUUID } from "crypto";
|
|
14
|
-
import { spawn } from "child_process";
|
|
14
|
+
import { spawn, spawnSync } from "child_process";
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
16
|
+
import { isAbsolute, join, relative, resolve as resolvePath } from "path";
|
|
15
17
|
import { StringDecoder } from "string_decoder";
|
|
16
18
|
import { SIG, sig } from "./types.js";
|
|
17
19
|
import { sendTaskEnd, sendTaskStart, sendTaskStream } from "./relay-client.js";
|
|
@@ -30,6 +32,9 @@ const DEFAULT_OWNER_FORBIDDEN_ACTIONS = [
|
|
|
30
32
|
const ROLE_SCOPES = ["owner", "public", "order", "agent", "system"];
|
|
31
33
|
const MEMORY_SCOPES = ["none", "public", "task", "owner"];
|
|
32
34
|
const RISK_LEVELS = ["low", "medium", "high"];
|
|
35
|
+
const MAX_STREAM_SUMMARY_CHARS = 12_000;
|
|
36
|
+
const STREAM_SUMMARY_HEAD_CHARS = 4_000;
|
|
37
|
+
const STREAM_SUMMARY_TAIL_CHARS = 8_000;
|
|
33
38
|
export class CodexSoftwareAgentPeripheral {
|
|
34
39
|
id;
|
|
35
40
|
name;
|
|
@@ -39,6 +44,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
39
44
|
bus = null;
|
|
40
45
|
activeChild = null;
|
|
41
46
|
activeTaskId = null;
|
|
47
|
+
activeWorkdir = null;
|
|
42
48
|
sessionId = randomUUID();
|
|
43
49
|
constructor(config) {
|
|
44
50
|
this.config = config;
|
|
@@ -73,15 +79,21 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
73
79
|
}
|
|
74
80
|
this.activeChild = null;
|
|
75
81
|
this.activeTaskId = null;
|
|
82
|
+
this.activeWorkdir = null;
|
|
76
83
|
this.sessionId = randomUUID();
|
|
77
84
|
}
|
|
78
85
|
getState() {
|
|
86
|
+
const currentWorkdir = this.activeWorkdir || resolvePath(this.config.workdir);
|
|
79
87
|
return {
|
|
80
88
|
id: this.id,
|
|
81
89
|
sessionId: this.sessionId,
|
|
82
90
|
activeTaskId: this.activeTaskId,
|
|
91
|
+
activeWorkdir: this.activeWorkdir,
|
|
83
92
|
busy: !!this.activeChild,
|
|
84
93
|
transport: "codex-exec",
|
|
94
|
+
baseWorkdir: resolvePath(this.config.workdir),
|
|
95
|
+
workdirStatus: this.collectWorkdirStatus(currentWorkdir),
|
|
96
|
+
taskLedgerDir: this.config.taskLedgerDir,
|
|
85
97
|
};
|
|
86
98
|
}
|
|
87
99
|
async send(signal) {
|
|
@@ -97,13 +109,16 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
97
109
|
const result = await this.sendTask(envelope);
|
|
98
110
|
return sig(SIG.SOFTWARE_AGENT_RESPONSE, { ...result }, this.id);
|
|
99
111
|
}
|
|
100
|
-
async sendTask(envelope,
|
|
112
|
+
async sendTask(envelope, taskOptions) {
|
|
101
113
|
if (this.activeChild) {
|
|
102
114
|
throw new Error(`Software agent busy (task=${this.activeTaskId})`);
|
|
103
115
|
}
|
|
116
|
+
const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
|
|
104
117
|
const taskId = envelope.taskId || `sw_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
118
|
+
const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
|
|
119
|
+
const workdir = workdirSafety.effectiveWorkdir;
|
|
120
|
+
const effectiveEnvelope = { ...envelope, taskId, workdir, workdirSafety };
|
|
121
|
+
const prompt = buildTaskEnvelopePrompt(effectiveEnvelope);
|
|
107
122
|
const { cmd, args } = buildCodexExecCommand({
|
|
108
123
|
command: this.config.command || "codex",
|
|
109
124
|
workdir,
|
|
@@ -111,17 +126,38 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
111
126
|
sandbox: this.config.sandbox || "workspace-write",
|
|
112
127
|
});
|
|
113
128
|
const startedAt = Date.now();
|
|
129
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
114
130
|
const relay = this.config.taskRelay || defaultTaskRelay;
|
|
115
131
|
const commandLine = [cmd, ...args].join(" ");
|
|
116
132
|
const spawnImpl = this.config.spawnImpl || spawn;
|
|
117
133
|
const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
|
|
134
|
+
const workdirStatus = this.collectWorkdirStatus(workdir);
|
|
135
|
+
const baseTaskRecord = () => ({
|
|
136
|
+
schemaVersion: 1,
|
|
137
|
+
taskId,
|
|
138
|
+
agentId: this.id,
|
|
139
|
+
sessionId: this.sessionId,
|
|
140
|
+
transport: "codex-exec",
|
|
141
|
+
commandLine,
|
|
142
|
+
envelope: effectiveEnvelope,
|
|
143
|
+
startedAt: startedAtIso,
|
|
144
|
+
workdirStatus,
|
|
145
|
+
});
|
|
118
146
|
return new Promise((resolve) => {
|
|
119
147
|
this.activeTaskId = taskId;
|
|
120
|
-
|
|
148
|
+
this.activeWorkdir = workdir;
|
|
149
|
+
this.writeTaskRecord({
|
|
150
|
+
...baseTaskRecord(),
|
|
151
|
+
status: "running",
|
|
152
|
+
updatedAt: startedAtIso,
|
|
153
|
+
});
|
|
154
|
+
const origin = "software_agent";
|
|
155
|
+
relay.sendTaskStart(taskId, origin, commandLine);
|
|
156
|
+
observer?.onStart?.({ taskId, origin, commandLine });
|
|
121
157
|
this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
|
|
122
158
|
taskId,
|
|
123
159
|
taskType: "software_agent",
|
|
124
|
-
description:
|
|
160
|
+
description: effectiveEnvelope.goal,
|
|
125
161
|
peripheral: this.id,
|
|
126
162
|
sessionId: this.sessionId,
|
|
127
163
|
}, this.id));
|
|
@@ -137,8 +173,8 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
137
173
|
catch (err) {
|
|
138
174
|
this.activeChild = null;
|
|
139
175
|
this.activeTaskId = null;
|
|
176
|
+
this.activeWorkdir = null;
|
|
140
177
|
const durationMs = Date.now() - startedAt;
|
|
141
|
-
relay.sendTaskEnd(taskId, null, durationMs);
|
|
142
178
|
const result = {
|
|
143
179
|
success: false,
|
|
144
180
|
taskId,
|
|
@@ -147,6 +183,19 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
147
183
|
exitCode: null,
|
|
148
184
|
durationMs,
|
|
149
185
|
};
|
|
186
|
+
relay.sendTaskEnd(taskId, null, durationMs);
|
|
187
|
+
observer?.onEnd?.({ taskId, exitCode: null, durationMs, result });
|
|
188
|
+
const completedAt = new Date().toISOString();
|
|
189
|
+
this.writeTaskRecord({
|
|
190
|
+
...baseTaskRecord(),
|
|
191
|
+
status: "failed",
|
|
192
|
+
updatedAt: completedAt,
|
|
193
|
+
completedAt,
|
|
194
|
+
durationMs,
|
|
195
|
+
result,
|
|
196
|
+
stdoutSummary: summarizeText(""),
|
|
197
|
+
stderrSummary: summarizeText(result.error || ""),
|
|
198
|
+
});
|
|
150
199
|
this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
|
|
151
200
|
resolve(result);
|
|
152
201
|
return;
|
|
@@ -169,15 +218,17 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
169
218
|
if (tailOut) {
|
|
170
219
|
stdout += tailOut;
|
|
171
220
|
relay.sendTaskStream(taskId, "stdout", tailOut);
|
|
221
|
+
observer?.onStream?.({ taskId, stream: "stdout", chunk: tailOut });
|
|
172
222
|
}
|
|
173
223
|
if (tailErr) {
|
|
174
224
|
stderr += tailErr;
|
|
175
225
|
relay.sendTaskStream(taskId, "stderr", tailErr);
|
|
226
|
+
observer?.onStream?.({ taskId, stream: "stderr", chunk: tailErr });
|
|
176
227
|
}
|
|
177
228
|
const durationMs = Date.now() - startedAt;
|
|
178
|
-
relay.sendTaskEnd(taskId, exitCode, durationMs);
|
|
179
229
|
this.activeChild = null;
|
|
180
230
|
this.activeTaskId = null;
|
|
231
|
+
this.activeWorkdir = null;
|
|
181
232
|
const output = stdout.trim() || stderr.trim();
|
|
182
233
|
const success = !error && !aborted && exitCode === 0;
|
|
183
234
|
const result = {
|
|
@@ -188,6 +239,19 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
188
239
|
exitCode,
|
|
189
240
|
durationMs,
|
|
190
241
|
};
|
|
242
|
+
relay.sendTaskEnd(taskId, exitCode, durationMs);
|
|
243
|
+
observer?.onEnd?.({ taskId, exitCode, durationMs, result });
|
|
244
|
+
const completedAt = new Date().toISOString();
|
|
245
|
+
this.writeTaskRecord({
|
|
246
|
+
...baseTaskRecord(),
|
|
247
|
+
status: success ? "completed" : "failed",
|
|
248
|
+
updatedAt: completedAt,
|
|
249
|
+
completedAt,
|
|
250
|
+
durationMs,
|
|
251
|
+
result,
|
|
252
|
+
stdoutSummary: summarizeText(stdout),
|
|
253
|
+
stderrSummary: summarizeText(stderr),
|
|
254
|
+
});
|
|
191
255
|
this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
|
|
192
256
|
...result,
|
|
193
257
|
taskLabel: `software_agent:${this.id}`,
|
|
@@ -239,6 +303,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
239
303
|
return;
|
|
240
304
|
stdout += text;
|
|
241
305
|
relay.sendTaskStream(taskId, "stdout", text);
|
|
306
|
+
observer?.onStream?.({ taskId, stream: "stdout", chunk: text });
|
|
242
307
|
});
|
|
243
308
|
child.stderr?.on("data", (chunk) => {
|
|
244
309
|
const text = errDecoder.write(chunk);
|
|
@@ -246,6 +311,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
246
311
|
return;
|
|
247
312
|
stderr += text;
|
|
248
313
|
relay.sendTaskStream(taskId, "stderr", text);
|
|
314
|
+
observer?.onStream?.({ taskId, stream: "stderr", chunk: text });
|
|
249
315
|
});
|
|
250
316
|
child.on("close", (code) => {
|
|
251
317
|
child.unref();
|
|
@@ -257,6 +323,48 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
257
323
|
});
|
|
258
324
|
});
|
|
259
325
|
}
|
|
326
|
+
writeTaskRecord(record) {
|
|
327
|
+
const dir = this.config.taskLedgerDir;
|
|
328
|
+
if (!dir)
|
|
329
|
+
return;
|
|
330
|
+
try {
|
|
331
|
+
mkdirSync(dir, { recursive: true });
|
|
332
|
+
const safeTaskId = safeTaskFilename(record.taskId);
|
|
333
|
+
writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(record, null, 2)}\n`);
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
collectWorkdirStatus(workdir) {
|
|
340
|
+
const impl = this.config.gitStatusImpl || readGitWorktreeStatus;
|
|
341
|
+
return impl(workdir);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
export function summarizeText(text, maxChars = MAX_STREAM_SUMMARY_CHARS) {
|
|
345
|
+
const normalized = text || "";
|
|
346
|
+
const chars = normalized.length;
|
|
347
|
+
const bytes = Buffer.byteLength(normalized, "utf8");
|
|
348
|
+
const lines = normalized ? normalized.split(/\r\n|\r|\n/).length : 0;
|
|
349
|
+
if (chars <= maxChars) {
|
|
350
|
+
return { chars, bytes, lines, text: normalized, truncated: false };
|
|
351
|
+
}
|
|
352
|
+
const headChars = Math.min(STREAM_SUMMARY_HEAD_CHARS, maxChars);
|
|
353
|
+
const tailChars = Math.max(0, maxChars - headChars);
|
|
354
|
+
const omittedChars = chars - headChars - tailChars;
|
|
355
|
+
const tailText = tailChars > 0 ? normalized.slice(-tailChars) : "";
|
|
356
|
+
return {
|
|
357
|
+
chars,
|
|
358
|
+
bytes,
|
|
359
|
+
lines,
|
|
360
|
+
text: [
|
|
361
|
+
normalized.slice(0, headChars),
|
|
362
|
+
`[truncated ${omittedChars} chars]`,
|
|
363
|
+
tailText,
|
|
364
|
+
].join("\n"),
|
|
365
|
+
truncated: true,
|
|
366
|
+
omittedChars,
|
|
367
|
+
};
|
|
260
368
|
}
|
|
261
369
|
export function buildTaskEnvelopePrompt(envelope) {
|
|
262
370
|
const lines = [
|
|
@@ -269,11 +377,15 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
269
377
|
`Memory scope: ${envelope.memoryScope}`,
|
|
270
378
|
`Risk level: ${envelope.riskLevel}`,
|
|
271
379
|
`Workdir: ${envelope.workdir}`,
|
|
272
|
-
"",
|
|
273
|
-
"Goal:",
|
|
274
|
-
envelope.goal,
|
|
275
|
-
"",
|
|
276
380
|
];
|
|
381
|
+
if (envelope.workdirSafety) {
|
|
382
|
+
lines.push(`Base workdir: ${envelope.workdirSafety.baseWorkdir}`);
|
|
383
|
+
lines.push(`Requested workdir: ${envelope.workdirSafety.requestedWorkdir}`);
|
|
384
|
+
lines.push(`Effective workdir: ${envelope.workdirSafety.effectiveWorkdir}`);
|
|
385
|
+
lines.push(`Outside base workdir: ${envelope.workdirSafety.outsideBaseWorkdir ? "yes" : "no"}`);
|
|
386
|
+
lines.push(`Outside workdir explicitly allowed: ${envelope.workdirSafety.allowOutsideWorkdir ? "yes" : "no"}`);
|
|
387
|
+
}
|
|
388
|
+
lines.push("", "Goal:", envelope.goal, "");
|
|
277
389
|
if (envelope.memorySummary?.trim()) {
|
|
278
390
|
lines.push("Visible Akemon memory/context:");
|
|
279
391
|
lines.push(envelope.memorySummary.trim());
|
|
@@ -307,27 +419,123 @@ export function createOwnerTaskEnvelope(body, defaultWorkdir) {
|
|
|
307
419
|
const goal = typeof body?.goal === "string" ? body.goal.trim() : "";
|
|
308
420
|
if (!goal)
|
|
309
421
|
throw new Error("Missing required string field: goal");
|
|
310
|
-
const callerForbiddenActions = readOptionalStringArray(body
|
|
422
|
+
const callerForbiddenActions = readOptionalStringArray(body?.forbiddenActions, "forbiddenActions");
|
|
423
|
+
const requestedWorkdir = readOptionalString(body?.workdir, "workdir") || defaultWorkdir;
|
|
424
|
+
const workdirSafety = resolveWorkdirSafety(defaultWorkdir, requestedWorkdir, readOptionalBoolean(body?.allowOutsideWorkdir, "allowOutsideWorkdir") || false);
|
|
311
425
|
return {
|
|
312
|
-
taskId: readOptionalString(body
|
|
426
|
+
taskId: readOptionalString(body?.taskId, "taskId"),
|
|
313
427
|
sourceModule: "owner-http",
|
|
314
|
-
purpose: readOptionalString(body
|
|
428
|
+
purpose: readOptionalString(body?.purpose, "purpose") || "owner software-agent task",
|
|
315
429
|
goal,
|
|
316
|
-
workdir:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
430
|
+
workdir: workdirSafety.effectiveWorkdir,
|
|
431
|
+
workdirSafety,
|
|
432
|
+
roleScope: readEnum(body?.roleScope, "roleScope", ROLE_SCOPES, "owner"),
|
|
433
|
+
memoryScope: readEnum(body?.memoryScope, "memoryScope", MEMORY_SCOPES, "owner"),
|
|
434
|
+
riskLevel: readEnum(body?.riskLevel, "riskLevel", RISK_LEVELS, "medium"),
|
|
435
|
+
allowedActions: body?.allowedActions !== undefined
|
|
436
|
+
? readOptionalStringArray(body?.allowedActions, "allowedActions")
|
|
322
437
|
: [...DEFAULT_OWNER_ALLOWED_ACTIONS],
|
|
323
438
|
forbiddenActions: [...new Set([...DEFAULT_OWNER_FORBIDDEN_ACTIONS, ...callerForbiddenActions])],
|
|
324
|
-
memorySummary: typeof body
|
|
325
|
-
deliverable: typeof body
|
|
439
|
+
memorySummary: typeof body?.memorySummary === "string" ? body.memorySummary : "",
|
|
440
|
+
deliverable: typeof body?.deliverable === "string"
|
|
326
441
|
? body.deliverable
|
|
327
442
|
: "Return a concise engineering summary with changes, verification, and remaining risks.",
|
|
328
|
-
timeoutMs: readTimeoutMs(body
|
|
443
|
+
timeoutMs: readTimeoutMs(body?.timeoutMs),
|
|
329
444
|
};
|
|
330
445
|
}
|
|
446
|
+
export function resolveWorkdirSafety(baseWorkdir, requestedWorkdir, allowOutsideWorkdir = false) {
|
|
447
|
+
const base = resolvePath(baseWorkdir);
|
|
448
|
+
const requested = isAbsolute(requestedWorkdir)
|
|
449
|
+
? resolvePath(requestedWorkdir)
|
|
450
|
+
: resolvePath(base, requestedWorkdir);
|
|
451
|
+
const rel = relative(base, requested);
|
|
452
|
+
const outsideBaseWorkdir = !!rel && (rel.startsWith("..") || isAbsolute(rel));
|
|
453
|
+
if (outsideBaseWorkdir && !allowOutsideWorkdir) {
|
|
454
|
+
throw new Error(`Invalid workdir: ${requested} is outside base workdir ${base}`);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
baseWorkdir: base,
|
|
458
|
+
requestedWorkdir,
|
|
459
|
+
effectiveWorkdir: requested,
|
|
460
|
+
allowOutsideWorkdir,
|
|
461
|
+
outsideBaseWorkdir,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
export function readGitWorktreeStatus(workdir) {
|
|
465
|
+
const resolvedWorkdir = resolvePath(workdir);
|
|
466
|
+
try {
|
|
467
|
+
const rootResult = spawnSync("git", ["-C", resolvedWorkdir, "rev-parse", "--show-toplevel"], {
|
|
468
|
+
encoding: "utf8",
|
|
469
|
+
timeout: 5000,
|
|
470
|
+
});
|
|
471
|
+
if (rootResult.status !== 0) {
|
|
472
|
+
return {
|
|
473
|
+
workdir: resolvedWorkdir,
|
|
474
|
+
isRepo: false,
|
|
475
|
+
dirty: false,
|
|
476
|
+
changedFiles: [],
|
|
477
|
+
error: summarizeGitError(rootResult.stderr, rootResult.error),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const root = String(rootResult.stdout || "").trim();
|
|
481
|
+
const statusResult = spawnSync("git", ["-C", resolvedWorkdir, "status", "--short"], {
|
|
482
|
+
encoding: "utf8",
|
|
483
|
+
timeout: 5000,
|
|
484
|
+
});
|
|
485
|
+
if (statusResult.status !== 0) {
|
|
486
|
+
return {
|
|
487
|
+
workdir: resolvedWorkdir,
|
|
488
|
+
isRepo: true,
|
|
489
|
+
dirty: false,
|
|
490
|
+
changedFiles: [],
|
|
491
|
+
root,
|
|
492
|
+
error: summarizeGitError(statusResult.stderr, statusResult.error),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const changedFiles = String(statusResult.stdout || "")
|
|
496
|
+
.split(/\r?\n/)
|
|
497
|
+
.map((line) => line.trimEnd())
|
|
498
|
+
.filter(Boolean)
|
|
499
|
+
.map((line) => line.slice(3).trim())
|
|
500
|
+
.filter(Boolean);
|
|
501
|
+
return {
|
|
502
|
+
workdir: resolvedWorkdir,
|
|
503
|
+
isRepo: true,
|
|
504
|
+
dirty: changedFiles.length > 0,
|
|
505
|
+
changedFiles,
|
|
506
|
+
root,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
return {
|
|
511
|
+
workdir: resolvedWorkdir,
|
|
512
|
+
isRepo: false,
|
|
513
|
+
dirty: false,
|
|
514
|
+
changedFiles: [],
|
|
515
|
+
error: err.message || String(err),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
|
|
520
|
+
const safeLimit = normalizeTaskRecordLimit(limit);
|
|
521
|
+
try {
|
|
522
|
+
if (!existsSync(taskLedgerDir))
|
|
523
|
+
return [];
|
|
524
|
+
return readdirSync(taskLedgerDir, { withFileTypes: true })
|
|
525
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
526
|
+
.map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
|
|
527
|
+
.filter((record) => !!record)
|
|
528
|
+
.sort(compareSoftwareAgentTaskRecords)
|
|
529
|
+
.slice(0, safeLimit);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
|
|
536
|
+
const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
|
|
537
|
+
return readSoftwareAgentTaskRecordFile(file);
|
|
538
|
+
}
|
|
331
539
|
function readOptionalString(value, field) {
|
|
332
540
|
if (value === undefined || value === null)
|
|
333
541
|
return undefined;
|
|
@@ -336,6 +544,19 @@ function readOptionalString(value, field) {
|
|
|
336
544
|
const trimmed = value.trim();
|
|
337
545
|
return trimmed || undefined;
|
|
338
546
|
}
|
|
547
|
+
function normalizeSoftwareAgentTaskOptions(options) {
|
|
548
|
+
if (!options)
|
|
549
|
+
return {};
|
|
550
|
+
if (isAbortSignal(options)) {
|
|
551
|
+
return { signal: options };
|
|
552
|
+
}
|
|
553
|
+
return options;
|
|
554
|
+
}
|
|
555
|
+
function isAbortSignal(value) {
|
|
556
|
+
return !!value
|
|
557
|
+
&& typeof value.aborted === "boolean"
|
|
558
|
+
&& typeof value.addEventListener === "function";
|
|
559
|
+
}
|
|
339
560
|
function readOptionalStringArray(value, field) {
|
|
340
561
|
if (value === undefined || value === null)
|
|
341
562
|
return [];
|
|
@@ -348,6 +569,13 @@ function readOptionalStringArray(value, field) {
|
|
|
348
569
|
return item.trim();
|
|
349
570
|
});
|
|
350
571
|
}
|
|
572
|
+
function readOptionalBoolean(value, field) {
|
|
573
|
+
if (value === undefined || value === null)
|
|
574
|
+
return undefined;
|
|
575
|
+
if (typeof value !== "boolean")
|
|
576
|
+
throw new Error(`Invalid ${field}: expected boolean`);
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
351
579
|
function readEnum(value, field, allowed, fallback) {
|
|
352
580
|
if (value === undefined || value === null || value === "")
|
|
353
581
|
return fallback;
|
|
@@ -367,6 +595,51 @@ function readTimeoutMs(value) {
|
|
|
367
595
|
}
|
|
368
596
|
return value;
|
|
369
597
|
}
|
|
598
|
+
function safeTaskFilename(taskId) {
|
|
599
|
+
const safe = taskId.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 200);
|
|
600
|
+
return safe || "task";
|
|
601
|
+
}
|
|
602
|
+
function summarizeGitError(stderr, error) {
|
|
603
|
+
if (error)
|
|
604
|
+
return error.message;
|
|
605
|
+
const text = typeof stderr === "string" ? stderr.trim() : "";
|
|
606
|
+
return text || undefined;
|
|
607
|
+
}
|
|
608
|
+
function normalizeTaskRecordLimit(limit) {
|
|
609
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
610
|
+
return 20;
|
|
611
|
+
return Math.min(limit, 100);
|
|
612
|
+
}
|
|
613
|
+
function readSoftwareAgentTaskRecordFile(file) {
|
|
614
|
+
try {
|
|
615
|
+
if (!existsSync(file))
|
|
616
|
+
return null;
|
|
617
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
618
|
+
if (!isSoftwareAgentTaskRecord(parsed))
|
|
619
|
+
return null;
|
|
620
|
+
return parsed;
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function isSoftwareAgentTaskRecord(value) {
|
|
627
|
+
return value
|
|
628
|
+
&& value.schemaVersion === 1
|
|
629
|
+
&& typeof value.taskId === "string"
|
|
630
|
+
&& (value.status === "running" || value.status === "completed" || value.status === "failed")
|
|
631
|
+
&& typeof value.startedAt === "string"
|
|
632
|
+
&& typeof value.updatedAt === "string"
|
|
633
|
+
&& value.envelope
|
|
634
|
+
&& typeof value.envelope.goal === "string";
|
|
635
|
+
}
|
|
636
|
+
function compareSoftwareAgentTaskRecords(a, b) {
|
|
637
|
+
const bTime = Date.parse(b.updatedAt || b.startedAt) || 0;
|
|
638
|
+
const aTime = Date.parse(a.updatedAt || a.startedAt) || 0;
|
|
639
|
+
if (bTime !== aTime)
|
|
640
|
+
return bTime - aTime;
|
|
641
|
+
return b.taskId.localeCompare(a.taskId);
|
|
642
|
+
}
|
|
370
643
|
function buildCodexExecCommand(opts) {
|
|
371
644
|
const args = [
|
|
372
645
|
"exec",
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akemon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Agent work marketplace — train your agent, let it work for others",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/lhead/akemon"
|
|
9
|
+
"url": "git+https://github.com/lhead/akemon.git"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"ai",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"gemini"
|
|
19
19
|
],
|
|
20
20
|
"bin": {
|
|
21
|
-
"akemon": "
|
|
21
|
+
"akemon": "dist/cli.js"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc && cp src/live.html dist/live.html",
|
|
29
|
+
"build": "rm -rf dist && tsc && cp src/live.html dist/live.html",
|
|
30
30
|
"dev": "tsc --watch",
|
|
31
31
|
"start": "node dist/cli.js",
|
|
32
32
|
"postinstall": "node scripts/fix-node-pty.cjs",
|