@animalabs/agent-framework 0.15.0 → 0.16.0

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 (96) hide show
  1. package/README.md +22 -1
  2. package/dist/src/agent.d.ts +32 -0
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +55 -0
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts +9 -0
  7. package/dist/src/api/server.d.ts.map +1 -1
  8. package/dist/src/api/server.js +156 -0
  9. package/dist/src/api/server.js.map +1 -1
  10. package/dist/src/api/types.d.ts +15 -0
  11. package/dist/src/api/types.d.ts.map +1 -1
  12. package/dist/src/framework.d.ts +418 -3
  13. package/dist/src/framework.d.ts.map +1 -1
  14. package/dist/src/framework.js +1782 -68
  15. package/dist/src/framework.js.map +1 -1
  16. package/dist/src/gate/event-gate.d.ts +16 -0
  17. package/dist/src/gate/event-gate.d.ts.map +1 -1
  18. package/dist/src/gate/event-gate.js +41 -3
  19. package/dist/src/gate/event-gate.js.map +1 -1
  20. package/dist/src/index.d.ts +4 -1
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/index.js +3 -1
  23. package/dist/src/index.js.map +1 -1
  24. package/dist/src/mcpl/channel-registry.d.ts +172 -0
  25. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  26. package/dist/src/mcpl/channel-registry.js +444 -2
  27. package/dist/src/mcpl/channel-registry.js.map +1 -1
  28. package/dist/src/mcpl/server-connection.d.ts +9 -0
  29. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  30. package/dist/src/mcpl/server-connection.js +71 -13
  31. package/dist/src/mcpl/server-connection.js.map +1 -1
  32. package/dist/src/modules/history/index.d.ts +89 -13
  33. package/dist/src/modules/history/index.d.ts.map +1 -1
  34. package/dist/src/modules/history/index.js +655 -24
  35. package/dist/src/modules/history/index.js.map +1 -1
  36. package/dist/src/operator-log.d.ts +78 -0
  37. package/dist/src/operator-log.d.ts.map +1 -0
  38. package/dist/src/operator-log.js +102 -0
  39. package/dist/src/operator-log.js.map +1 -0
  40. package/dist/src/recovery/discord-awareness-outbox.d.ts +10 -0
  41. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  42. package/dist/src/recovery/discord-awareness-outbox.js +20 -0
  43. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  44. package/dist/src/types/framework.d.ts +24 -0
  45. package/dist/src/types/framework.d.ts.map +1 -1
  46. package/dist/src/types/trace.d.ts +36 -0
  47. package/dist/src/types/trace.d.ts.map +1 -1
  48. package/dist/test/agent-runtime-settings.test.js +199 -1
  49. package/dist/test/agent-runtime-settings.test.js.map +1 -1
  50. package/dist/test/api-admin-gate.test.d.ts +2 -0
  51. package/dist/test/api-admin-gate.test.d.ts.map +1 -0
  52. package/dist/test/api-admin-gate.test.js +116 -0
  53. package/dist/test/api-admin-gate.test.js.map +1 -0
  54. package/dist/test/channel-label-history.test.d.ts +2 -0
  55. package/dist/test/channel-label-history.test.d.ts.map +1 -0
  56. package/dist/test/channel-label-history.test.js +424 -0
  57. package/dist/test/channel-label-history.test.js.map +1 -0
  58. package/dist/test/discord-awareness-outbox.test.js +16 -0
  59. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  60. package/dist/test/event-gate.test.js +44 -0
  61. package/dist/test/event-gate.test.js.map +1 -1
  62. package/dist/test/helpers/quiesce-crash-child.d.ts +2 -0
  63. package/dist/test/helpers/quiesce-crash-child.d.ts.map +1 -0
  64. package/dist/test/helpers/quiesce-crash-child.js +156 -0
  65. package/dist/test/helpers/quiesce-crash-child.js.map +1 -0
  66. package/dist/test/history-module-overview.test.d.ts +2 -0
  67. package/dist/test/history-module-overview.test.d.ts.map +1 -0
  68. package/dist/test/history-module-overview.test.js +711 -0
  69. package/dist/test/history-module-overview.test.js.map +1 -0
  70. package/dist/test/history-module.test.js +146 -0
  71. package/dist/test/history-module.test.js.map +1 -1
  72. package/dist/test/host-quiesce.test.d.ts +2 -0
  73. package/dist/test/host-quiesce.test.d.ts.map +1 -0
  74. package/dist/test/host-quiesce.test.js +863 -0
  75. package/dist/test/host-quiesce.test.js.map +1 -0
  76. package/dist/test/operator-surgery.test.d.ts +2 -0
  77. package/dist/test/operator-surgery.test.d.ts.map +1 -0
  78. package/dist/test/operator-surgery.test.js +398 -0
  79. package/dist/test/operator-surgery.test.js.map +1 -0
  80. package/dist/test/puppet-tool-call.test.js +5 -0
  81. package/dist/test/puppet-tool-call.test.js.map +1 -1
  82. package/dist/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +2 -2
  84. package/src/agent.ts +69 -0
  85. package/src/api/server.ts +159 -2
  86. package/src/api/types.ts +15 -0
  87. package/src/framework.ts +2081 -89
  88. package/src/gate/event-gate.ts +47 -4
  89. package/src/index.ts +5 -1
  90. package/src/mcpl/channel-registry.ts +495 -2
  91. package/src/mcpl/server-connection.ts +73 -13
  92. package/src/modules/history/index.ts +689 -25
  93. package/src/operator-log.ts +132 -0
  94. package/src/recovery/discord-awareness-outbox.ts +19 -0
  95. package/src/types/framework.ts +27 -0
  96. package/src/types/trace.ts +42 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Operator action log — a durable, append-only JSONL record of every
3
+ * operator-initiated mutation of a live resident: branch rollbacks, message
4
+ * suppressions, undo/redo/hide, unstick/nudge, runtime-settings changes,
5
+ * host quiesce/resume. One file per store, next to the chronicle
6
+ * (`<storePath>/operator-actions.jsonl`), so it travels with backups and
7
+ * survives host restarts the way `repair-receipts.jsonl` and
8
+ * `watchdog-wedge.jsonl` do.
9
+ *
10
+ * The chronicle record log is the authoritative history of *what* changed;
11
+ * this file records *who asked for it, from where, and why* — the part the
12
+ * store cannot know. Writes are best-effort (a logging failure never fails
13
+ * the action) and synchronous (`write(2)`, not `fsync`): an entry survives a
14
+ * process crash once the call returns, not a power loss. Mutations record
15
+ * after they complete, so a crash between the chronicle switch and the
16
+ * append loses the who/why for that one action — the branch itself remains
17
+ * the evidence. The file is created 0600: it carries requester identity and
18
+ * free-text operator notes.
19
+ */
20
+
21
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
22
+ import { dirname, join } from 'node:path';
23
+
24
+ /** Who asked. `via` names the surface ('webui', 'host-command', 'api',
25
+ * 'cli', …); `name`/`id` are whatever identity that surface has. */
26
+ export interface OperatorRequester {
27
+ via: string;
28
+ name?: string;
29
+ id?: string;
30
+ }
31
+
32
+ export interface OperatorLogEntry {
33
+ /** ISO-8601 wall-clock time of the record. */
34
+ at: string;
35
+ /** Action kind, e.g. 'rollback', 'suppress', 'undo', 'hide', 'quiesce'. */
36
+ kind: string;
37
+ /** Agent the action targeted (absent for host-wide actions). */
38
+ agent?: string;
39
+ requester?: OperatorRequester;
40
+ /** Free-text reason supplied by the operator. */
41
+ note?: string;
42
+ /** Inputs as the caller supplied them (ids, counts, patches). */
43
+ params?: Record<string, unknown>;
44
+ /** Outcome details (branches, removed ids, refs) when the action succeeded. */
45
+ result?: Record<string, unknown>;
46
+ /** Error message when the action was refused or failed. */
47
+ error?: string;
48
+ }
49
+
50
+ export type OperatorLogInput = Omit<OperatorLogEntry, 'at'> & { at?: string };
51
+
52
+ export class OperatorLog {
53
+ private warned = false;
54
+
55
+ constructor(readonly path: string | undefined) {}
56
+
57
+ get enabled(): boolean {
58
+ return this.path !== undefined;
59
+ }
60
+
61
+ /** Append one record. Never throws. Returns the stamped entry. */
62
+ append(input: OperatorLogInput): OperatorLogEntry {
63
+ const entry: OperatorLogEntry = { ...input, at: input.at ?? new Date().toISOString() };
64
+ if (!this.path) return entry;
65
+ try {
66
+ const dir = dirname(this.path);
67
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
68
+ appendFileSync(this.path, JSON.stringify(entry) + '\n', { mode: 0o600 });
69
+ } catch (error) {
70
+ if (!this.warned) {
71
+ this.warned = true;
72
+ console.error(
73
+ `[operator-log] failed to write ${this.path}: ${error instanceof Error ? error.message : String(error)}`,
74
+ );
75
+ }
76
+ }
77
+ return entry;
78
+ }
79
+
80
+ /**
81
+ * Read the newest `limit` entries (oldest first within the slice). Reads
82
+ * the whole file — the log is small (one line per operator action).
83
+ * Malformed lines are skipped rather than failing the read.
84
+ */
85
+ readTail(limit = 100): OperatorLogEntry[] {
86
+ if (!this.path || !existsSync(this.path)) return [];
87
+ let raw: string;
88
+ try {
89
+ raw = readFileSync(this.path, 'utf8');
90
+ } catch {
91
+ return [];
92
+ }
93
+ const lines = raw.split('\n').filter((l) => l.length > 0);
94
+ const slice = lines.slice(Math.max(0, lines.length - Math.max(0, limit)));
95
+ const out: OperatorLogEntry[] = [];
96
+ for (const line of slice) {
97
+ try {
98
+ const parsed = JSON.parse(line) as OperatorLogEntry;
99
+ if (parsed && typeof parsed === 'object' && typeof parsed.kind === 'string') out.push(parsed);
100
+ } catch {
101
+ // skip torn/corrupt line
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ }
107
+
108
+ export function defaultOperatorLogPath(storePath: string): string {
109
+ return join(storePath, 'operator-actions.jsonl');
110
+ }
111
+
112
+ /** Ids lists in log records are capped so one wide `hide`/`suppress` cannot
113
+ * write a multi-KB line; the full count is always recorded alongside. */
114
+ export const OPERATOR_LOG_ID_CAP = 50;
115
+ export function capIds(ids: string[]): { ids: string[]; count: number; truncated?: true } {
116
+ return ids.length > OPERATOR_LOG_ID_CAP
117
+ ? { ids: ids.slice(0, OPERATOR_LOG_ID_CAP), count: ids.length, truncated: true }
118
+ : { ids, count: ids.length };
119
+ }
120
+
121
+ /** Thrown by live surgery methods when a request cannot be honored. `code`
122
+ * lets surfaces distinguish "agent busy — quiesce first" from bad input. */
123
+ export class OperatorActionError extends Error {
124
+ constructor(
125
+ readonly code: 'unknown-agent' | 'agent-busy' | 'unknown-message' | 'invalid' | 'failed',
126
+ message: string,
127
+ options?: { cause?: unknown },
128
+ ) {
129
+ super(message, options);
130
+ this.name = 'OperatorActionError';
131
+ }
132
+ }
@@ -158,6 +158,25 @@ export class DiscordAwarenessOutbox {
158
158
  this.write(document);
159
159
  }
160
160
 
161
+ /**
162
+ * Retire a batch that was prepared but whose operation did not complete
163
+ * (fork failed, a removal threw and the source branch was restored). A
164
+ * prepared batch is armed: it re-fires at boot or on branch switch through
165
+ * `activatePreparedForBranch` / `preparedSuppressionsForBranch`, and an
166
+ * un-completable suppression there aborts framework start. Only prepared
167
+ * batches can be discarded — an active batch owns delivered markers and
168
+ * must be reconciled, not dropped. Returns false when nothing was removed.
169
+ */
170
+ discard(batchId: string): boolean {
171
+ const document = this.read();
172
+ const index = document.batches.findIndex((candidate) => candidate.id === batchId);
173
+ if (index < 0) return false;
174
+ if (document.batches[index].status !== 'prepared') return false;
175
+ document.batches.splice(index, 1);
176
+ this.write(document);
177
+ return true;
178
+ }
179
+
161
180
  /**
162
181
  * Reconcile every retained ledger entry against the active branch. A marker
163
182
  * is desired when any active batch for the same ref+emoji has a target branch
@@ -95,9 +95,36 @@ export interface FrameworkConfig {
95
95
  */
96
96
  discordAwarenessOutboxPath?: string;
97
97
 
98
+ /**
99
+ * Branch-INDEPENDENT record of the host's quiesce state (issue #122).
100
+ * Defaults to `<storePath>/recovery/host-mode.json`. Lives outside the
101
+ * chronicle because a historical rollback (branchAt + switchBranch) would
102
+ * otherwise move the active branch to a version of `framework/state`
103
+ * predating the marker, and a restart would boot SERVING mid-surgery. Set
104
+ * explicitly when providing an app-owned `store` without `storePath`;
105
+ * without either, the branch-local slot is used with a one-time warning.
106
+ */
107
+ hostModePath?: string;
108
+
109
+ /**
110
+ * Branch-independent durable queue of context writes deferred while
111
+ * quiesced. Defaults to `<storePath>/recovery/deferred-writes.json`; same
112
+ * rationale and fallback as `hostModePath`.
113
+ */
114
+ deferredWritesPath?: string;
115
+
98
116
  /** Emoji used for branch-awareness markers (default: 💤). */
99
117
  discordAwarenessEmoji?: string;
100
118
 
119
+ /**
120
+ * Durable JSONL record of operator-initiated mutations (rollback, suppress,
121
+ * undo/hide, settings, quiesce). Defaults to
122
+ * `<storePath>/operator-actions.jsonl`; set explicitly when providing an
123
+ * app-owned `store` without `storePath`. `false` disables the file (actions
124
+ * still emit `operator:action` traces).
125
+ */
126
+ operatorLogPath?: string | false;
127
+
101
128
  /**
102
129
  * Mandatory deadline for each Discord awareness marker tools/call. This is
103
130
  * independent of MCPL requestTimeoutMs and cannot be disabled with 0.
@@ -266,6 +266,20 @@ export type TraceEvent =
266
266
  toBranch: string;
267
267
  })
268
268
 
269
+ // Operator-initiated mutation (rollback/suppress/undo/hide/settings/quiesce…).
270
+ // Mirrors the durable operator-actions.jsonl record so live UIs see the
271
+ // same entry the file keeps.
272
+ | (TraceEventBase & {
273
+ type: 'operator:action';
274
+ agentName?: string;
275
+ kind: string;
276
+ requester?: { via: string; name?: string; id?: string };
277
+ note?: string;
278
+ params?: Record<string, unknown>;
279
+ result?: Record<string, unknown>;
280
+ error?: string;
281
+ })
282
+
269
283
  // MCPL subprocess stderr (one trace per line, for receipts when things break)
270
284
  | (TraceEventBase & {
271
285
  type: 'mcpl:server-stderr';
@@ -426,6 +440,34 @@ export type TraceEvent =
426
440
  message: string;
427
441
  /** Kind-specific structured payload (mirrors the failures.log record). */
428
442
  data?: Record<string, unknown>;
443
+ })
444
+
445
+ // Host quiesce/maintenance mode (issue #122)
446
+ | (TraceEventBase & {
447
+ type: 'host:quiesce';
448
+ reason?: string;
449
+ /** Whether all turns settled within the drain window. */
450
+ drained: boolean;
451
+ /** Turns still alive at return (0 unless the drain timed out). */
452
+ activeTurns: number;
453
+ /** Wakes still parked on provider admission at return (see HostModeStatus). */
454
+ parkedAdmissions?: number;
455
+ /** True when undrained turns were force-cancelled (abandon). */
456
+ abandoned?: boolean;
457
+ /** Agents whose turn abandon could not cancel (token held, no stream). */
458
+ unabandonable?: string[];
459
+ })
460
+ | (TraceEventBase & {
461
+ type: 'host:resume';
462
+ /** True when a failing feasibility verdict was overridden. */
463
+ forced?: boolean;
464
+ /** Gated inference requests released back to the scheduler. */
465
+ releasedRequests: number;
466
+ })
467
+ | (TraceEventBase & {
468
+ type: 'host:quiesced_boot';
469
+ reason?: string;
470
+ since?: number;
429
471
  });
430
472
 
431
473
  /**