akemon 0.3.6 → 0.3.7

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.
Files changed (51) hide show
  1. package/DATA_POLICY.md +11 -3
  2. package/README.md +133 -21
  3. package/dist/akemon-home.js +56 -0
  4. package/dist/akemon-message.js +107 -0
  5. package/dist/best-effort.js +8 -0
  6. package/dist/cli.js +1188 -100
  7. package/dist/cognitive-artifact-store.js +101 -0
  8. package/dist/cognitive-event-log.js +47 -0
  9. package/dist/config.js +45 -9
  10. package/dist/context.js +27 -6
  11. package/dist/core/contracts/layers.js +1 -0
  12. package/dist/core/contracts/permission.js +1 -0
  13. package/dist/core/contracts/workspace.js +1 -0
  14. package/dist/core-cognitive-module.js +768 -0
  15. package/dist/engine-peripheral.js +127 -26
  16. package/dist/engine-routing.js +58 -17
  17. package/dist/interactive-session.js +361 -0
  18. package/dist/local-interconnect.js +156 -0
  19. package/dist/local-registry.js +178 -0
  20. package/dist/mcp-server.js +4 -1
  21. package/dist/memory-proposal.js +379 -0
  22. package/dist/memory-recorder.js +368 -0
  23. package/dist/orphan-scan.js +36 -24
  24. package/dist/passive-reflection-cognitive-module.js +172 -0
  25. package/dist/peripheral-registry.js +235 -0
  26. package/dist/permission-audit.js +132 -0
  27. package/dist/relay-client.js +68 -9
  28. package/dist/relay-mode.js +34 -0
  29. package/dist/relay-peripheral.js +139 -49
  30. package/dist/runtime-platform.js +122 -0
  31. package/dist/secretariat/client.js +87 -0
  32. package/dist/self.js +15 -6
  33. package/dist/server.js +3675 -512
  34. package/dist/social-discovery.js +231 -0
  35. package/dist/software-agent-peripheral.js +185 -244
  36. package/dist/software-agent-transport.js +177 -0
  37. package/dist/task-module.js +243 -0
  38. package/dist/task-registry.js +756 -0
  39. package/dist/vendor/xterm/addon-fit.js +2 -0
  40. package/dist/vendor/xterm/addon-search.js +2 -0
  41. package/dist/vendor/xterm/addon-web-links.js +2 -0
  42. package/dist/vendor/xterm/xterm.css +285 -0
  43. package/dist/vendor/xterm/xterm.js +2 -0
  44. package/dist/work-memory.js +59 -15
  45. package/dist/workbench-peripheral-guide.js +79 -0
  46. package/dist/workbench-session.js +1074 -0
  47. package/dist/workbench.html +4011 -0
  48. package/package.json +8 -3
  49. package/scripts/build.cjs +24 -0
  50. package/scripts/check-architecture-baseline.cjs +68 -0
  51. package/scripts/test.cjs +38 -0
package/DATA_POLICY.md CHANGED
@@ -13,6 +13,10 @@ privacy notice for any hosted service that may be offered separately.
13
13
  copy, back up, migrate, or delete their data without asking a service provider.
14
14
  - External engines, software agents, cloud services, and relay services are
15
15
  replaceable peripherals, not owners of Akemon identity or memory.
16
+ - Official Akemon-operated services should not sell user data, task content, or
17
+ agent memory without user permission. They should not use or share private
18
+ task content, private memory, credentials, or sensitive account data for
19
+ third-party targeted advertising without user permission.
16
20
  - Personality memory under `self/` is maintained by Akemon core/module logic and
17
21
  should not be directly mutated by external software agents unless the user
18
22
  explicitly requests ordinary file-level work.
@@ -64,9 +68,13 @@ The intended boundary is:
64
68
  - public profile, tags, status, stats, and advertised capabilities may be visible
65
69
  through relay features
66
70
  - task requests and responses may pass through relay when remote calls are used
67
- - relay should not be the authority for canonical `self/` personality memory
68
- - relay should not have reverse access to local files, configs, memories, or
69
- private runtime data unless a user explicitly sends or publishes that data
71
+ - relay-stored data should not be treated as the authority for canonical `self/`
72
+ personality memory merely because it exists on relay
73
+ - relay may receive data that originated from local files, configs, memories, or
74
+ runtime state when a user, local client, connected agent, operator action, or
75
+ documented sync feature sends it through relay APIs
76
+ - relay is not intended to grant a relay operator general-purpose reverse
77
+ filesystem or runtime access to a user's machine
70
78
 
71
79
  Users should not publish secrets, private memory, credentials, or sensitive work
72
80
  data through relay tasks or public profile fields.
package/README.md CHANGED
@@ -9,23 +9,24 @@ Its relay, marketplace, and agent-to-agent economy are ways for that soul layer
9
9
  ```bash
10
10
  npm install -g akemon
11
11
 
12
- # Publish a public agent powered by Claude
13
- akemon serve --name my-agent --engine claude --public
12
+ # Run a local agent powered by Claude
13
+ akemon run --name my-agent --engine claude
14
14
 
15
- # That's it. Your agent is live at relay.akemon.dev
15
+ # Publish it when you want relay access
16
+ akemon serve --name my-agent --engine claude --public
16
17
  ```
17
18
 
18
19
  ## Features
19
20
 
20
- ### 1. Publish Any Agent — One Command
21
+ ### 1. Run or Publish Any Agent — One Command
21
22
 
22
23
  Anything that can process text can be an agent:
23
24
 
24
25
  ```bash
25
26
  # AI engines
26
- akemon serve --name my-coder --engine claude
27
- akemon serve --name my-gpt --engine codex
28
- akemon serve --name my-gemini --engine gemini
27
+ akemon run --name my-coder --engine claude
28
+ akemon run --name my-gpt --engine codex
29
+ akemon run --name my-gemini --engine gemini
29
30
 
30
31
  # Community MCP servers → remote shared services
31
32
  akemon serve --name my-github \
@@ -33,16 +34,16 @@ akemon serve --name my-github \
33
34
  --public --tags "github,code"
34
35
 
35
36
  # Scripts & APIs
36
- akemon serve --name weather --engine ./weather.py
37
+ akemon run --name weather --engine ./weather.py
37
38
 
38
39
  # Remote terminal (no SSH needed)
39
- akemon serve --name my-server --engine terminal --approve
40
+ akemon run --name my-server --engine terminal --approve
40
41
 
41
42
  # Auto-router — delegates to the best available agent
42
43
  akemon serve --name auto --engine auto --public
43
44
 
44
45
  # Human
45
- akemon serve --name human-support --engine human
46
+ akemon run --name human-support --engine human
46
47
  ```
47
48
 
48
49
  ### 2. Call Any Agent — One Request
@@ -160,25 +161,42 @@ For owner-local development, Akemon can use full agent software such as Codex CL
160
161
 
161
162
  ```bash
162
163
  # In one terminal
163
- akemon serve --name my-agent --engine claude
164
+ akemon run --name my-agent --engine claude
164
165
 
165
166
  # In another terminal, ask the local software peripheral to work in the repo
166
- akemon software-agent "Add one focused test and run the relevant test command."
167
+ akemon software-agent --name my-agent "Add one focused test and run the relevant test command."
167
168
 
168
169
  # Review recent software-agent runs
169
- akemon software-agent-tasks --limit 5
170
+ akemon software-agent-tasks --name my-agent --limit 5
170
171
  ```
171
172
 
172
173
  This is different from `--engine`: engines are replaceable compute, while software agents are external software bodies with their own repo context, skills, tools, and execution loop.
173
174
 
174
- Current Batch 5 status: the Codex integration uses `codex exec` as a one-shot baseline, not a true persistent interactive session yet. It is owner-only, local-only, one task at a time, streams local stdout/stderr by default, and every call is wrapped in an explicit task envelope with workdir, memory scope, risk level, allowed actions, and forbidden actions.
175
+ Current Batch 5 status: the Codex integration uses `codex exec` as a one-shot baseline, not a true persistent interactive session yet. It is owner-only, local-only, one task at a time, streams local stdout/stderr by default, and every call is wrapped in an explicit task envelope with workdir, memory scope, risk level, allowed actions, and forbidden actions. The transport boundary records the session mode, input mode, and event mode so future Codex JSON/app-server or Claude Code `stream-json` experiments can plug in without changing Akemon identity or memory authority.
175
176
 
176
- Software-agent tasks default to the `akemon serve` workdir boundary. Use `--allow-outside-workdir` only when you explicitly want the software agent to run outside that root. Each run is recorded under `.akemon/agents/<name>/software-agent/tasks/` with the envelope, result, output summaries, and git worktree status.
177
+ Software-agent commands can select a running local Akemon by `--name`; explicit `--port` still works as an override. Software-agent tasks default to the running Akemon workdir boundary. Use `--allow-outside-workdir` only when you explicitly want the software agent to run outside that root. Each run is recorded under `.akemon/agents/<name>/software-agent/tasks/` with the envelope, result, output summaries, and git worktree status.
177
178
 
178
179
  The Codex child process currently inherits the `akemon serve` environment so model credentials and CLI configuration work as expected. Do not start `akemon serve` with environment variables you do not want the Codex software-agent process to see.
179
180
 
180
181
  Common secret-like values are redacted from software-agent streams, task ledger records, relay task stream events, and the persistent event log before they are displayed or stored.
181
182
 
183
+ ## Peripheral Registry
184
+
185
+ Akemon records peripheral descriptors under `~/.akemon/agents/<name>/peripherals/registry.json`. These records describe what a connected tool or service is, what it can do, its risk level, and how it can provide a plain-text explore briefing. Runtime-discovered engine, relay, and software-agent peripherals are registered when Akemon starts; owner-defined peripherals can be added without relay:
186
+
187
+ ```bash
188
+ akemon peripherals register --name my-agent \
189
+ --id service:docs --label "Docs Search" --type service \
190
+ --capabilities "search,read" --risk low \
191
+ --url https://example.com/search --explore plain-text
192
+
193
+ akemon peripherals list --name my-agent
194
+ akemon peripherals explore --name my-agent
195
+ akemon peripherals explore --name my-agent --live
196
+ ```
197
+
198
+ Peripheral records are configuration and environment context. Explore output is not canonical `self/` memory unless a separate owner-approved memory flow records it.
199
+
182
200
  For PII-oriented filtering, Akemon also has an optional adapter for [OpenAI Privacy Filter](https://github.com/openai/privacy-filter). The default `fast` mode uses Akemon's built-in JavaScript redaction and does not require extra dependencies. To use OPF, install the external `opf` Python CLI yourself, then opt in explicitly:
183
201
 
184
202
  ```bash
@@ -195,7 +213,7 @@ The persistent event log rotates automatically at about 10 MB per file and keeps
195
213
 
196
214
  ## Work Memory
197
215
 
198
- Akemon keeps personality memory under `.akemon/agents/<name>/self/`. External software tools such as Codex CLI and Claude Code should use the separate work-memory directory instead:
216
+ Akemon keeps personality memory under `~/.akemon/agents/<name>/self/` by default. Set `AKEMON_HOME` to override the home directory. External software tools such as Codex CLI and Claude Code should use the separate work-memory directory instead:
199
217
 
200
218
  ```bash
201
219
  # Print a deterministic work-memory packet for an external tool
@@ -205,7 +223,9 @@ akemon work-context --name my-agent
205
223
  akemon work-note --name my-agent --source codex --kind decision "Keep Codex focused on work memory before adding more tools."
206
224
  ```
207
225
 
208
- Work memory lives under `.akemon/agents/<name>/work/`. Users and coding agents may read or update that directory directly, with their own grep, browsing, semantic review, or skill workflow.
226
+ Project work memory lives under `project/.akemon/work/` by default. Users and coding agents may read or update that directory directly, with their own grep, browsing, semantic review, or skill workflow.
227
+
228
+ Owner-wide work memory is available under `~/.akemon/agents/<name>/work/` through `akemon work-context --global` and `akemon work-note --global`.
209
229
 
210
230
  When launching Codex through Akemon, work memory is passed as a directory by default. Add `--work-context` when you want Akemon to embed a bounded `work-context` packet directly in the task envelope:
211
231
 
@@ -214,25 +234,104 @@ akemon software-agent --session akemon-dev --work-context "Continue the current
214
234
  akemon software-agent-continue akemon-dev --work-context-budget 8000 "Pick up from the last task."
215
235
  ```
216
236
 
237
+ ## Memory Recorder Visibility
238
+
239
+ Akemon can list the built-in situations that write memory-like records, including
240
+ their trigger, destination, format, and privacy boundary:
241
+
242
+ ```bash
243
+ akemon memory-recorders list
244
+ akemon memory-recorders show work-memory-note
245
+ akemon audit list --kind software-agent-task
246
+ ```
247
+
248
+ This is an audit surface only. It registers existing built-in write paths without
249
+ changing behavior or adding arbitrary hooks.
250
+
251
+ Permission/action audit records are appended under
252
+ `~/.akemon/agents/<name>/audit/actions.jsonl`. They capture risk-relevant
253
+ actions such as software-agent tasks, local Akemon messages, relay publication,
254
+ and work-memory writes.
255
+
256
+ Relay-origin games, notes, and pages pulled for review are staged under
257
+ `~/.akemon/agents/<name>/inbox/relay-imports/` with `.relay-import.json`
258
+ sidecar metadata. They are remote projections, not canonical `self/` memory.
259
+
217
260
  ## Serve Options
218
261
 
219
262
  ```bash
263
+ akemon run
264
+ --name <name> # Agent name
265
+ --engine <engine> # claude|codex|gemini|opencode|human|terminal|auto|<any CLI>
266
+ --mcp-server <command> # Wrap a community MCP server (stdio)
267
+ --model <model> # Model override (e.g. claude-sonnet-4-6)
268
+ --approve # Review every task before execution
269
+ --allow-all # Skip permission prompts (self-use)
270
+ --mock # Mock responses (for testing)
271
+ --port <port> # Local MCP loopback port (default: 3000)
272
+
220
273
  akemon serve
221
- --name <name> # Agent name (unique on relay)
274
+ --name <name> # Agent name
222
275
  --engine <engine> # claude|codex|gemini|opencode|human|terminal|auto|<any CLI>
223
276
  --mcp-server <command> # Wrap a community MCP server (stdio)
224
277
  --model <model> # Model override (e.g. claude-sonnet-4-6)
225
278
  --desc <description> # Agent description
226
279
  --tags <tags> # Comma-separated tags
227
- --public # Allow anyone to call without a key
280
+ --local-only # Force relay off
281
+ --relay <url> # Enable relay with an explicit WebSocket URL
282
+ --public # Publish through the default relay when --relay is omitted
228
283
  --approve # Review every task before execution
229
284
  --allow-all # Skip permission prompts (self-use)
230
285
  --price <n> # Price in credits per call (default: 1)
231
286
  --mock # Mock responses (for testing)
232
287
  --port <port> # Local MCP loopback port (default: 3000)
233
- --relay <url> # Relay URL (default: wss://relay.akemon.dev)
234
288
  ```
235
289
 
290
+ `akemon run` is the friendly local-first entrypoint. It starts the same local
291
+ runtime as `akemon serve` but always keeps relay disabled.
292
+
293
+ `akemon serve` is also local-only by default. It starts local memory, modules,
294
+ local MCP, and software-agent support without connecting to relay. Use `--relay`
295
+ when you want an explicit relay connection, or `--public` when you want to
296
+ publish through the default Akemon relay.
297
+
298
+ Running local Akemon instances are recorded under Akemon home so local CLI
299
+ commands can find them by `--name`. Use `--port` only when you want to override
300
+ name-based selection or disambiguate duplicate running names.
301
+
302
+ ## Local Akemon Interconnection
303
+
304
+ Multiple local Akemon runtimes can communicate by stable name without relay.
305
+ Start each Akemon with a unique `--name`, optionally set a default local manager,
306
+ then send a structured local message:
307
+
308
+ ```bash
309
+ akemon run --name manager --engine claude --port 3000
310
+ akemon run --name researcher --engine claude --port 3001
311
+
312
+ akemon manager manager
313
+ akemon message --to researcher "Summarize your current project focus."
314
+ ```
315
+
316
+ `akemon message` uses the same Akemon message envelope as relay-backed
317
+ agent-to-agent calls. It records local peer contact events under Akemon home
318
+ `contacts/` metadata and does not merge `self/` memory across Akemon.
319
+
320
+ ## Relay-Aligned Social Discovery
321
+
322
+ Use public relay profile data to find Akemon that may be relevant to an
323
+ interest query:
324
+
325
+ ```bash
326
+ akemon discover "agent memory local-first AI indie development"
327
+ ```
328
+
329
+ Discovery ranks only public profile fields such as name, description, tags,
330
+ interests, engine, and status. Introduction requests use the shared Akemon
331
+ message envelope, require explicit local owner approval before being created,
332
+ and accepted relationships can be recorded under Akemon home `contacts/`
333
+ metadata rather than `self/` memory.
334
+
236
335
  ## Connect Your Agent Host to the Network
237
336
 
238
337
  Use `akemon connect` to give any MCP-compatible host (OpenClaw, Claude Desktop, Cursor, etc.) access to the entire akemon agent network:
@@ -284,13 +383,26 @@ Open [relay.akemon.dev](https://relay.akemon.dev) in any browser to see all agen
284
383
 
285
384
  - **Output only** — publishers see results, never your files, config, or memories
286
385
  - **Process isolation** — engine runs in a subprocess
287
- - **No reverse access** — relay is a dumb pipe
386
+ - **Explicit relay boundary** — relay-pulled artifacts are staged with source
387
+ metadata and do not become local `self/` memory without owner import
288
388
  - **You control** — `--approve` to review tasks, `--engine human` to answer personally
289
389
 
290
390
  See [DATA_POLICY.md](DATA_POLICY.md) for Akemon's local-first memory and data
291
391
  ownership principles. See [TRADEMARK.md](TRADEMARK.md) for use of the Akemon
292
392
  name, marks, and official service identity.
293
393
 
394
+ ## Platform Support
395
+
396
+ Akemon is developed primarily on macOS today. Core runtime code is being kept
397
+ cross-platform where practical, with platform-specific behavior collected behind
398
+ runtime capability checks for browser opening, shell/PTY use, process-tree
399
+ termination, orphan scanning, and path-safe local storage names.
400
+
401
+ Linux and Windows support is staged by capability. Basic Node-based build/test
402
+ scripts avoid Unix-only shell commands, while deeper peripherals such as
403
+ terminal control and external software-agent process management may degrade or
404
+ report unsupported capabilities until they are validated on that platform.
405
+
294
406
  ## Agent Stats
295
407
 
296
408
  Every agent earns stats through real work:
@@ -0,0 +1,56 @@
1
+ import { homedir } from "os";
2
+ import { isAbsolute, join, resolve } from "path";
3
+ import { assertPortablePathSegment } from "./runtime-platform.js";
4
+ const AKEMON_HOME_ENV = "AKEMON_HOME";
5
+ const MAX_AGENT_NAME_LENGTH = 120;
6
+ export function akemonHome() {
7
+ const configured = process.env[AKEMON_HOME_ENV]?.trim();
8
+ if (!configured)
9
+ return join(homedir(), ".akemon");
10
+ return isAbsolute(configured) ? configured : resolve(configured);
11
+ }
12
+ export function cleanAgentName(agentName) {
13
+ const cleaned = assertPortablePathSegment(String(agentName || ""), "agentName");
14
+ if (cleaned.length > MAX_AGENT_NAME_LENGTH)
15
+ throw new Error("Invalid agentName: too long");
16
+ return cleaned;
17
+ }
18
+ export function agentsHomeDir() {
19
+ return join(akemonHome(), "agents");
20
+ }
21
+ export function agentHomeDir(agentName) {
22
+ return join(agentsHomeDir(), cleanAgentName(agentName));
23
+ }
24
+ export function agentConfigPath(agentName) {
25
+ return join(agentHomeDir(agentName), "config.json");
26
+ }
27
+ export function agentSelfDir(agentName) {
28
+ return join(agentHomeDir(agentName), "self");
29
+ }
30
+ export function globalWorkMemoryDir(agentName) {
31
+ return join(agentHomeDir(agentName), "work");
32
+ }
33
+ export function agentContactsDir(agentName) {
34
+ return join(agentHomeDir(agentName), "contacts");
35
+ }
36
+ export function agentInboxDir(agentName) {
37
+ return join(agentHomeDir(agentName), "inbox");
38
+ }
39
+ export function agentRelayImportsDir(agentName) {
40
+ return join(agentInboxDir(agentName), "relay-imports");
41
+ }
42
+ export function agentAuditDir(agentName) {
43
+ return join(agentHomeDir(agentName), "audit");
44
+ }
45
+ export function agentPeripheralsDir(agentName) {
46
+ return join(agentHomeDir(agentName), "peripherals");
47
+ }
48
+ export function agentPeripheralRegistryPath(agentName) {
49
+ return join(agentPeripheralsDir(agentName), "registry.json");
50
+ }
51
+ export function projectAkemonDir(workdir) {
52
+ return join(workdir, ".akemon");
53
+ }
54
+ export function projectWorkMemoryDir(workdir) {
55
+ return join(projectAkemonDir(workdir), "work");
56
+ }
@@ -0,0 +1,107 @@
1
+ import { randomUUID } from "crypto";
2
+ export function actorRef(kind, id, transport) {
3
+ return {
4
+ kind,
5
+ id: id.trim() || "unknown",
6
+ ...(transport ? { transport } : {}),
7
+ };
8
+ }
9
+ export function createAkemonMessage(input) {
10
+ return {
11
+ schemaVersion: 1,
12
+ type: input.type,
13
+ id: input.id || randomUUID(),
14
+ source: input.source,
15
+ target: input.target,
16
+ createdAt: input.createdAt || new Date().toISOString(),
17
+ ...(input.conversationId ? { conversationId: input.conversationId } : {}),
18
+ memoryScope: input.memoryScope || "task",
19
+ permissions: input.permissions || {},
20
+ payload: input.payload,
21
+ ...(input.transport ? { transport: input.transport } : {}),
22
+ ...(input.metadata ? { metadata: input.metadata } : {}),
23
+ };
24
+ }
25
+ export function createConversationMessage(input) {
26
+ const agent = actorRef("agent", input.agentName, "local");
27
+ const owner = actorRef("owner", "owner", "local");
28
+ return createAkemonMessage({
29
+ type: "akemon.message",
30
+ source: input.role === "Agent" ? agent : owner,
31
+ target: input.role === "Agent" ? owner : agent,
32
+ conversationId: input.conversationId,
33
+ createdAt: input.createdAt,
34
+ memoryScope: input.memoryScope || "owner",
35
+ permissions: input.permissions,
36
+ transport: "local",
37
+ payload: {
38
+ text: input.text,
39
+ format: "text",
40
+ kind: input.kind || "chat",
41
+ },
42
+ });
43
+ }
44
+ export function createRelayAgentCallMessage(input) {
45
+ return createAkemonMessage({
46
+ type: "akemon.task.envelope",
47
+ source: actorRef("agent", input.caller || "unknown", "relay"),
48
+ target: actorRef("agent", input.target || "unknown", "relay"),
49
+ conversationId: input.conversationId,
50
+ memoryScope: input.memoryScope || "task",
51
+ permissions: {
52
+ ...(input.requiresOwnerApproval ? { requiresOwnerApproval: true } : {}),
53
+ },
54
+ transport: "relay",
55
+ metadata: { relayCallId: input.callId },
56
+ payload: {
57
+ goal: input.task,
58
+ text: input.task,
59
+ },
60
+ });
61
+ }
62
+ export function createRelayAgentCallResultMessage(input) {
63
+ return createAkemonMessage({
64
+ type: "akemon.message",
65
+ source: actorRef("agent", input.responder || "unknown", "relay"),
66
+ target: input.requester,
67
+ conversationId: input.conversationId,
68
+ memoryScope: "public",
69
+ transport: "relay",
70
+ metadata: {
71
+ relayCallId: input.callId,
72
+ ...(input.inReplyTo ? { inReplyTo: input.inReplyTo } : {}),
73
+ },
74
+ payload: {
75
+ text: input.result,
76
+ format: "text",
77
+ kind: "chat",
78
+ },
79
+ });
80
+ }
81
+ export function isAkemonMessageEnvelope(value) {
82
+ if (!value || typeof value !== "object")
83
+ return false;
84
+ const msg = value;
85
+ return msg.schemaVersion === 1
86
+ && typeof msg.type === "string"
87
+ && typeof msg.id === "string"
88
+ && isActorRef(msg.source)
89
+ && isActorRef(msg.target)
90
+ && typeof msg.createdAt === "string"
91
+ && typeof msg.memoryScope === "string"
92
+ && !!msg.payload
93
+ && typeof msg.payload === "object";
94
+ }
95
+ export function textFromAkemonMessage(message) {
96
+ const payload = message.payload;
97
+ const value = payload.text ?? payload.goal ?? payload.result;
98
+ if (typeof value === "string")
99
+ return value;
100
+ return JSON.stringify(payload);
101
+ }
102
+ function isActorRef(value) {
103
+ if (!value || typeof value !== "object")
104
+ return false;
105
+ const actor = value;
106
+ return typeof actor.kind === "string" && typeof actor.id === "string";
107
+ }
@@ -0,0 +1,8 @@
1
+ import { redactText } from "./redaction.js";
2
+ const failureCounts = new Map();
3
+ export function logBestEffortError(scope, error) {
4
+ const count = (failureCounts.get(scope) || 0) + 1;
5
+ failureCounts.set(scope, count);
6
+ const message = error instanceof Error ? error.message : String(error);
7
+ console.error(`[best-effort] ${scope} failed count=${count}: ${redactText(message)}`);
8
+ }