@animalabs/agent-framework 0.33.0 → 0.34.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 +136 -8
- package/dist/src/agent.d.ts +25 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +68 -13
- package/dist/src/agent.js.map +1 -1
- package/dist/src/append-all.d.ts +5 -0
- package/dist/src/append-all.d.ts.map +1 -0
- package/dist/src/append-all.js +8 -0
- package/dist/src/append-all.js.map +1 -0
- package/dist/src/context-options.d.ts +9 -6
- package/dist/src/context-options.d.ts.map +1 -1
- package/dist/src/context-options.js +13 -8
- package/dist/src/context-options.js.map +1 -1
- package/dist/src/cut-off-round.js +4 -4
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +4 -2
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/failure-text.d.ts +36 -0
- package/dist/src/failure-text.d.ts.map +1 -0
- package/dist/src/failure-text.js +89 -0
- package/dist/src/failure-text.js.map +1 -0
- package/dist/src/framework.d.ts +94 -30
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +987 -284
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +40 -6
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +66 -25
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +4 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -1
- package/dist/src/gate/reply-judge.js +12 -3
- package/dist/src/gate/reply-judge.js.map +1 -1
- package/dist/src/gate/suppression-review.js +2 -2
- package/dist/src/gate/suppression-review.js.map +1 -1
- package/dist/src/gate/types.d.ts +7 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +1 -1
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +2 -1
- package/dist/src/inference-bounds.d.ts.map +1 -1
- package/dist/src/inference-bounds.js +9 -6
- package/dist/src/inference-bounds.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +57 -13
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +136 -24
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +109 -15
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +593 -86
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +15 -9
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +37 -18
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +31 -16
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +10 -6
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +7 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +2 -1
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +25 -4
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/health/index.js +2 -2
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/semantic.js +3 -3
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +4 -3
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +2 -1
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +3 -2
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -8
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -8
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/presentation.d.ts +2 -1
- package/dist/src/presentation.d.ts.map +1 -1
- package/dist/src/presentation.js +4 -2
- package/dist/src/presentation.js.map +1 -1
- package/dist/src/reasoning-loss.d.ts +5 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +8 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +5 -4
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +2 -1
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/supplied-text.d.ts +1 -1
- package/dist/src/supplied-text.js +3 -3
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +2 -0
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +38 -9
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +13 -4
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +33 -16
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +2 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +39 -5
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -8
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -0
- package/dist/src/types/inference-log.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +64 -4
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +71 -14
- package/src/append-all.ts +6 -0
- package/src/context-options.ts +18 -8
- package/src/cut-off-round.ts +4 -4
- package/src/dendrite/registry.ts +4 -2
- package/src/failure-text.ts +88 -0
- package/src/framework.ts +1018 -293
- package/src/gate/event-gate.ts +90 -26
- package/src/gate/reply-judge.ts +12 -3
- package/src/gate/suppression-review.ts +2 -2
- package/src/gate/types.ts +7 -0
- package/src/heartbeat/heartbeat.ts +1 -1
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +11 -6
- package/src/mcpl/channel-registry.ts +160 -31
- package/src/mcpl/modern-connection.ts +629 -91
- package/src/mcpl/protocol-family.ts +15 -9
- package/src/mcpl/server-connection.ts +28 -15
- package/src/mcpl/tool-presence.ts +10 -5
- package/src/mcpl/toolset-host.ts +7 -6
- package/src/mcpl/toolsets-module.ts +2 -1
- package/src/mcpl/types.ts +25 -4
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/health/index.ts +2 -2
- package/src/modules/history/semantic.ts +3 -3
- package/src/modules/workspace/index.ts +4 -3
- package/src/modules/workspace/observe.ts +2 -1
- package/src/modules/workspace/reconcile.ts +3 -2
- package/src/notice-tags.ts +11 -8
- package/src/presentation.ts +4 -2
- package/src/reasoning-loss.ts +7 -0
- package/src/recovery/offline-branch.ts +5 -4
- package/src/recovery/recover-cli.ts +2 -1
- package/src/sent-rows.ts +1 -1
- package/src/supplied-text.ts +3 -3
- package/src/thinking-drops.ts +2 -0
- package/src/tool-presentation.ts +34 -9
- package/src/tune-out/coordinator.ts +32 -15
- package/src/tune-out/tools.ts +2 -1
- package/src/types/agent.ts +36 -5
- package/src/types/framework.ts +44 -8
- package/src/types/inference-log.ts +1 -0
- package/src/types/trace.ts +66 -4
package/src/gate/event-gate.ts
CHANGED
|
@@ -22,6 +22,9 @@ import { canonical, duplicateName, lastRecipePath, noChanges, reconcileLine, rec
|
|
|
22
22
|
import { closestNames, isNearMiss } from '../closest-names.js';
|
|
23
23
|
import { MAX_TIMER_MS } from '../deadline-timer.js';
|
|
24
24
|
import { safeSlice } from '../safe-slice.js';
|
|
25
|
+
import { failureWords } from '../failure-text.js';
|
|
26
|
+
import { JudgeFailure } from './reply-judge.js';
|
|
27
|
+
import { isSystemNotice } from '../mcpl/tags.js';
|
|
25
28
|
|
|
26
29
|
import type { ToolDefinition } from '../types/events.js';
|
|
27
30
|
import type { TraceEventInput } from '../types/trace.js';
|
|
@@ -45,6 +48,7 @@ import type {
|
|
|
45
48
|
JudgeVerdict,
|
|
46
49
|
UpcomingWake,
|
|
47
50
|
} from './types.js';
|
|
51
|
+
import { appendAll } from '../append-all.js';
|
|
48
52
|
|
|
49
53
|
// Re-export types consumers need
|
|
50
54
|
export type {
|
|
@@ -132,7 +136,9 @@ interface PendingEvent {
|
|
|
132
136
|
* composite channel id. */
|
|
133
137
|
channelLabel?: string;
|
|
134
138
|
/** Author id of the event (discord snowflake etc.), when the metadata carries
|
|
135
|
-
* one — a debounced wake
|
|
139
|
+
* one — a debounced wake's counterparty is the author of the event
|
|
140
|
+
* wakeProvenance picks; when it picks none and gives no
|
|
141
|
+
* `batchChannels`, the author of the batch's newest notice that has one. */
|
|
136
142
|
authorId?: string;
|
|
137
143
|
/** MCPL server id the event came from — the namespace for the counterparty
|
|
138
144
|
* (`<serverId>:user:<id>`), same as the direct channel-incoming path. */
|
|
@@ -141,6 +147,7 @@ interface PendingEvent {
|
|
|
141
147
|
* newer ambient chatter when a batched wake picks its provenance — the same
|
|
142
148
|
* rule the framework applies to the turn's speech locus. */
|
|
143
149
|
addressed: boolean;
|
|
150
|
+
systemNotice?: true;
|
|
144
151
|
/** The REGISTERED (composite) channel id the event arrived on, resolved by
|
|
145
152
|
* the host at enqueue time. Differs from `channelId` for push events,
|
|
146
153
|
* whose `channelId` is the adapter's raw id (a bare Discord snowflake for a
|
|
@@ -179,7 +186,13 @@ export interface WakeProvenance {
|
|
|
179
186
|
* channel); the framework derives the composite id on the direct path. */
|
|
180
187
|
channelId?: string;
|
|
181
188
|
/** Server-namespaced author id: `<serverId>:user:<id>` (falls back to the
|
|
182
|
-
* channel id's first segment only when the event carried no server id).
|
|
189
|
+
* channel id's first segment only when the event carried no server id).
|
|
190
|
+
* A `chat:system` notice, unless it's a coalesced render's latest
|
|
191
|
+
* occurrence (GateEventInfo.folds), is never chosen for a batch's route,
|
|
192
|
+
* channel or `addressed` flag. In a batch in which no other event names a
|
|
193
|
+
* channel or an author, and which has no `batchChannels`, the chosen
|
|
194
|
+
* event is its newest notice that has an author, of which the wake names
|
|
195
|
+
* only the author and its time (`at`). */
|
|
183
196
|
counterparty?: string;
|
|
184
197
|
/** True when the chosen event was `chat:addressed`. */
|
|
185
198
|
addressed?: boolean;
|
|
@@ -196,6 +209,19 @@ export interface WakeProvenance {
|
|
|
196
209
|
* event named its server: channel ids are unique only within one
|
|
197
210
|
* server's connection (MCPL §14.2). */
|
|
198
211
|
routeServerId?: string;
|
|
212
|
+
/** The channels a batch came from, each its registered id and server, in
|
|
213
|
+
* the order they first appear, when no event of it addressed the agent:
|
|
214
|
+
* those the host resolved, one or more. A channel some events gave with
|
|
215
|
+
* its server and others without is listed once each way, since only the
|
|
216
|
+
* host can tell they're one. A `chat:system` notice counts
|
|
217
|
+
* neither as addressing the agent nor as a channel, except as a
|
|
218
|
+
* coalesced render's latest occurrence, which the gate takes as a
|
|
219
|
+
* message (GateEventInfo.folds). The wake has no route, and the turn's
|
|
220
|
+
* no-locus notice names them, one given without its server only if its id
|
|
221
|
+
* still names one registered channel when the wake is admitted, unless the
|
|
222
|
+
* turn's own wake named a channel that resolves to none, or the turn is an
|
|
223
|
+
* ephemeral run's, which writes none. */
|
|
224
|
+
batchChannels?: Array<{ channelId: string; serverId?: string }>;
|
|
199
225
|
/** Timestamp (ms) of the chosen event, so a consumer coalescing several
|
|
200
226
|
* requests can order by event recency, not by flush order. */
|
|
201
227
|
at?: number;
|
|
@@ -296,15 +322,25 @@ function isMetadataTruthy(value: unknown): boolean {
|
|
|
296
322
|
}
|
|
297
323
|
|
|
298
324
|
/**
|
|
299
|
-
* Provenance of a batched wake: the newest ADDRESSED
|
|
300
|
-
* wins (mention / reply / DM outranks newer ambient
|
|
301
|
-
* own locus rule); else the newest
|
|
302
|
-
*
|
|
325
|
+
* Provenance of a batched wake: the newest ADDRESSED event that names a
|
|
326
|
+
* channel or an author wins (mention / reply / DM outranks newer ambient
|
|
327
|
+
* chatter — the framework's own locus rule); else the newest event that
|
|
328
|
+
* names either. An addressed event's resolved route counts as its channel;
|
|
329
|
+
* an ambient one's doesn't. Channel, author and addressed all come from
|
|
330
|
+
* that one event, so they can never disagree. A `chat:system` notice is
|
|
331
|
+
* never chosen for the batch's route, channel or addressed flag, even one
|
|
332
|
+
* addressed by its place (a pin in a DM), since a notice never decides where
|
|
333
|
+
* the agent's speech goes; a coalesced render whose latest occurrence is a
|
|
334
|
+
* notice isn't taken as one (GateEventInfo.folds). A batch with no such event
|
|
335
|
+
* carries only its `batchChannels` when it has them; else its chosen event is
|
|
336
|
+
* its newest notice that has an author, of which it takes only the author
|
|
337
|
+
* and time.
|
|
303
338
|
*/
|
|
304
339
|
export function wakeProvenance(events: PendingEvent[]): WakeProvenance | undefined {
|
|
340
|
+
const said = events.filter((e) => !e.systemNotice);
|
|
305
341
|
let newest: PendingEvent | undefined;
|
|
306
342
|
let newestAddressed: PendingEvent | undefined;
|
|
307
|
-
for (const e of
|
|
343
|
+
for (const e of said) {
|
|
308
344
|
// A resolved route counts as naming a channel only for an ADDRESSED
|
|
309
345
|
// event: a push whose origin declares only `mcplChannelId` (no raw
|
|
310
346
|
// channel id, no author) is still a routable addressed message, and
|
|
@@ -317,22 +353,42 @@ export function wakeProvenance(events: PendingEvent[]): WakeProvenance | undefin
|
|
|
317
353
|
if (e.addressed && (!newestAddressed || e.timestamp >= newestAddressed.timestamp)) newestAddressed = e;
|
|
318
354
|
}
|
|
319
355
|
const pick = newestAddressed ?? newest;
|
|
320
|
-
|
|
356
|
+
const channels = said.some((e) => e.addressed) ? [] : batchRoutes(said);
|
|
357
|
+
if (!pick && channels.length === 0) {
|
|
358
|
+
const notice = events.reduce<PendingEvent | undefined>(
|
|
359
|
+
(n, e) => (e.systemNotice && e.authorId && (!n || e.timestamp >= n.timestamp) ? e : n), undefined);
|
|
360
|
+
return notice ? { counterparty: counterpartyOf(notice), at: notice.timestamp } : undefined;
|
|
361
|
+
}
|
|
321
362
|
// push-event channel ids are the adapter's raw ids (a Discord snowflake):
|
|
322
363
|
// not a composite channel id, so not reported as one
|
|
323
|
-
const channelId = pick
|
|
324
|
-
const ns = pick.serverId || (pick.channelId ? pick.channelId.split(':')[0] : '') || 'channel';
|
|
364
|
+
const channelId = pick?.channelId && pick.eventType !== 'mcpl:push-event' ? pick.channelId : undefined;
|
|
325
365
|
return {
|
|
326
366
|
...(channelId ? { channelId } : {}),
|
|
327
|
-
...(pick
|
|
328
|
-
...(pick
|
|
329
|
-
...(pick
|
|
367
|
+
...(pick?.authorId ? { counterparty: counterpartyOf(pick) } : {}),
|
|
368
|
+
...(pick?.addressed ? { addressed: true } : {}),
|
|
369
|
+
...(pick?.addressed && pick.routeChannelId
|
|
330
370
|
? { routeChannelId: pick.routeChannelId, ...(pick.serverId ? { routeServerId: pick.serverId } : {}) }
|
|
331
371
|
: {}),
|
|
332
|
-
|
|
372
|
+
...(channels.length > 0 ? { batchChannels: channels } : {}),
|
|
373
|
+
...(pick ? { at: pick.timestamp } : {}),
|
|
333
374
|
};
|
|
334
375
|
}
|
|
335
376
|
|
|
377
|
+
function counterpartyOf(e: PendingEvent): string {
|
|
378
|
+
const ns = e.serverId || (e.channelId ? e.channelId.split(':')[0] : '') || 'channel';
|
|
379
|
+
return `${ns}:user:${e.authorId}`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function batchRoutes(events: PendingEvent[]): Array<{ channelId: string; serverId?: string }> {
|
|
383
|
+
const routes: Array<{ channelId: string; serverId?: string }> = [];
|
|
384
|
+
for (const e of events) {
|
|
385
|
+
if (!e.routeChannelId) continue;
|
|
386
|
+
if (routes.some((r) => r.channelId === e.routeChannelId && r.serverId === e.serverId)) continue;
|
|
387
|
+
routes.push({ channelId: e.routeChannelId, ...(e.serverId ? { serverId: e.serverId } : {}) });
|
|
388
|
+
}
|
|
389
|
+
return routes;
|
|
390
|
+
}
|
|
391
|
+
|
|
336
392
|
/** A `[Gate: …]` line's reference to messages already in the window: how
|
|
337
393
|
* many, in which channel, and how long ago the oldest arrived, as of `now`. */
|
|
338
394
|
function referenceLine(count: number, channel: string, oldest: number, now: number): string {
|
|
@@ -1689,7 +1745,7 @@ function readConfig(
|
|
|
1689
1745
|
};
|
|
1690
1746
|
}
|
|
1691
1747
|
}
|
|
1692
|
-
for (const rule of rules) sentences
|
|
1748
|
+
for (const rule of rules) appendAll(sentences, readingSentences(rule.reading, rule.outcome));
|
|
1693
1749
|
// A list with no rules, or none the gate reads, where no sentence above said it has none.
|
|
1694
1750
|
if (notUsed !== undefined && !sentences.some((said) => said.includes(notUsed))) {
|
|
1695
1751
|
sentences.push(`${File} has no rules${rules.length > 0 ? ' but ones the gate leaves out' : ''}, so ${notUsedWhy}.`);
|
|
@@ -2126,9 +2182,15 @@ export function formatShadowWarning(w: ShadowWarning): string {
|
|
|
2126
2182
|
+ `A scope on ${later} naming the event types it's for clears this note.`;
|
|
2127
2183
|
}
|
|
2128
2184
|
|
|
2185
|
+
function judgeFailureWords(err: unknown): string {
|
|
2186
|
+
if (err instanceof JudgeFailure) return err.message;
|
|
2187
|
+
return err instanceof Error ? failureWords(err, 'the call failed') : 'the call failed';
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2129
2190
|
/** The text of a thrown value, for a log line. It never throws itself, whatever was thrown. */
|
|
2130
2191
|
function errorText(err: unknown): string {
|
|
2131
2192
|
try {
|
|
2193
|
+
if (err instanceof JudgeFailure && err.detail !== undefined) return `${err.message} (${err.detail})`;
|
|
2132
2194
|
return err instanceof Error ? err.message : String(err);
|
|
2133
2195
|
} catch {
|
|
2134
2196
|
return 'a value that could not be printed';
|
|
@@ -4023,6 +4085,7 @@ export class EventGate {
|
|
|
4023
4085
|
authorId: this.extractAuthorId(info.metadata) ?? undefined,
|
|
4024
4086
|
serverId: info.serverId || undefined,
|
|
4025
4087
|
addressed: Array.isArray(info.tags) && info.tags.includes('chat:addressed'),
|
|
4088
|
+
...(isSystemNotice(info.tags) && !info.folds ? { systemNotice: true as const } : {}),
|
|
4026
4089
|
routeChannelId: this.routeChannelFor(info),
|
|
4027
4090
|
...(typeof info.metadata?.messageId === 'string' ? { messageId: info.metadata.messageId } : {}),
|
|
4028
4091
|
...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
|
|
@@ -4226,12 +4289,13 @@ export class EventGate {
|
|
|
4226
4289
|
|
|
4227
4290
|
// Provenance of the batched wake (who/where woke the agent): the newest
|
|
4228
4291
|
// ADDRESSED event first, else the newest event naming a channel or an
|
|
4229
|
-
// author; channel + author + addressed from
|
|
4230
|
-
// event addressed the agent and its registered
|
|
4231
|
-
// `routeChannelId` names the turn's speech locus too —
|
|
4232
|
-
// back to whoever addressed the agent, not to whichever
|
|
4233
|
-
// any traffic. Ambient-only batches set no locus
|
|
4234
|
-
//
|
|
4292
|
+
// author, `chat:system` notices aside; channel + author + addressed from
|
|
4293
|
+
// that ONE event. When that event addressed the agent and its registered
|
|
4294
|
+
// channel is known, `routeChannelId` names the turn's speech locus too —
|
|
4295
|
+
// the reply goes back to whoever addressed the agent, not to whichever
|
|
4296
|
+
// channel last saw any traffic. Ambient-only batches set no locus, and
|
|
4297
|
+
// name the channels they came from (`batchChannels`). A batch whose line
|
|
4298
|
+
// the gate left to the turn's start carries it too (`line`), one object
|
|
4235
4299
|
// shared by every agent woken with these messages, so it is written once.
|
|
4236
4300
|
const chosen = wakeProvenance(events);
|
|
4237
4301
|
const provenance = line ? { ...chosen, line } : chosen;
|
|
@@ -4268,7 +4332,7 @@ export class EventGate {
|
|
|
4268
4332
|
if (this.disposed) return;
|
|
4269
4333
|
const deciding = this.judging.get(policyName);
|
|
4270
4334
|
if (deciding) {
|
|
4271
|
-
deciding.queued
|
|
4335
|
+
appendAll(deciding.queued, events);
|
|
4272
4336
|
this.capJudgeEvents(deciding.queued);
|
|
4273
4337
|
return;
|
|
4274
4338
|
}
|
|
@@ -4287,8 +4351,8 @@ export class EventGate {
|
|
|
4287
4351
|
this.judging.set(policyName, state);
|
|
4288
4352
|
const timedOut = new Promise<never>((_, reject) => {
|
|
4289
4353
|
state.timer = setTimeout(() => {
|
|
4354
|
+
reject(new JudgeFailure(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
4290
4355
|
abort.abort();
|
|
4291
|
-
reject(new Error(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
4292
4356
|
}, this.judgeTimeoutMs);
|
|
4293
4357
|
// Never keep the process alive on the judge's account.
|
|
4294
4358
|
state.timer.unref?.();
|
|
@@ -4321,7 +4385,7 @@ export class EventGate {
|
|
|
4321
4385
|
private holdForJudge(policyName: string, events: PendingEvent[], judge: JudgeConfig): void {
|
|
4322
4386
|
const held = this.judgeHeld.get(policyName);
|
|
4323
4387
|
if (held) {
|
|
4324
|
-
held.events
|
|
4388
|
+
appendAll(held.events, events);
|
|
4325
4389
|
held.judge = judge;
|
|
4326
4390
|
this.capJudgeEvents(held.events);
|
|
4327
4391
|
} else {
|
|
@@ -4447,7 +4511,7 @@ export class EventGate {
|
|
|
4447
4511
|
}
|
|
4448
4512
|
const outcome: 'reply' | 'wait' | 'error' = failed ? 'error' : verdict.reply ? 'reply' : 'wait';
|
|
4449
4513
|
const reason = failed
|
|
4450
|
-
? (err !== undefined ?
|
|
4514
|
+
? (err !== undefined ? judgeFailureWords(err) : 'the judge gave no verdict')
|
|
4451
4515
|
: typeof verdict.reason === 'string' ? verdict.reason : '';
|
|
4452
4516
|
const record = this.judgeRecords.get(policyName) ?? { verdicts: { reply: 0, wait: 0, error: 0 } };
|
|
4453
4517
|
record.verdicts[outcome]++;
|
|
@@ -4469,7 +4533,7 @@ export class EventGate {
|
|
|
4469
4533
|
const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
|
|
4470
4534
|
if (failed) {
|
|
4471
4535
|
console.error(
|
|
4472
|
-
`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${reason}), so its batch of ${state.events.length} `
|
|
4536
|
+
`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${err !== undefined ? errorText(err) : reason}), so its batch of ${state.events.length} `
|
|
4473
4537
|
+ `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
|
|
4474
4538
|
);
|
|
4475
4539
|
}
|
package/src/gate/reply-judge.ts
CHANGED
|
@@ -29,6 +29,15 @@ import { NativeFormatter } from '@animalabs/membrane';
|
|
|
29
29
|
import type { ContentBlock, DetailedUsage, Membrane, NormalizedMessage, NormalizedRequest, NormalizedResponse } from '@animalabs/membrane';
|
|
30
30
|
import type { JudgeEventSummary, JudgeFn, JudgeRequest, JudgeVerdict, ReplyJudgeOptions, UpcomingWake } from './types.js';
|
|
31
31
|
|
|
32
|
+
export class JudgeFailure extends Error {
|
|
33
|
+
readonly detail?: string;
|
|
34
|
+
constructor(message: string, detail?: string) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'JudgeFailure';
|
|
37
|
+
if (detail !== undefined) this.detail = detail;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
/** The judge's model when the host names none. */
|
|
33
42
|
export const DEFAULT_JUDGE_MODEL = 'claude-haiku-5-5';
|
|
34
43
|
/** The judge's input budget when the host names none: under Haiku 5.5's 100K price line, with margin. */
|
|
@@ -334,9 +343,9 @@ export class ReplyJudge {
|
|
|
334
343
|
/** Build the judge's request for one batch: the system prompt, the document in chunks, the question. */
|
|
335
344
|
async buildRequest(request: JudgeRequest, now = Date.now()): Promise<{ request: NormalizedRequest; agentName: string; chars: number }> {
|
|
336
345
|
const agentName = this.chooseAgent(request.agentNames);
|
|
337
|
-
if (!agentName) throw new
|
|
346
|
+
if (!agentName) throw new JudgeFailure('no agent to judge for');
|
|
338
347
|
const preview = await this.preview(agentName);
|
|
339
|
-
if (request.signal.aborted) throw new
|
|
348
|
+
if (request.signal.aborted) throw new JudgeFailure('the judge was stopped before its request');
|
|
340
349
|
|
|
341
350
|
let extra: UpcomingWake[] = [];
|
|
342
351
|
try {
|
|
@@ -499,7 +508,7 @@ export class ReplyJudge {
|
|
|
499
508
|
const verdict = parseVerdict(text);
|
|
500
509
|
if (!verdict) {
|
|
501
510
|
const stop = response.stopReason;
|
|
502
|
-
throw new
|
|
511
|
+
throw new JudgeFailure("the judge's answer held no verdict", `stop: ${stop}${text ? `, text: ${JSON.stringify(cut(text, 120))}` : ''}`);
|
|
503
512
|
}
|
|
504
513
|
return {
|
|
505
514
|
...verdict,
|
|
@@ -388,7 +388,7 @@ export class SuppressionReview {
|
|
|
388
388
|
// A rule the reviewer couldn't answer about last time is reviewed from
|
|
389
389
|
// where its unreviewed events begin.
|
|
390
390
|
const fromOf = (name: string): number => Math.min(this.retryFrom.get(name) ?? fromMs, fromMs);
|
|
391
|
-
const earliest =
|
|
391
|
+
const earliest = [...this.retryFrom.values()].reduce((min, from) => Math.min(min, from), fromMs);
|
|
392
392
|
const byRule = new Map<string, SuppressedEventRecord[]>();
|
|
393
393
|
for (const record of this.deps.records(agentName, earliest, toMs)) {
|
|
394
394
|
if (Date.parse(record.at) < fromOf(record.rule)) continue;
|
|
@@ -410,7 +410,7 @@ export class SuppressionReview {
|
|
|
410
410
|
rules.push(await this.judge(agentName, { name, description, ...(rule ?? {}) }, sample, records.length, { fromMs: fromOf(name), toMs }, base));
|
|
411
411
|
}
|
|
412
412
|
const summary: SuppressionReviewSummary = {
|
|
413
|
-
fromMs: rules.length > 0 ?
|
|
413
|
+
fromMs: rules.length > 0 ? rules.reduce((min, r) => Math.min(min, fromOf(r.rule)), Infinity) : fromMs, toMs, rules,
|
|
414
414
|
total: rules.reduce((n, r) => n + r.suppressed, 0),
|
|
415
415
|
flagged: rules.reduce((n, r) => n + r.flagged.length, 0),
|
|
416
416
|
};
|
package/src/gate/types.ts
CHANGED
|
@@ -521,6 +521,13 @@ export interface GateEventInfo {
|
|
|
521
521
|
* that lands where no view hides suppressed rows.
|
|
522
522
|
*/
|
|
523
523
|
suppressionAsked?: boolean;
|
|
524
|
+
/**
|
|
525
|
+
* A coalesced render, which folds its batch's every occurrence while its
|
|
526
|
+
* `tags` are the latest's (MCPL RFC-006): never taken as a `chat:system`
|
|
527
|
+
* notice, as the render's own wake isn't, so a message it folds keeps
|
|
528
|
+
* its channel when a notice came last.
|
|
529
|
+
*/
|
|
530
|
+
folds?: true;
|
|
524
531
|
}
|
|
525
532
|
|
|
526
533
|
/** An MCPL event whose content the framework stored at arrival, as a
|
|
@@ -603,7 +603,7 @@ export class Heartbeat {
|
|
|
603
603
|
this.cancelReminders?.();
|
|
604
604
|
this.cancelReminders = null;
|
|
605
605
|
if (!this.started || this.state.reminders.length === 0) return;
|
|
606
|
-
const due =
|
|
606
|
+
const due = this.state.reminders.reduce((min, rem) => Math.min(min, dueAt(rem)), Infinity);
|
|
607
607
|
this.cancelReminders = callAt(Math.max(due, this.notBefore), this.now, () => {
|
|
608
608
|
this.cancelReminders = null;
|
|
609
609
|
this.fireDueReminders();
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './types/index.js';
|
|
|
3
3
|
|
|
4
4
|
// Core classes
|
|
5
5
|
export { AgentFramework, AgentStoppedError, BudgetPreflightError, DefaultErrorPolicy, ProviderWaitNotSleptError, QuiesceBlockedError, ResumeBlockedError } from './framework.js';
|
|
6
|
+
export { isReasoningLoss } from './reasoning-loss.js';
|
|
6
7
|
export type { RuntimeSettingsPreview, HostModeStatus, DeriveAgentOptions } from './framework.js';
|
|
7
8
|
export { Agent } from './agent.js';
|
|
8
9
|
export { FLAT_MINIMUM_MS, type InferenceBoundsOptions } from './inference-bounds.js';
|
package/src/inference-bounds.ts
CHANGED
|
@@ -72,11 +72,9 @@ export function wholeCallMs(bounds: InferenceBounds, maxTokens: number | undefin
|
|
|
72
72
|
return setupMs(bounds.progressBounds) + allowanceMs(bounds.progressBounds, maxTokens);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// membrane 0.8.0
|
|
76
|
-
// symbol (on a sent request, or the Anthropic SDK's timeout, which
|
|
77
|
-
// connect timeout too) and
|
|
78
|
-
// the deadline class here and the transient-failure check of an exhausted
|
|
79
|
-
// turn's re-arm both use it.
|
|
75
|
+
// membrane 0.8.0 and 0.9.0 mark a deadline they report as run out with this
|
|
76
|
+
// registered symbol (on a sent request, or the Anthropic SDK's timeout, which
|
|
77
|
+
// covers its connect timeout too) and export no reader or marker for it.
|
|
80
78
|
const DEADLINE_EXPIRY = Symbol.for('membrane.deadlineExpiry');
|
|
81
79
|
|
|
82
80
|
export function isDeadlineExpiry(error: unknown): boolean {
|
|
@@ -85,7 +83,7 @@ export function isDeadlineExpiry(error: unknown): boolean {
|
|
|
85
83
|
|
|
86
84
|
/**
|
|
87
85
|
* Names the host setting for the bound a stream cut hit, read from the opening
|
|
88
|
-
* of membrane 0.8.0's message for it (
|
|
86
|
+
* of membrane 0.8.0's and 0.9.0's message for it (their progress-bounds.ts and
|
|
89
87
|
* providers/utils.ts), so the reason an operator and the agent see names the
|
|
90
88
|
* host's setting for that bound, as the README's table maps them. A message it
|
|
91
89
|
* doesn't know is left as it is.
|
|
@@ -99,6 +97,8 @@ export function withHostSetting(message: string): string {
|
|
|
99
97
|
return setting === undefined ? message : `${message} (the host's setting for this bound: \`inferenceBounds.${setting}\`)`;
|
|
100
98
|
}
|
|
101
99
|
|
|
100
|
+
const CUTS = new WeakSet<object>();
|
|
101
|
+
|
|
102
102
|
export function deadlineCut(message: string | undefined): MembraneError {
|
|
103
103
|
const error = new MembraneError({
|
|
104
104
|
type: 'timeout',
|
|
@@ -107,5 +107,10 @@ export function deadlineCut(message: string | undefined): MembraneError {
|
|
|
107
107
|
rawError: undefined,
|
|
108
108
|
});
|
|
109
109
|
Object.defineProperty(error, DEADLINE_EXPIRY, { value: true });
|
|
110
|
+
CUTS.add(error);
|
|
110
111
|
return error;
|
|
111
112
|
}
|
|
113
|
+
|
|
114
|
+
export function isDeadlineCut(error: unknown): boolean {
|
|
115
|
+
return typeof error === 'object' && error !== null && CUTS.has(error);
|
|
116
|
+
}
|