akemon 0.3.5 → 0.3.6

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/dist/server.js CHANGED
@@ -163,6 +163,16 @@ async function readOwnerSoftwareAgentEnvelope(req, res, deps) {
163
163
  envelope,
164
164
  request: body,
165
165
  });
166
+ if (readOptionalBooleanBody(body?.includeWorkMemoryContext, "includeWorkMemoryContext")) {
167
+ const workContext = await buildWorkMemoryContext({
168
+ workdir: deps.workdir,
169
+ agentName: deps.agentName,
170
+ purpose: `software-agent task: ${envelope.goal}`,
171
+ budget: readOptionalPositiveIntBody(body?.workMemoryContextBudget, "workMemoryContextBudget"),
172
+ });
173
+ envelope.workMemoryDir = workContext.workMemoryDir;
174
+ envelope.workMemoryContext = workContext.text;
175
+ }
166
176
  return envelope;
167
177
  }
168
178
  catch (err) {
@@ -225,6 +235,9 @@ export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
225
235
  type: "start",
226
236
  taskId: event.taskId,
227
237
  commandLine: event.commandLine,
238
+ contextSessionId: event.contextSessionId,
239
+ contextPacketPath: event.contextPacketPath,
240
+ workMemoryDir: event.workMemoryDir,
228
241
  });
229
242
  },
230
243
  onStream(event) {
@@ -243,6 +256,9 @@ export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
243
256
  exitCode: event.exitCode,
244
257
  durationMs: event.durationMs,
245
258
  result: event.result,
259
+ contextSessionId: event.contextSessionId,
260
+ contextPacketPath: event.contextPacketPath,
261
+ workMemoryDir: event.workMemoryDir,
246
262
  });
247
263
  },
248
264
  },
@@ -282,7 +298,9 @@ export async function handleSoftwareAgentTasksHttp(req, res, deps) {
282
298
  const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
283
299
  if (url.pathname === basePath) {
284
300
  const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
285
- const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit);
301
+ const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit, {
302
+ contextSessionId: url.searchParams.get("session") || undefined,
303
+ });
286
304
  writeJsonResponse(res, 200, { tasks }, true);
287
305
  return;
288
306
  }
@@ -297,11 +315,57 @@ export async function handleSoftwareAgentTasksHttp(req, res, deps) {
297
315
  writeJsonResponse(res, 404, { error: "Software-agent task not found" });
298
316
  return;
299
317
  }
300
- writeJsonResponse(res, 200, { task }, true);
318
+ let contextSession;
319
+ if (readBooleanQuery(url.searchParams.get("includeContext")) && task.contextSession?.sessionId) {
320
+ try {
321
+ contextSession = readSoftwareAgentContextSession(softwareAgentContextSessionDir(deps.workdir, deps.agentName), task.contextSession.sessionId, { includeContextPacket: true });
322
+ }
323
+ catch {
324
+ contextSession = null;
325
+ }
326
+ }
327
+ writeJsonResponse(res, 200, { task, ...(contextSession ? { contextSession } : {}) }, true);
301
328
  return;
302
329
  }
303
330
  writeJsonResponse(res, 404, { error: "Software-agent task endpoint not found" });
304
331
  }
332
+ export async function handleSoftwareAgentContextSessionsHttp(req, res, deps) {
333
+ if (!requireOwnerRequest(req, res, deps.options))
334
+ return;
335
+ const url = new URL(req.url || "/", "http://127.0.0.1");
336
+ const basePath = "/self/software-agent/sessions";
337
+ const contextSessionDir = softwareAgentContextSessionDir(deps.workdir, deps.agentName);
338
+ if (url.pathname === basePath) {
339
+ const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
340
+ const sessions = listSoftwareAgentContextSessions(contextSessionDir, limit);
341
+ writeJsonResponse(res, 200, { sessions }, true);
342
+ return;
343
+ }
344
+ if (url.pathname.startsWith(`${basePath}/`)) {
345
+ const sessionId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
346
+ if (!sessionId || sessionId.includes("/")) {
347
+ writeJsonResponse(res, 400, { error: "Invalid software-agent context session id" });
348
+ return;
349
+ }
350
+ let session;
351
+ try {
352
+ session = readSoftwareAgentContextSession(contextSessionDir, sessionId, {
353
+ includeContextPacket: readBooleanQuery(url.searchParams.get("includeContext")),
354
+ });
355
+ }
356
+ catch (err) {
357
+ writeJsonResponse(res, 400, { error: err.message || "Invalid software-agent context session id" });
358
+ return;
359
+ }
360
+ if (!session) {
361
+ writeJsonResponse(res, 404, { error: "Software-agent context session not found" });
362
+ return;
363
+ }
364
+ writeJsonResponse(res, 200, { session }, true);
365
+ return;
366
+ }
367
+ writeJsonResponse(res, 404, { error: "Software-agent context session endpoint not found" });
368
+ }
305
369
  function softwareAgentTaskLedgerDir(workdir, agentName) {
306
370
  return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
307
371
  }
@@ -316,6 +380,24 @@ function readPositiveIntQuery(value, fallback, max) {
316
380
  return fallback;
317
381
  return Math.min(parsed, max);
318
382
  }
383
+ function readBooleanQuery(value) {
384
+ return value === "1" || value === "true" || value === "yes";
385
+ }
386
+ function readOptionalBooleanBody(value, field) {
387
+ if (value === undefined || value === null)
388
+ return false;
389
+ if (typeof value !== "boolean")
390
+ throw new Error(`Invalid ${field}: expected boolean`);
391
+ return value;
392
+ }
393
+ function readOptionalPositiveIntBody(value, field) {
394
+ if (value === undefined || value === null)
395
+ return undefined;
396
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
397
+ throw new Error(`Invalid ${field}: expected positive integer`);
398
+ }
399
+ return value;
400
+ }
319
401
  function writeSoftwareAgentStreamEvent(res, event) {
320
402
  if (res.destroyed)
321
403
  return;
@@ -347,8 +429,9 @@ import { LongTermModule } from "./longterm-module.js";
347
429
  import { ReflectionModule } from "./reflection-module.js";
348
430
  import { ScriptModule } from "./script-module.js";
349
431
  import { FileEventLog, PersistentEventBus } from "./event-bus.js";
350
- import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentTaskRecords, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
432
+ import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentContextSessions, listSoftwareAgentTaskRecords, readSoftwareAgentContextSession, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
351
433
  import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
434
+ import { buildWorkMemoryContext, workMemoryDir } from "./work-memory.js";
352
435
  import { SIG, sig } from "./types.js";
353
436
  import { loadConversation, listConversations, buildLLMContext } from "./context.js";
354
437
  import { redactSecrets } from "./redaction.js";
@@ -458,6 +541,15 @@ export async function serve(options) {
458
541
  return;
459
542
  }
460
543
  const requestPath = req.url?.split("?")[0] || "";
544
+ if (req.method === "GET"
545
+ && (requestPath === "/self/software-agent/sessions" || requestPath.startsWith("/self/software-agent/sessions/"))) {
546
+ await handleSoftwareAgentContextSessionsHttp(req, res, {
547
+ options,
548
+ workdir,
549
+ agentName: options.agentName,
550
+ });
551
+ return;
552
+ }
461
553
  if (req.method === "GET"
462
554
  && (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
463
555
  await handleSoftwareAgentTasksHttp(req, res, {
@@ -664,6 +756,7 @@ export async function serve(options) {
664
756
  sandbox: "workspace-write",
665
757
  taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
666
758
  contextSessionDir: softwareAgentContextSessionDir(workdir, options.agentName),
759
+ workMemoryDir: workMemoryDir(workdir, options.agentName),
667
760
  envPolicy: options.softwareAgentEnvPolicy,
668
761
  envAllowlist: options.softwareAgentEnvAllowlist,
669
762
  });
@@ -133,6 +133,7 @@ export class CodexSoftwareAgentPeripheral {
133
133
  workdirStatus: this.collectWorkdirStatus(currentWorkdir),
134
134
  taskLedgerDir: this.config.taskLedgerDir,
135
135
  contextSessionDir: this.config.contextSessionDir,
136
+ workMemoryDir: this.config.workMemoryDir,
136
137
  environment: buildSoftwareAgentChildEnvironment({
137
138
  policy: this.config.envPolicy,
138
139
  allowlist: this.config.envAllowlist,
@@ -162,7 +163,14 @@ export class CodexSoftwareAgentPeripheral {
162
163
  const contextSessionId = normalizeContextSessionId(envelope.contextSessionId) || taskId;
163
164
  const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
164
165
  const workdir = workdirSafety.effectiveWorkdir;
165
- let effectiveEnvelope = { ...envelope, taskId, contextSessionId, workdir, workdirSafety };
166
+ let effectiveEnvelope = {
167
+ ...envelope,
168
+ taskId,
169
+ contextSessionId,
170
+ workdir,
171
+ workdirSafety,
172
+ workMemoryDir: envelope.workMemoryDir || this.config.workMemoryDir,
173
+ };
166
174
  const contextSession = this.config.contextSessionDir
167
175
  ? writeSoftwareAgentContextPacket(this.config.contextSessionDir, effectiveEnvelope)
168
176
  : undefined;
@@ -184,6 +192,11 @@ export class CodexSoftwareAgentPeripheral {
184
192
  const spawnImpl = this.config.spawnImpl || spawn;
185
193
  const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
186
194
  const workdirStatus = this.collectWorkdirStatus(workdir);
195
+ const taskMetadata = {
196
+ contextSessionId: effectiveEnvelope.contextSessionId,
197
+ contextPacketPath: effectiveEnvelope.contextPacketPath,
198
+ workMemoryDir: effectiveEnvelope.workMemoryDir,
199
+ };
187
200
  const childEnvironment = buildSoftwareAgentChildEnvironment({
188
201
  policy: this.config.envPolicy,
189
202
  allowlist: this.config.envAllowlist,
@@ -212,7 +225,7 @@ export class CodexSoftwareAgentPeripheral {
212
225
  });
213
226
  const origin = "software_agent";
214
227
  relay.sendTaskStart(taskId, origin, commandLine);
215
- observer?.onStart?.({ taskId, origin, commandLine });
228
+ observer?.onStart?.({ taskId, origin, commandLine, ...taskMetadata });
216
229
  this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
217
230
  taskId,
218
231
  taskType: "software_agent",
@@ -241,9 +254,16 @@ export class CodexSoftwareAgentPeripheral {
241
254
  error: err.message || String(err),
242
255
  exitCode: null,
243
256
  durationMs,
257
+ ...taskMetadata,
244
258
  };
245
259
  relay.sendTaskEnd(taskId, null, durationMs);
246
- observer?.onEnd?.({ taskId, exitCode: null, durationMs, result: redactSecrets(result) });
260
+ observer?.onEnd?.({
261
+ taskId,
262
+ exitCode: null,
263
+ durationMs,
264
+ result: redactSecrets(result),
265
+ ...taskMetadata,
266
+ });
247
267
  const completedAt = new Date().toISOString();
248
268
  this.writeTaskRecord({
249
269
  ...baseTaskRecord(),
@@ -308,9 +328,16 @@ export class CodexSoftwareAgentPeripheral {
308
328
  error: success ? undefined : error || stderr.trim() || `codex exited with code ${exitCode}`,
309
329
  exitCode,
310
330
  durationMs,
331
+ ...taskMetadata,
311
332
  };
312
333
  relay.sendTaskEnd(taskId, exitCode, durationMs);
313
- observer?.onEnd?.({ taskId, exitCode, durationMs, result: redactSecrets(result) });
334
+ observer?.onEnd?.({
335
+ taskId,
336
+ exitCode,
337
+ durationMs,
338
+ result: redactSecrets(result),
339
+ ...taskMetadata,
340
+ });
314
341
  const completedAt = new Date().toISOString();
315
342
  this.writeTaskRecord({
316
343
  ...baseTaskRecord(),
@@ -451,6 +478,9 @@ export function buildTaskEnvelopePrompt(envelope) {
451
478
  `Risk level: ${envelope.riskLevel}`,
452
479
  `Workdir: ${envelope.workdir}`,
453
480
  ];
481
+ if (envelope.workMemoryDir) {
482
+ lines.push(`Work memory directory: ${envelope.workMemoryDir}`);
483
+ }
454
484
  if (envelope.contextPacketPath) {
455
485
  lines.push(`Context packet path: ${envelope.contextPacketPath}`);
456
486
  }
@@ -472,6 +502,20 @@ export function buildTaskEnvelopePrompt(envelope) {
472
502
  lines.push(envelope.memorySummary.trim());
473
503
  lines.push("");
474
504
  }
505
+ if (envelope.workMemoryDir) {
506
+ lines.push("Work memory:");
507
+ lines.push("- This is user-owned working context for engineering/task continuity.");
508
+ lines.push("- You may read it with grep, direct file browsing, or semantic review as appropriate.");
509
+ lines.push("- You may update files under this directory when the task or user asks you to maintain work memory.");
510
+ lines.push("- Do not read or edit Akemon self memory as part of this software-agent task.");
511
+ lines.push("- For a quick append, use `akemon work-note \"<durable work memory>\" --source codex --kind note`.");
512
+ lines.push("");
513
+ }
514
+ if (envelope.workMemoryContext?.trim()) {
515
+ lines.push("Included work-memory context:");
516
+ lines.push(envelope.workMemoryContext.trim());
517
+ lines.push("");
518
+ }
475
519
  if (envelope.allowedActions?.length) {
476
520
  lines.push("Allowed actions:");
477
521
  for (const item of envelope.allowedActions)
@@ -492,7 +536,9 @@ export function buildTaskEnvelopePrompt(envelope) {
492
536
  lines.push("Instructions:");
493
537
  lines.push("- Treat this envelope as the complete Akemon-provided context for this task.");
494
538
  lines.push("- Do not attempt to read Akemon private memory outside the visible context above.");
539
+ lines.push("- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.");
495
540
  lines.push("- Work only in the stated workdir unless the envelope explicitly allows otherwise.");
541
+ lines.push("- If you learn durable work memory, update the work memory directory or use `akemon work-note`.");
496
542
  lines.push("- Report what changed, what you verified, and any remaining risk.");
497
543
  return lines.join("\n");
498
544
  }
@@ -504,6 +550,7 @@ export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
504
550
  `Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
505
551
  `Context packet: ${contextPacketPath}`,
506
552
  `Workdir: ${envelope.workdir}`,
553
+ `Work memory directory: ${envelope.workMemoryDir || "(not configured)"}`,
507
554
  "",
508
555
  "Goal:",
509
556
  envelope.goal,
@@ -512,7 +559,9 @@ export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
512
559
  "- Read the context packet first before doing repository work.",
513
560
  "- Treat that file as the complete Akemon-provided context for this task.",
514
561
  "- Do not read Akemon private memory outside the context packet.",
562
+ "- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.",
515
563
  "- Work only in the stated workdir unless the packet explicitly allows otherwise.",
564
+ "- If you learn durable work memory, update the work memory directory or use `akemon work-note`.",
516
565
  "- Report what changed, what you verified, and any remaining risk.",
517
566
  ].join("\n");
518
567
  }
@@ -618,8 +667,9 @@ export function readGitWorktreeStatus(workdir) {
618
667
  };
619
668
  }
620
669
  }
621
- export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
670
+ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20, opts = {}) {
622
671
  const safeLimit = normalizeTaskRecordLimit(limit);
672
+ const contextSessionId = opts.contextSessionId?.trim();
623
673
  try {
624
674
  if (!existsSync(taskLedgerDir))
625
675
  return [];
@@ -627,6 +677,7 @@ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
627
677
  .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
628
678
  .map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
629
679
  .filter((record) => !!record)
680
+ .filter((record) => !contextSessionId || record.contextSession?.sessionId === contextSessionId || record.envelope.contextSessionId === contextSessionId)
630
681
  .sort(compareSoftwareAgentTaskRecords)
631
682
  .slice(0, safeLimit);
632
683
  }
@@ -638,6 +689,62 @@ export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
638
689
  const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
639
690
  return readSoftwareAgentTaskRecordFile(file);
640
691
  }
692
+ export function listSoftwareAgentContextSessions(contextSessionDir, limit = 20) {
693
+ const safeLimit = normalizeTaskRecordLimit(limit);
694
+ try {
695
+ if (!existsSync(contextSessionDir))
696
+ return [];
697
+ return readdirSync(contextSessionDir, { withFileTypes: true })
698
+ .filter((entry) => entry.isDirectory())
699
+ .map((entry) => {
700
+ try {
701
+ return readSoftwareAgentContextSession(contextSessionDir, entry.name);
702
+ }
703
+ catch {
704
+ return null;
705
+ }
706
+ })
707
+ .filter((record) => !!record)
708
+ .sort(compareSoftwareAgentContextSessions)
709
+ .slice(0, safeLimit);
710
+ }
711
+ catch {
712
+ return [];
713
+ }
714
+ }
715
+ export function readSoftwareAgentContextSession(contextSessionDir, sessionId, opts = {}) {
716
+ const safeSessionId = normalizeContextSessionId(sessionId, "sessionId");
717
+ if (!safeSessionId)
718
+ return null;
719
+ const sessionDir = join(contextSessionDir, safeSessionId);
720
+ if (!existsSync(sessionDir))
721
+ return null;
722
+ const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
723
+ const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
724
+ const state = readSoftwareAgentContextSessionState(statePath);
725
+ const record = {
726
+ sessionId: safeSessionId,
727
+ packetPath,
728
+ statePath,
729
+ hasContextPacket: existsSync(packetPath),
730
+ };
731
+ if (state) {
732
+ record.updatedAt = state.updatedAt;
733
+ record.workMemoryDir = state.workMemoryDir;
734
+ record.lastTaskId = state.lastTaskId;
735
+ record.lastGoal = state.lastGoal;
736
+ record.lastResult = state.lastResult;
737
+ }
738
+ if (opts.includeContextPacket && record.hasContextPacket) {
739
+ try {
740
+ record.contextPacket = readFileSync(packetPath, "utf8");
741
+ }
742
+ catch {
743
+ record.contextPacket = "";
744
+ }
745
+ }
746
+ return record;
747
+ }
641
748
  export function pruneSoftwareAgentTaskRecords(taskLedgerDir, maxRecords = DEFAULT_TASK_LEDGER_MAX_RECORDS, preserveTaskId) {
642
749
  const safeMaxRecords = normalizeTaskLedgerMaxRecords(maxRecords);
643
750
  try {
@@ -728,6 +835,7 @@ function writeSoftwareAgentContextSessionState(statePath, envelope, result, upda
728
835
  schemaVersion: 1,
729
836
  sessionId: envelope.contextSessionId,
730
837
  updatedAt,
838
+ workMemoryDir: envelope.workMemoryDir,
731
839
  lastTaskId: result.taskId,
732
840
  lastGoal: envelope.goal,
733
841
  lastResult: {
@@ -746,10 +854,8 @@ function writeSoftwareAgentContextSessionState(statePath, envelope, result, upda
746
854
  }
747
855
  function readSoftwareAgentContextSessionSummary(statePath) {
748
856
  try {
749
- if (!existsSync(statePath))
750
- return undefined;
751
- const parsed = JSON.parse(readFileSync(statePath, "utf8"));
752
- if (!parsed || parsed.schemaVersion !== 1 || !parsed.lastTaskId || !parsed.lastResult)
857
+ const parsed = readSoftwareAgentContextSessionState(statePath);
858
+ if (!parsed?.lastTaskId || !parsed.lastResult)
753
859
  return undefined;
754
860
  const result = parsed.lastResult;
755
861
  const status = result.success === true ? "completed" : "failed";
@@ -771,6 +877,31 @@ function readSoftwareAgentContextSessionSummary(statePath) {
771
877
  return undefined;
772
878
  }
773
879
  }
880
+ function readSoftwareAgentContextSessionState(statePath) {
881
+ try {
882
+ if (!existsSync(statePath))
883
+ return null;
884
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
885
+ if (!isSoftwareAgentContextSessionState(parsed))
886
+ return null;
887
+ return parsed;
888
+ }
889
+ catch {
890
+ return null;
891
+ }
892
+ }
893
+ function isSoftwareAgentContextSessionState(value) {
894
+ return value
895
+ && value.schemaVersion === 1
896
+ && typeof value.sessionId === "string"
897
+ && typeof value.updatedAt === "string"
898
+ && (value.workMemoryDir === undefined || typeof value.workMemoryDir === "string")
899
+ && typeof value.lastTaskId === "string"
900
+ && value.lastResult
901
+ && typeof value.lastResult.success === "boolean"
902
+ && (typeof value.lastResult.exitCode === "number" || value.lastResult.exitCode === null)
903
+ && typeof value.lastResult.durationMs === "number";
904
+ }
774
905
  function readOptionalString(value, field) {
775
906
  if (value === undefined || value === null)
776
907
  return undefined;
@@ -933,6 +1064,13 @@ function compareSoftwareAgentTaskRecords(a, b) {
933
1064
  return bTime - aTime;
934
1065
  return b.taskId.localeCompare(a.taskId);
935
1066
  }
1067
+ function compareSoftwareAgentContextSessions(a, b) {
1068
+ const bTime = Date.parse(b.updatedAt || "") || 0;
1069
+ const aTime = Date.parse(a.updatedAt || "") || 0;
1070
+ if (bTime !== aTime)
1071
+ return bTime - aTime;
1072
+ return b.sessionId.localeCompare(a.sessionId);
1073
+ }
936
1074
  function buildCodexExecCommand(opts) {
937
1075
  const args = [
938
1076
  "exec",
@@ -0,0 +1,69 @@
1
+ export function renderSoftwareAgentRunResult(result, writers = {
2
+ stdout: (chunk) => process.stdout.write(chunk),
3
+ stderr: (chunk) => process.stderr.write(chunk),
4
+ }) {
5
+ const output = readString(result?.output);
6
+ if (output) {
7
+ writers.stdout(output);
8
+ if (!output.endsWith("\n"))
9
+ writers.stdout("\n");
10
+ }
11
+ else {
12
+ writers.stdout(`${JSON.stringify(result, null, 2)}\n`);
13
+ }
14
+ const taskId = readString(result?.taskId) || "unknown";
15
+ const success = result?.success === false ? false : true;
16
+ const exitCode = readExitCode(result?.exitCode);
17
+ const durationMs = readDurationMs(result?.durationMs);
18
+ const parts = [`[software-agent] task ${taskId} ${success ? "finished" : "failed"}`];
19
+ if (exitCode !== undefined)
20
+ parts.push(`exit=${exitCode}`);
21
+ if (durationMs !== undefined)
22
+ parts.push(`duration=${durationMs}ms`);
23
+ stderrLine(writers, parts.join(" "));
24
+ const error = readString(result?.error);
25
+ if (!success && error)
26
+ stderrLine(writers, `[software-agent] error: ${truncateOneLine(error, 240)}`);
27
+ printMetadata(result, writers);
28
+ printNextSteps(result, taskId, writers);
29
+ return !success;
30
+ }
31
+ function printMetadata(result, writers) {
32
+ const contextSessionId = readString(result?.contextSessionId);
33
+ const contextPacketPath = readString(result?.contextPacketPath);
34
+ const workMemoryDir = readString(result?.workMemoryDir);
35
+ if (contextSessionId)
36
+ stderrLine(writers, `[software-agent] session: ${truncateOneLine(contextSessionId, 120)}`);
37
+ if (contextPacketPath)
38
+ stderrLine(writers, `[software-agent] context: ${truncateOneLine(contextPacketPath, 240)}`);
39
+ if (workMemoryDir)
40
+ stderrLine(writers, `[software-agent] work memory: ${truncateOneLine(workMemoryDir, 240)}`);
41
+ }
42
+ function printNextSteps(result, taskId, writers) {
43
+ const contextSessionId = readString(result?.contextSessionId);
44
+ const workMemoryDir = readString(result?.workMemoryDir);
45
+ const hints = [`akemon software-agent-tasks ${taskId}`];
46
+ if (contextSessionId)
47
+ hints.push(`akemon software-agent-sessions ${contextSessionId} --context`);
48
+ if (workMemoryDir)
49
+ hints.push("akemon work-note \"<durable work memory>\" --source codex");
50
+ stderrLine(writers, `[software-agent] next: ${hints.join(" | ")}`);
51
+ }
52
+ function stderrLine(writers, line) {
53
+ writers.stderr(`${line}\n`);
54
+ }
55
+ function readString(value) {
56
+ return typeof value === "string" && value.trim() ? value : undefined;
57
+ }
58
+ function readExitCode(value) {
59
+ return typeof value === "number" && Number.isInteger(value) ? value : undefined;
60
+ }
61
+ function readDurationMs(value) {
62
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
63
+ }
64
+ function truncateOneLine(value, max) {
65
+ const oneLine = value.replace(/\s+/g, " ").trim();
66
+ if (oneLine.length <= max)
67
+ return oneLine;
68
+ return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
69
+ }
@@ -31,6 +31,7 @@ export class SoftwareAgentStreamCliRenderer {
31
31
  const commandLine = readString(event.commandLine);
32
32
  if (commandLine)
33
33
  this.stderrLine(`[software-agent] command: ${truncateOneLine(commandLine, 160)}`);
34
+ this.printMetadata(event);
34
35
  return false;
35
36
  }
36
37
  if (type === "stdout" && typeof event.chunk === "string") {
@@ -72,8 +73,30 @@ export class SoftwareAgentStreamCliRenderer {
72
73
  if (output) {
73
74
  this.stderrLine(`[software-agent] summary: ${truncateOneLine(output, 240)}`);
74
75
  }
76
+ this.printNextSteps(event, taskId);
75
77
  return !success;
76
78
  }
79
+ printMetadata(event) {
80
+ const contextSessionId = readString(event.contextSessionId);
81
+ const contextPacketPath = readString(event.contextPacketPath);
82
+ const workMemoryDir = readString(event.workMemoryDir);
83
+ if (contextSessionId)
84
+ this.stderrLine(`[software-agent] session: ${truncateOneLine(contextSessionId, 120)}`);
85
+ if (contextPacketPath)
86
+ this.stderrLine(`[software-agent] context: ${truncateOneLine(contextPacketPath, 240)}`);
87
+ if (workMemoryDir)
88
+ this.stderrLine(`[software-agent] work memory: ${truncateOneLine(workMemoryDir, 240)}`);
89
+ }
90
+ printNextSteps(event, taskId) {
91
+ const contextSessionId = readString(event.contextSessionId);
92
+ const workMemoryDir = readString(event.workMemoryDir);
93
+ const hints = [`akemon software-agent-tasks ${taskId}`];
94
+ if (contextSessionId)
95
+ hints.push(`akemon software-agent-sessions ${contextSessionId} --context`);
96
+ if (workMemoryDir)
97
+ hints.push("akemon work-note \"<durable work memory>\" --source codex");
98
+ this.stderrLine(`[software-agent] next: ${hints.join(" | ")}`);
99
+ }
77
100
  stderrLine(line) {
78
101
  if (!this.stderrEndsWithNewline)
79
102
  this.writers.stderr("\n");