akemon 0.3.3 → 0.3.5
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 +24 -1
- package/dist/cli.js +218 -14
- package/dist/engine-peripheral.js +5 -4
- package/dist/engine-routing.js +99 -0
- package/dist/event-bus.js +63 -17
- package/dist/privacy-filter.js +269 -0
- package/dist/redaction.js +159 -0
- package/dist/relay-client.js +39 -2
- package/dist/server.js +222 -52
- package/dist/software-agent-memory.js +139 -0
- package/dist/software-agent-peripheral.js +599 -33
- package/dist/software-agent-stream-cli.js +101 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
2
2
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
3
|
import { exec } from "child_process";
|
|
4
4
|
import { scanAndKillOrphans } from "./orphan-scan.js";
|
|
5
|
+
import { timingSafeEqual } from "node:crypto";
|
|
5
6
|
import { createServer } from "http";
|
|
6
7
|
import { createInterface } from "readline";
|
|
7
8
|
import { mkdir } from "fs/promises";
|
|
@@ -86,8 +87,35 @@ function bearerToken(req) {
|
|
|
86
87
|
}
|
|
87
88
|
function isOwnerRequest(req, options) {
|
|
88
89
|
const token = bearerToken(req);
|
|
89
|
-
const validTokens = [options.secretKey, options.key]
|
|
90
|
-
|
|
90
|
+
const validTokens = [options.secretKey, options.key]
|
|
91
|
+
.filter((validToken) => typeof validToken === "string" && validToken.length > 0);
|
|
92
|
+
return !!token && validTokens.some((validToken) => constantTimeTokenEqual(token, validToken));
|
|
93
|
+
}
|
|
94
|
+
function constantTimeTokenEqual(left, right) {
|
|
95
|
+
const leftBuffer = Buffer.from(left);
|
|
96
|
+
const rightBuffer = Buffer.from(right);
|
|
97
|
+
const length = Math.max(leftBuffer.length, rightBuffer.length, 1);
|
|
98
|
+
const leftPadded = Buffer.alloc(length);
|
|
99
|
+
const rightPadded = Buffer.alloc(length);
|
|
100
|
+
leftBuffer.copy(leftPadded);
|
|
101
|
+
rightBuffer.copy(rightPadded);
|
|
102
|
+
return timingSafeEqual(leftPadded, rightPadded) && leftBuffer.length === rightBuffer.length;
|
|
103
|
+
}
|
|
104
|
+
function writeJsonResponse(res, statusCode, body, pretty = false) {
|
|
105
|
+
res.writeHead(statusCode, { "Content-Type": "application/json" })
|
|
106
|
+
.end(JSON.stringify(body, null, pretty ? 2 : 0));
|
|
107
|
+
}
|
|
108
|
+
function requireOwnerRequest(req, res, options) {
|
|
109
|
+
if (isOwnerRequest(req, options))
|
|
110
|
+
return true;
|
|
111
|
+
writeJsonResponse(res, 401, { error: "Owner token required" });
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function requireSoftwareAgent(res, softwareAgent) {
|
|
115
|
+
if (softwareAgent)
|
|
116
|
+
return softwareAgent;
|
|
117
|
+
writeJsonResponse(res, 503, { error: "Software agent peripheral not ready" });
|
|
118
|
+
return null;
|
|
91
119
|
}
|
|
92
120
|
function readJsonBody(req, maxBytes = 256 * 1024) {
|
|
93
121
|
return new Promise((resolve, reject) => {
|
|
@@ -118,79 +146,193 @@ function readJsonBody(req, maxBytes = 256 * 1024) {
|
|
|
118
146
|
req.on("error", reject);
|
|
119
147
|
});
|
|
120
148
|
}
|
|
121
|
-
|
|
122
|
-
if (!isOwnerRequest(req, deps.options)) {
|
|
123
|
-
res.writeHead(401, { "Content-Type": "application/json" })
|
|
124
|
-
.end(JSON.stringify({ error: "Owner token required" }));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (!deps.softwareAgent) {
|
|
128
|
-
res.writeHead(503, { "Content-Type": "application/json" })
|
|
129
|
-
.end(JSON.stringify({ error: "Software agent peripheral not ready" }));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
149
|
+
async function readOwnerSoftwareAgentEnvelope(req, res, deps) {
|
|
132
150
|
let body;
|
|
133
151
|
try {
|
|
134
152
|
body = await readJsonBody(req);
|
|
135
153
|
}
|
|
136
154
|
catch (err) {
|
|
137
|
-
res
|
|
138
|
-
|
|
139
|
-
return;
|
|
155
|
+
writeJsonResponse(res, 400, { error: err.message || "Invalid request body" });
|
|
156
|
+
return null;
|
|
140
157
|
}
|
|
141
|
-
let envelope;
|
|
142
158
|
try {
|
|
143
|
-
envelope = createOwnerTaskEnvelope(body, deps.workdir);
|
|
159
|
+
const envelope = createOwnerTaskEnvelope(body, deps.workdir);
|
|
160
|
+
envelope.memorySummary = await buildSoftwareAgentMemorySummary({
|
|
161
|
+
workdir: deps.workdir,
|
|
162
|
+
agentName: deps.agentName,
|
|
163
|
+
envelope,
|
|
164
|
+
request: body,
|
|
165
|
+
});
|
|
166
|
+
return envelope;
|
|
144
167
|
}
|
|
145
168
|
catch (err) {
|
|
146
|
-
res
|
|
147
|
-
|
|
148
|
-
return;
|
|
169
|
+
writeJsonResponse(res, 400, { error: err.message || "Invalid software-agent envelope" });
|
|
170
|
+
return null;
|
|
149
171
|
}
|
|
172
|
+
}
|
|
173
|
+
export async function handleSoftwareAgentRunHttp(req, res, deps) {
|
|
174
|
+
if (!requireOwnerRequest(req, res, deps.options))
|
|
175
|
+
return;
|
|
176
|
+
const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
|
|
177
|
+
if (!softwareAgent)
|
|
178
|
+
return;
|
|
179
|
+
const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
|
|
180
|
+
if (!envelope)
|
|
181
|
+
return;
|
|
150
182
|
try {
|
|
151
|
-
const result = await
|
|
152
|
-
res
|
|
153
|
-
.end(JSON.stringify(result, null, 2));
|
|
183
|
+
const result = await softwareAgent.sendTask(envelope);
|
|
184
|
+
writeJsonResponse(res, 200, redactSecrets(result), true);
|
|
154
185
|
}
|
|
155
186
|
catch (err) {
|
|
156
187
|
const busy = String(err.message || "").includes("busy");
|
|
157
|
-
res
|
|
158
|
-
|
|
188
|
+
writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
|
|
192
|
+
if (!requireOwnerRequest(req, res, deps.options))
|
|
193
|
+
return;
|
|
194
|
+
const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
|
|
195
|
+
if (!softwareAgent)
|
|
196
|
+
return;
|
|
197
|
+
const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
|
|
198
|
+
if (!envelope)
|
|
199
|
+
return;
|
|
200
|
+
const abortController = new AbortController();
|
|
201
|
+
let responseFinished = false;
|
|
202
|
+
let streamStarted = false;
|
|
203
|
+
res.on("close", () => {
|
|
204
|
+
if (!responseFinished)
|
|
205
|
+
abortController.abort();
|
|
206
|
+
});
|
|
207
|
+
const ensureStreamStarted = () => {
|
|
208
|
+
if (streamStarted)
|
|
209
|
+
return;
|
|
210
|
+
streamStarted = true;
|
|
211
|
+
res.writeHead(200, {
|
|
212
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
213
|
+
"Cache-Control": "no-cache",
|
|
214
|
+
"X-Accel-Buffering": "no",
|
|
215
|
+
});
|
|
216
|
+
res.flushHeaders?.();
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
await softwareAgent.sendTask(envelope, {
|
|
220
|
+
signal: abortController.signal,
|
|
221
|
+
observer: {
|
|
222
|
+
onStart(event) {
|
|
223
|
+
ensureStreamStarted();
|
|
224
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
225
|
+
type: "start",
|
|
226
|
+
taskId: event.taskId,
|
|
227
|
+
commandLine: event.commandLine,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
onStream(event) {
|
|
231
|
+
ensureStreamStarted();
|
|
232
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
233
|
+
type: event.stream,
|
|
234
|
+
taskId: event.taskId,
|
|
235
|
+
chunk: event.chunk,
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
onEnd(event) {
|
|
239
|
+
ensureStreamStarted();
|
|
240
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
241
|
+
type: "end",
|
|
242
|
+
taskId: event.taskId,
|
|
243
|
+
exitCode: event.exitCode,
|
|
244
|
+
durationMs: event.durationMs,
|
|
245
|
+
result: event.result,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
if (!streamStarted) {
|
|
253
|
+
const busy = String(err.message || "").includes("busy");
|
|
254
|
+
writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
|
|
255
|
+
responseFinished = true;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
259
|
+
type: "error",
|
|
260
|
+
error: err.message || String(err),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
responseFinished = true;
|
|
265
|
+
if (streamStarted && !res.writableEnded)
|
|
266
|
+
res.end();
|
|
159
267
|
}
|
|
160
268
|
}
|
|
161
269
|
export async function handleSoftwareAgentStatusHttp(req, res, deps) {
|
|
162
|
-
if (!
|
|
163
|
-
|
|
164
|
-
|
|
270
|
+
if (!requireOwnerRequest(req, res, deps.options))
|
|
271
|
+
return;
|
|
272
|
+
const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
|
|
273
|
+
if (!softwareAgent)
|
|
274
|
+
return;
|
|
275
|
+
writeJsonResponse(res, 200, softwareAgent.getState(), true);
|
|
276
|
+
}
|
|
277
|
+
export async function handleSoftwareAgentTasksHttp(req, res, deps) {
|
|
278
|
+
if (!requireOwnerRequest(req, res, deps.options))
|
|
279
|
+
return;
|
|
280
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
281
|
+
const basePath = "/self/software-agent/tasks";
|
|
282
|
+
const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
|
|
283
|
+
if (url.pathname === basePath) {
|
|
284
|
+
const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
|
|
285
|
+
const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit);
|
|
286
|
+
writeJsonResponse(res, 200, { tasks }, true);
|
|
165
287
|
return;
|
|
166
288
|
}
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
289
|
+
if (url.pathname.startsWith(`${basePath}/`)) {
|
|
290
|
+
const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
|
|
291
|
+
if (!taskId || taskId.includes("/")) {
|
|
292
|
+
writeJsonResponse(res, 400, { error: "Invalid software-agent task id" });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const task = readSoftwareAgentTaskRecord(taskLedgerDir, taskId);
|
|
296
|
+
if (!task) {
|
|
297
|
+
writeJsonResponse(res, 404, { error: "Software-agent task not found" });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
writeJsonResponse(res, 200, { task }, true);
|
|
170
301
|
return;
|
|
171
302
|
}
|
|
172
|
-
res
|
|
173
|
-
|
|
303
|
+
writeJsonResponse(res, 404, { error: "Software-agent task endpoint not found" });
|
|
304
|
+
}
|
|
305
|
+
function softwareAgentTaskLedgerDir(workdir, agentName) {
|
|
306
|
+
return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
|
|
307
|
+
}
|
|
308
|
+
function softwareAgentContextSessionDir(workdir, agentName) {
|
|
309
|
+
return join(workdir, ".akemon", "agents", agentName, "software-agent", "sessions");
|
|
310
|
+
}
|
|
311
|
+
function readPositiveIntQuery(value, fallback, max) {
|
|
312
|
+
if (!value)
|
|
313
|
+
return fallback;
|
|
314
|
+
const parsed = Number(value);
|
|
315
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
316
|
+
return fallback;
|
|
317
|
+
return Math.min(parsed, max);
|
|
318
|
+
}
|
|
319
|
+
function writeSoftwareAgentStreamEvent(res, event) {
|
|
320
|
+
if (res.destroyed)
|
|
321
|
+
return;
|
|
322
|
+
res.write(`${JSON.stringify(redactSecrets(event))}\n`);
|
|
174
323
|
}
|
|
175
324
|
export async function handleSoftwareAgentResetHttp(req, res, deps) {
|
|
176
|
-
if (!
|
|
177
|
-
res.writeHead(401, { "Content-Type": "application/json" })
|
|
178
|
-
.end(JSON.stringify({ error: "Owner token required" }));
|
|
325
|
+
if (!requireOwnerRequest(req, res, deps.options))
|
|
179
326
|
return;
|
|
180
|
-
|
|
181
|
-
if (!
|
|
182
|
-
res.writeHead(503, { "Content-Type": "application/json" })
|
|
183
|
-
.end(JSON.stringify({ error: "Software agent peripheral not ready" }));
|
|
327
|
+
const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
|
|
328
|
+
if (!softwareAgent)
|
|
184
329
|
return;
|
|
185
|
-
}
|
|
186
330
|
try {
|
|
187
|
-
await
|
|
188
|
-
res
|
|
189
|
-
.end(JSON.stringify({ ok: true, state: deps.softwareAgent.getState() }, null, 2));
|
|
331
|
+
await softwareAgent.resetSession();
|
|
332
|
+
writeJsonResponse(res, 200, { ok: true, state: softwareAgent.getState() }, true);
|
|
190
333
|
}
|
|
191
334
|
catch (err) {
|
|
192
|
-
res
|
|
193
|
-
.end(JSON.stringify({ error: err.message || String(err) }));
|
|
335
|
+
writeJsonResponse(res, 500, { error: err.message || String(err) });
|
|
194
336
|
}
|
|
195
337
|
}
|
|
196
338
|
import { RelayPeripheral } from "./relay-peripheral.js";
|
|
@@ -205,9 +347,11 @@ import { LongTermModule } from "./longterm-module.js";
|
|
|
205
347
|
import { ReflectionModule } from "./reflection-module.js";
|
|
206
348
|
import { ScriptModule } from "./script-module.js";
|
|
207
349
|
import { FileEventLog, PersistentEventBus } from "./event-bus.js";
|
|
208
|
-
import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope } from "./software-agent-peripheral.js";
|
|
350
|
+
import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentTaskRecords, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
|
|
351
|
+
import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
|
|
209
352
|
import { SIG, sig } from "./types.js";
|
|
210
353
|
import { loadConversation, listConversations, buildLLMContext } from "./context.js";
|
|
354
|
+
import { redactSecrets } from "./redaction.js";
|
|
211
355
|
import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
|
|
212
356
|
import { autoRoute, runCollaborativeQuery } from "./agent-utils.js";
|
|
213
357
|
// createMcpServer, initMcpProxy, createMcpProxyServer → see mcp-server.ts
|
|
@@ -216,11 +360,11 @@ const LLM_ENGINES = LLM_ENGINES_SET;
|
|
|
216
360
|
// Engine execution — delegates to EnginePeripheral (V2 Step 3)
|
|
217
361
|
// ---------------------------------------------------------------------------
|
|
218
362
|
/** Unified engine runner — delegates to EnginePeripheral */
|
|
219
|
-
function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay, signal, origin, routing, taskId) {
|
|
363
|
+
function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay, signal, origin, routing, taskId, routeRequest) {
|
|
220
364
|
if (!_engineP) {
|
|
221
365
|
throw new Error("Engine peripheral not initialized");
|
|
222
366
|
}
|
|
223
|
-
const result = _engineP.runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId);
|
|
367
|
+
const result = _engineP.runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId, routeRequest);
|
|
224
368
|
// Sync trace back to module-level for reporting
|
|
225
369
|
result.then(() => { lastEngineTrace = _engineP.lastTrace; }).catch(() => { lastEngineTrace = _engineP.lastTrace; });
|
|
226
370
|
return result;
|
|
@@ -288,10 +432,20 @@ export async function serve(options) {
|
|
|
288
432
|
if (!isQuiet)
|
|
289
433
|
console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
|
|
290
434
|
try {
|
|
435
|
+
if (req.url === "/self/software-agent/run-stream" && req.method === "POST") {
|
|
436
|
+
await handleSoftwareAgentRunStreamHttp(req, res, {
|
|
437
|
+
options,
|
|
438
|
+
workdir,
|
|
439
|
+
agentName: options.agentName,
|
|
440
|
+
softwareAgent: codexSoftwareAgent,
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
291
444
|
if (req.url === "/self/software-agent/run" && req.method === "POST") {
|
|
292
445
|
await handleSoftwareAgentRunHttp(req, res, {
|
|
293
446
|
options,
|
|
294
447
|
workdir,
|
|
448
|
+
agentName: options.agentName,
|
|
295
449
|
softwareAgent: codexSoftwareAgent,
|
|
296
450
|
});
|
|
297
451
|
return;
|
|
@@ -303,6 +457,16 @@ export async function serve(options) {
|
|
|
303
457
|
});
|
|
304
458
|
return;
|
|
305
459
|
}
|
|
460
|
+
const requestPath = req.url?.split("?")[0] || "";
|
|
461
|
+
if (req.method === "GET"
|
|
462
|
+
&& (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
|
|
463
|
+
await handleSoftwareAgentTasksHttp(req, res, {
|
|
464
|
+
options,
|
|
465
|
+
workdir,
|
|
466
|
+
agentName: options.agentName,
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
306
470
|
if (req.url === "/self/software-agent/reset" && req.method === "POST") {
|
|
307
471
|
await handleSoftwareAgentResetHttp(req, res, {
|
|
308
472
|
options,
|
|
@@ -498,10 +662,16 @@ export async function serve(options) {
|
|
|
498
662
|
workdir,
|
|
499
663
|
model: process.env.AKEMON_CODEX_MODEL,
|
|
500
664
|
sandbox: "workspace-write",
|
|
665
|
+
taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
|
|
666
|
+
contextSessionDir: softwareAgentContextSessionDir(workdir, options.agentName),
|
|
667
|
+
envPolicy: options.softwareAgentEnvPolicy,
|
|
668
|
+
envAllowlist: options.softwareAgentEnvAllowlist,
|
|
501
669
|
});
|
|
502
|
-
await codexSoftwareAgent.start(bus);
|
|
503
670
|
// Peripheral registry — Core routes by capability
|
|
504
671
|
const peripherals = [relay, engineP, codexSoftwareAgent];
|
|
672
|
+
for (const peripheral of peripherals) {
|
|
673
|
+
await peripheral.start(bus);
|
|
674
|
+
}
|
|
505
675
|
// requestCompute: acquire the engine slot (priority-aware), execute with a
|
|
506
676
|
// hard timeout, and release. The slot release and subprocess kill are both
|
|
507
677
|
// driven by the same AbortController so a stuck engine can't hold the lock.
|
|
@@ -533,7 +703,7 @@ export async function serve(options) {
|
|
|
533
703
|
const abortController = new AbortController();
|
|
534
704
|
const timer = setTimeout(() => abortController.abort(), ENGINE_EXEC_TIMEOUT_MS);
|
|
535
705
|
try {
|
|
536
|
-
const response = await runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay, abortController.signal, req.origin, routing, req.taskId);
|
|
706
|
+
const response = await runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay, abortController.signal, req.origin, routing, req.taskId, req.engineHints);
|
|
537
707
|
emitTokenUsage(prompt.length, response.length);
|
|
538
708
|
return { success: true, response };
|
|
539
709
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { buildLLMContext, loadConversation } from "./context.js";
|
|
2
|
+
import { buildRoleContext, loadRoles, resolveRoles } from "./role-module.js";
|
|
3
|
+
const DEFAULT_CONTEXT_BUDGET = 6000;
|
|
4
|
+
const OWNER_MEMORY_EXCLUDE_TERMS = [
|
|
5
|
+
"owner",
|
|
6
|
+
"private",
|
|
7
|
+
"personal",
|
|
8
|
+
"note",
|
|
9
|
+
"diary",
|
|
10
|
+
"bio",
|
|
11
|
+
];
|
|
12
|
+
export async function buildSoftwareAgentMemorySummary(opts) {
|
|
13
|
+
const budget = opts.contextBudget ?? DEFAULT_CONTEXT_BUDGET;
|
|
14
|
+
const parts = [
|
|
15
|
+
"[Akemon memory boundary]",
|
|
16
|
+
`Role scope: ${opts.envelope.roleScope}`,
|
|
17
|
+
`Memory scope: ${opts.envelope.memoryScope}`,
|
|
18
|
+
boundaryDescription(opts.envelope.roleScope, opts.envelope.memoryScope),
|
|
19
|
+
];
|
|
20
|
+
if (opts.envelope.memoryScope === "none") {
|
|
21
|
+
parts.push("No Akemon memory/context is included for this task.");
|
|
22
|
+
return parts.join("\n");
|
|
23
|
+
}
|
|
24
|
+
const request = normalizeRequest(opts.request);
|
|
25
|
+
const roleTrigger = readRequestString(request, "roleTrigger") || triggerForRoleScope(opts.envelope.roleScope);
|
|
26
|
+
const productName = readRequestString(request, "productName");
|
|
27
|
+
const productId = readRequestString(request, "productId");
|
|
28
|
+
const rolePolicy = await resolveRoleMemoryPolicy(opts.workdir, opts.agentName, roleTrigger);
|
|
29
|
+
if (rolePolicy.exclude.length) {
|
|
30
|
+
parts.push(`Active role exclusions: ${rolePolicy.exclude.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
const roleContext = await buildRoleContext(opts.workdir, opts.agentName, roleTrigger, productName, productId);
|
|
33
|
+
if (roleContext.trim()) {
|
|
34
|
+
parts.push("");
|
|
35
|
+
parts.push("[Role/product context]");
|
|
36
|
+
parts.push(limitText(roleContext.trim(), Math.floor(budget * 0.55)));
|
|
37
|
+
}
|
|
38
|
+
const taskContext = readRequestString(request, "taskContext");
|
|
39
|
+
if (taskContext) {
|
|
40
|
+
parts.push("");
|
|
41
|
+
parts.push("[Task-provided context]");
|
|
42
|
+
parts.push(limitText(taskContext, Math.floor(budget * 0.25)));
|
|
43
|
+
}
|
|
44
|
+
const conversationId = readRequestString(request, "conversationId");
|
|
45
|
+
if (conversationId && canIncludeConversation(opts.envelope.roleScope, opts.envelope.memoryScope)) {
|
|
46
|
+
const conv = await loadConversation(opts.workdir, opts.agentName, conversationId);
|
|
47
|
+
const { text } = buildLLMContext(conv, Math.floor(budget * 0.3));
|
|
48
|
+
if (text.trim()) {
|
|
49
|
+
parts.push("");
|
|
50
|
+
parts.push("[Conversation context]");
|
|
51
|
+
parts.push(text.trim());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (conversationId) {
|
|
55
|
+
parts.push("");
|
|
56
|
+
parts.push("[Excluded conversation context]");
|
|
57
|
+
parts.push("A conversationId was supplied, but conversation memory is only included for owner-scoped software-agent tasks in v1.");
|
|
58
|
+
}
|
|
59
|
+
const ownerMemory = readRequestString(request, "memorySummary");
|
|
60
|
+
if (ownerMemory && canIncludeOwnerMemory(opts.envelope.roleScope, opts.envelope.memoryScope)) {
|
|
61
|
+
if (roleExcludesOwnerMemory(rolePolicy)) {
|
|
62
|
+
parts.push("");
|
|
63
|
+
parts.push("[Role-excluded owner memory]");
|
|
64
|
+
parts.push(`The active role (${rolePolicy.roleName || "unknown"}) excludes ${rolePolicy.exclude.join(", ")}, so owner-provided memory was not included.`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
parts.push("");
|
|
68
|
+
parts.push("[Owner-visible memory]");
|
|
69
|
+
parts.push(limitText(ownerMemory, Math.floor(budget * 0.35)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (ownerMemory) {
|
|
73
|
+
parts.push("");
|
|
74
|
+
parts.push("[Excluded owner memory]");
|
|
75
|
+
parts.push("A memorySummary was supplied, but it was not included because this envelope is not owner/owner scoped.");
|
|
76
|
+
}
|
|
77
|
+
return limitText(parts.join("\n"), budget);
|
|
78
|
+
}
|
|
79
|
+
export function canIncludeOwnerMemory(roleScope, memoryScope) {
|
|
80
|
+
return roleScope === "owner" && memoryScope === "owner";
|
|
81
|
+
}
|
|
82
|
+
function canIncludeConversation(roleScope, memoryScope) {
|
|
83
|
+
return roleScope === "owner" && (memoryScope === "owner" || memoryScope === "task");
|
|
84
|
+
}
|
|
85
|
+
function triggerForRoleScope(roleScope) {
|
|
86
|
+
switch (roleScope) {
|
|
87
|
+
case "owner": return "trigger:chat:owner";
|
|
88
|
+
case "public": return "trigger:chat:public";
|
|
89
|
+
case "order": return "trigger:order";
|
|
90
|
+
case "agent": return "trigger:agent_call";
|
|
91
|
+
case "system": return "trigger:system";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function boundaryDescription(roleScope, memoryScope) {
|
|
95
|
+
if (roleScope === "owner" && memoryScope === "owner") {
|
|
96
|
+
return "Owner-scoped task: owner-visible memory may be included after Akemon-side selection.";
|
|
97
|
+
}
|
|
98
|
+
if (memoryScope === "none") {
|
|
99
|
+
return "No-memory task: do not use Akemon private memory, conversation history, or subjective state.";
|
|
100
|
+
}
|
|
101
|
+
return "Non-owner task: exclude owner private conversations, personal notes, bio state, diary, subjective impressions, and owner-only memory.";
|
|
102
|
+
}
|
|
103
|
+
async function resolveRoleMemoryPolicy(workdir, agentName, roleTrigger) {
|
|
104
|
+
const roles = await loadRoles(workdir, agentName);
|
|
105
|
+
const { primary } = resolveRoles(roles, roleTrigger);
|
|
106
|
+
return {
|
|
107
|
+
roleName: primary?.name || null,
|
|
108
|
+
exclude: primary?.exclude || [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function roleExcludesOwnerMemory(policy) {
|
|
112
|
+
return policy.exclude.some((item) => {
|
|
113
|
+
const normalized = item.toLowerCase();
|
|
114
|
+
return OWNER_MEMORY_EXCLUDE_TERMS.some((term) => normalized.includes(term));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function normalizeRequest(value) {
|
|
118
|
+
if (value === undefined || value === null)
|
|
119
|
+
return {};
|
|
120
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
121
|
+
throw new Error("Invalid request: expected object");
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function readRequestString(request, field) {
|
|
126
|
+
const value = request[field];
|
|
127
|
+
if (value === undefined || value === null)
|
|
128
|
+
return "";
|
|
129
|
+
if (typeof value !== "string")
|
|
130
|
+
throw new Error(`Invalid ${field}: expected string`);
|
|
131
|
+
return value.trim();
|
|
132
|
+
}
|
|
133
|
+
function limitText(text, maxChars) {
|
|
134
|
+
if (text.length <= maxChars)
|
|
135
|
+
return text;
|
|
136
|
+
const head = Math.floor(maxChars * 0.45);
|
|
137
|
+
const tail = Math.max(0, maxChars - head - 40);
|
|
138
|
+
return `${text.slice(0, head)}\n[truncated ${text.length - head - tail} chars]\n${text.slice(-tail)}`;
|
|
139
|
+
}
|