@animalabs/agent-framework 0.15.0 → 0.17.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 +423 -3
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1857 -75
- 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 +56 -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/liveness-watchdog.test.js +18 -2
- package/dist/test/liveness-watchdog.test.js.map +1 -1
- 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/provider-acceleration-cooldown.test.js +145 -0
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
- 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 +2155 -96
- 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 +63 -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
|
@@ -70,6 +70,21 @@ export interface CodeExecutionConfig {
|
|
|
70
70
|
maxWakesPerScript?: number;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** See `FrameworkConfig.providerHold`. */
|
|
74
|
+
export interface ProviderHold {
|
|
75
|
+
holdMs: number;
|
|
76
|
+
/** Operator-facing explanation, logged with the hold. */
|
|
77
|
+
reason?: string;
|
|
78
|
+
}
|
|
79
|
+
/** What the framework knows about the failing call. */
|
|
80
|
+
export interface ProviderHoldContext {
|
|
81
|
+
/** Model of the agent whose call failed — a quota window may be model-scoped. */
|
|
82
|
+
model?: string;
|
|
83
|
+
}
|
|
84
|
+
export type ProviderHoldHook = (
|
|
85
|
+
error: Error, agentName: string, context: ProviderHoldContext,
|
|
86
|
+
) => ProviderHold | undefined;
|
|
87
|
+
|
|
73
88
|
export interface FrameworkConfig {
|
|
74
89
|
/**
|
|
75
90
|
* The subconscious resident (issue #77): a persistent side-agent that
|
|
@@ -95,9 +110,36 @@ export interface FrameworkConfig {
|
|
|
95
110
|
*/
|
|
96
111
|
discordAwarenessOutboxPath?: string;
|
|
97
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Branch-INDEPENDENT record of the host's quiesce state (issue #122).
|
|
115
|
+
* Defaults to `<storePath>/recovery/host-mode.json`. Lives outside the
|
|
116
|
+
* chronicle because a historical rollback (branchAt + switchBranch) would
|
|
117
|
+
* otherwise move the active branch to a version of `framework/state`
|
|
118
|
+
* predating the marker, and a restart would boot SERVING mid-surgery. Set
|
|
119
|
+
* explicitly when providing an app-owned `store` without `storePath`;
|
|
120
|
+
* without either, the branch-local slot is used with a one-time warning.
|
|
121
|
+
*/
|
|
122
|
+
hostModePath?: string;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Branch-independent durable queue of context writes deferred while
|
|
126
|
+
* quiesced. Defaults to `<storePath>/recovery/deferred-writes.json`; same
|
|
127
|
+
* rationale and fallback as `hostModePath`.
|
|
128
|
+
*/
|
|
129
|
+
deferredWritesPath?: string;
|
|
130
|
+
|
|
98
131
|
/** Emoji used for branch-awareness markers (default: 💤). */
|
|
99
132
|
discordAwarenessEmoji?: string;
|
|
100
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Durable JSONL record of operator-initiated mutations (rollback, suppress,
|
|
136
|
+
* undo/hide, settings, quiesce). Defaults to
|
|
137
|
+
* `<storePath>/operator-actions.jsonl`; set explicitly when providing an
|
|
138
|
+
* app-owned `store` without `storePath`. `false` disables the file (actions
|
|
139
|
+
* still emit `operator:action` traces).
|
|
140
|
+
*/
|
|
141
|
+
operatorLogPath?: string | false;
|
|
142
|
+
|
|
101
143
|
/**
|
|
102
144
|
* Mandatory deadline for each Discord awareness marker tools/call. This is
|
|
103
145
|
* independent of MCPL requestTimeoutMs and cannot be disabled with 0.
|
|
@@ -123,6 +165,27 @@ export interface FrameworkConfig {
|
|
|
123
165
|
/** Custom error policy */
|
|
124
166
|
errorPolicy?: ErrorPolicy;
|
|
125
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Host verdict on a failed inference: "this cannot succeed until later".
|
|
170
|
+
* Consulted before the error policy and before the built-in
|
|
171
|
+
* organization-acceleration classification, for a persistent agent's
|
|
172
|
+
* primary inference and for its auxiliary (compression) calls. Ephemeral
|
|
173
|
+
* runs (subagents) and conversation forks are NOT covered: they have no
|
|
174
|
+
* provider-admission gate and keep the ordinary error policy. A hold parks the agent's provider
|
|
175
|
+
* admission (primary and auxiliary) instead of retrying, and — like the
|
|
176
|
+
* built-in organization-acceleration cooldown — records no failed turn and
|
|
177
|
+
* does not feed the hard-down streak. The motivating case is a subscription
|
|
178
|
+
* credential whose quota window is spent: its 429 looks like a throttle,
|
|
179
|
+
* but retrying cannot help until the window resets.
|
|
180
|
+
*
|
|
181
|
+
* Holds are served in slices of at most 10 minutes. When a slice expires
|
|
182
|
+
* the hook is consulted again with the same error, before any inference is
|
|
183
|
+
* attempted; it extends the hold by returning another, or releases it by
|
|
184
|
+
* returning undefined. Must be synchronous and cheap; a throwing hook is
|
|
185
|
+
* treated as "no hold".
|
|
186
|
+
*/
|
|
187
|
+
providerHold?: ProviderHoldHook;
|
|
188
|
+
|
|
126
189
|
/** Interval for periodic store sync in milliseconds (default: 1000ms, 0 to disable) */
|
|
127
190
|
syncIntervalMs?: number;
|
|
128
191
|
|
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
|
/**
|