@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.
- package/README.md +22 -1
- package/dist/src/agent.d.ts +32 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +55 -0
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts +9 -0
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +156 -0
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/api/types.d.ts +15 -0
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/framework.d.ts +418 -3
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1782 -68
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +16 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +41 -3
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +172 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +444 -2
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +9 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +71 -13
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +89 -13
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +655 -24
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/operator-log.d.ts +78 -0
- package/dist/src/operator-log.d.ts.map +1 -0
- package/dist/src/operator-log.js +102 -0
- package/dist/src/operator-log.js.map +1 -0
- package/dist/src/recovery/discord-awareness-outbox.d.ts +10 -0
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +20 -0
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/types/framework.d.ts +24 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +36 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.js +199 -1
- package/dist/test/agent-runtime-settings.test.js.map +1 -1
- package/dist/test/api-admin-gate.test.d.ts +2 -0
- package/dist/test/api-admin-gate.test.d.ts.map +1 -0
- package/dist/test/api-admin-gate.test.js +116 -0
- package/dist/test/api-admin-gate.test.js.map +1 -0
- package/dist/test/channel-label-history.test.d.ts +2 -0
- package/dist/test/channel-label-history.test.d.ts.map +1 -0
- package/dist/test/channel-label-history.test.js +424 -0
- package/dist/test/channel-label-history.test.js.map +1 -0
- package/dist/test/discord-awareness-outbox.test.js +16 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -1
- package/dist/test/event-gate.test.js +44 -0
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/helpers/quiesce-crash-child.d.ts +2 -0
- package/dist/test/helpers/quiesce-crash-child.d.ts.map +1 -0
- package/dist/test/helpers/quiesce-crash-child.js +156 -0
- package/dist/test/helpers/quiesce-crash-child.js.map +1 -0
- package/dist/test/history-module-overview.test.d.ts +2 -0
- package/dist/test/history-module-overview.test.d.ts.map +1 -0
- package/dist/test/history-module-overview.test.js +711 -0
- package/dist/test/history-module-overview.test.js.map +1 -0
- package/dist/test/history-module.test.js +146 -0
- package/dist/test/history-module.test.js.map +1 -1
- package/dist/test/host-quiesce.test.d.ts +2 -0
- package/dist/test/host-quiesce.test.d.ts.map +1 -0
- package/dist/test/host-quiesce.test.js +863 -0
- package/dist/test/host-quiesce.test.js.map +1 -0
- package/dist/test/operator-surgery.test.d.ts +2 -0
- package/dist/test/operator-surgery.test.d.ts.map +1 -0
- package/dist/test/operator-surgery.test.js +398 -0
- package/dist/test/operator-surgery.test.js.map +1 -0
- package/dist/test/puppet-tool-call.test.js +5 -0
- package/dist/test/puppet-tool-call.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +69 -0
- package/src/api/server.ts +159 -2
- package/src/api/types.ts +15 -0
- package/src/framework.ts +2081 -89
- package/src/gate/event-gate.ts +47 -4
- package/src/index.ts +5 -1
- package/src/mcpl/channel-registry.ts +495 -2
- package/src/mcpl/server-connection.ts +73 -13
- package/src/modules/history/index.ts +689 -25
- package/src/operator-log.ts +132 -0
- package/src/recovery/discord-awareness-outbox.ts +19 -0
- package/src/types/framework.ts +27 -0
- 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
|
package/src/types/framework.ts
CHANGED
|
@@ -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.
|
package/src/types/trace.ts
CHANGED
|
@@ -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
|
/**
|