@animalabs/agent-framework 0.30.0 → 0.31.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 +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +129 -92
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1115 -704
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.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 +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +2 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +54 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1145 -723
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +2 -1
- package/src/types/framework.ts +55 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
|
@@ -98,6 +98,69 @@ function writeIdOf(m: Pick<StoredMessage, 'metadata'>): string | undefined {
|
|
|
98
98
|
return typeof writeId === 'string' ? writeId : undefined;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
type HeldRow = Pick<StoredMessage, 'participant' | 'content' | 'metadata'> & { id?: string; writeId?: string };
|
|
102
|
+
|
|
103
|
+
interface RowKeys {
|
|
104
|
+
ids: string[];
|
|
105
|
+
writeIds: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface ReleaseRecord extends RowKeys {
|
|
109
|
+
total: number;
|
|
110
|
+
truncated: number;
|
|
111
|
+
report: RowKeys;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function keysOf(rows: readonly HeldRow[]): RowKeys {
|
|
115
|
+
const keys: RowKeys = { ids: [], writeIds: [] };
|
|
116
|
+
for (const m of rows) {
|
|
117
|
+
const writeId = m.writeId ?? writeIdOf(m);
|
|
118
|
+
if (writeId !== undefined) keys.writeIds.push(writeId);
|
|
119
|
+
else if (m.id !== undefined) keys.ids.push(m.id);
|
|
120
|
+
}
|
|
121
|
+
return keys;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function keyedBy(keys: readonly RowKeys[]): (m: HeldRow) => boolean {
|
|
125
|
+
const ids = new Set(keys.flatMap((k) => k.ids));
|
|
126
|
+
const writeIds = new Set(keys.flatMap((k) => k.writeIds));
|
|
127
|
+
return (m) => {
|
|
128
|
+
const writeId = m.writeId ?? writeIdOf(m);
|
|
129
|
+
return writeId !== undefined ? writeIds.has(writeId) : m.id !== undefined && ids.has(m.id);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const isStrings = (value: unknown): value is string[] => Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
134
|
+
const isRowKeys = (value: unknown): value is RowKeys =>
|
|
135
|
+
typeof value === 'object' && value !== null && isStrings((value as RowKeys).ids) && isStrings((value as RowKeys).writeIds);
|
|
136
|
+
|
|
137
|
+
function isReleaseRecord(value: unknown): value is ReleaseRecord {
|
|
138
|
+
if (!isRowKeys(value)) return false;
|
|
139
|
+
const record = value as Partial<ReleaseRecord>;
|
|
140
|
+
return typeof record.total === 'number' && typeof record.truncated === 'number' && isRowKeys(record.report);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function releaseRecordsOf(rows: readonly HeldRow[], epochId: string): ReleaseRecord[] {
|
|
144
|
+
const records: ReleaseRecord[] = [];
|
|
145
|
+
for (const m of rows) {
|
|
146
|
+
const meta = m.metadata as { kind?: unknown; epochId?: unknown; released?: unknown } | undefined;
|
|
147
|
+
if (meta?.kind !== 'tune-out-cancel' || meta.epochId !== epochId) continue;
|
|
148
|
+
if (isReleaseRecord(meta.released) && !records.some((r) => JSON.stringify(r) === JSON.stringify(meta.released))) {
|
|
149
|
+
records.push(meta.released);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return records;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function truncationNote(records: readonly ReleaseRecord[]): string {
|
|
156
|
+
const truncated = records.reduce((n, r) => n + r.truncated, 0);
|
|
157
|
+
if (truncated === 0) return '';
|
|
158
|
+
const total = records.reduce((n, r) => n + r.total, 0);
|
|
159
|
+
return records.length === 1
|
|
160
|
+
? ` ${truncated} of ${total} diverted messages, the oldest, weren't delivered raw to the resident.`
|
|
161
|
+
: ` ${truncated} of ${total} diverted messages across ${records.length} dumps, the oldest in each dump that left any out, weren't delivered raw to the resident.`;
|
|
162
|
+
}
|
|
163
|
+
|
|
101
164
|
/**
|
|
102
165
|
* Whether a held message is one of this channel's: by its channel id, and by
|
|
103
166
|
* its server where it records one, since two servers can register the same
|
|
@@ -145,7 +208,7 @@ export interface TuneOutCheck {
|
|
|
145
208
|
peek?: string[];
|
|
146
209
|
}
|
|
147
210
|
|
|
148
|
-
/** How long after an end time
|
|
211
|
+
/** How long after an end time's or a wake budget's cancel threw the cancel is tried again. */
|
|
149
212
|
const EXPIRY_RETRY_MS = 60_000;
|
|
150
213
|
|
|
151
214
|
/** Ends a backlog line cut to fit in one of the residents' messages. */
|
|
@@ -215,12 +278,7 @@ export interface TuneOutFrameworkHooks {
|
|
|
215
278
|
* message held mid-turn is in the backlog it belongs to (#232 gap 7).
|
|
216
279
|
*/
|
|
217
280
|
getDeferredMessages?: () => Array<Pick<StoredMessage, 'participant' | 'content' | 'metadata'> & { writeId?: string; heldAt?: number }>;
|
|
218
|
-
|
|
219
|
-
* Make what was just delivered durable, including anything the delivery
|
|
220
|
-
* left in the deferred queue: a release flips durable epoch state, so its
|
|
221
|
-
* dump must survive a crash in the same window (#232 gap 6).
|
|
222
|
-
*/
|
|
223
|
-
commitDeliveries?: () => void;
|
|
281
|
+
commitDeliveries?: () => boolean;
|
|
224
282
|
/**
|
|
225
283
|
* The history-reading tool the resident actually has (e.g. a server's
|
|
226
284
|
* `fetch_history`), or null. A truncation note names it only when it exists
|
|
@@ -321,8 +379,6 @@ export class TuneOutCoordinator {
|
|
|
321
379
|
private lateForks = new WeakSet<object>();
|
|
322
380
|
/** The all-except scope's durable record, cached (undefined = not read yet). */
|
|
323
381
|
private scopeCache: AllExceptState | null | undefined;
|
|
324
|
-
/** The epoch whose release this process delivered and whose hold is not yet recorded as cleared. */
|
|
325
|
-
private scopeReleased: string | undefined;
|
|
326
382
|
/** Timer key for the scope's cadence and expiry. */
|
|
327
383
|
private static readonly SCOPE_KEY = '\0all-except';
|
|
328
384
|
|
|
@@ -378,25 +434,53 @@ export class TuneOutCoordinator {
|
|
|
378
434
|
// An epoch whose deadline passed while the process was down cancels
|
|
379
435
|
// immediately — the resident gets their backlog on the schedule they
|
|
380
436
|
// committed to, not whenever the host happened to restart.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
437
|
+
const inForce = (): boolean => this.channelRegistry.getTuneOutState(serverId, channelId)?.params.epochId === params.epochId;
|
|
438
|
+
const expired = params.expiresAtMs !== undefined && params.expiresAtMs <= Date.now();
|
|
439
|
+
if (expired) {
|
|
440
|
+
try {
|
|
441
|
+
this.cancel(serverId, channelId, 'duration', 'duration elapsed');
|
|
442
|
+
continue;
|
|
443
|
+
} catch (err) {
|
|
444
|
+
if (!inForce()) {
|
|
445
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} reached its end time while the process was down and its cancel ended it, but a step after the end failed:`, err);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} reached its end time while the process was down, and its cancel failed, so it stays tuned out and its cancel is tried again in ${EXPIRY_RETRY_MS / 1000} s:`, err);
|
|
449
|
+
}
|
|
384
450
|
}
|
|
385
451
|
this.startCadence(serverId, channelId, params.cadenceSeconds);
|
|
386
|
-
this.
|
|
452
|
+
const wakeCount = this.channelRegistry.getTuneOutState(serverId, channelId)?.wakeCount ?? 0;
|
|
453
|
+
if (wakeCount > params.maxWakes) {
|
|
454
|
+
this.cancelPastBudget(serverId, channelId, params.maxWakes, 'start');
|
|
455
|
+
if (!inForce()) continue;
|
|
456
|
+
} else {
|
|
457
|
+
this.armExpiry(serverId, channelId, expired ? { ...params, expiresAtMs: Date.now() + EXPIRY_RETRY_MS } : params);
|
|
458
|
+
}
|
|
387
459
|
if (oldestAnchor === null || params.startedAtSequence < oldestAnchor) {
|
|
388
460
|
oldestAnchor = params.startedAtSequence;
|
|
389
461
|
}
|
|
390
462
|
}
|
|
391
463
|
const scope = this.getAllExcept();
|
|
392
464
|
if (scope) {
|
|
465
|
+
let inForce = true;
|
|
393
466
|
if (scope.params.expiresAtMs <= Date.now()) {
|
|
394
|
-
|
|
467
|
+
try {
|
|
468
|
+
this.cancelAllExcept('duration', 'duration elapsed');
|
|
469
|
+
inForce = false;
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (this.getAllExcept()?.params.epochId !== scope.params.epochId) {
|
|
472
|
+
console.error('[tune-out] the all-except hold reached its end time while the process was down and its release ended it, but a step after the end failed:', err);
|
|
473
|
+
inForce = false;
|
|
474
|
+
} else {
|
|
475
|
+
console.error(`[tune-out] the all-except hold reached its end time while the process was down, and its release failed, so it stays in force and its release is tried again in ${EXPIRY_RETRY_MS / 1000} s:`, err);
|
|
476
|
+
this.armScopeTimers({ ...scope.params, expiresAtMs: Date.now() + EXPIRY_RETRY_MS });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
395
479
|
} else {
|
|
396
480
|
this.armScopeTimers(scope.params);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
481
|
+
}
|
|
482
|
+
if (inForce && (oldestAnchor === null || scope.params.startedAtSequence < oldestAnchor)) {
|
|
483
|
+
oldestAnchor = scope.params.startedAtSequence;
|
|
400
484
|
}
|
|
401
485
|
}
|
|
402
486
|
if (oldestAnchor !== null) {
|
|
@@ -600,13 +684,6 @@ export class TuneOutCoordinator {
|
|
|
600
684
|
return this.readerAvailable();
|
|
601
685
|
}
|
|
602
686
|
|
|
603
|
-
/**
|
|
604
|
-
* End a tune-out: durable flip, system-framed backlog dump into the
|
|
605
|
-
* resident's window (capped), lifecycle notice, one wake for the
|
|
606
|
-
* resident. `subconsciousNote` is the subconscious's own text (from its
|
|
607
|
-
* cancel_tuneout call) — delivered verbatim under its name, never
|
|
608
|
-
* synthesized here.
|
|
609
|
-
*/
|
|
610
687
|
cancel(
|
|
611
688
|
serverId: string,
|
|
612
689
|
channelId: string,
|
|
@@ -618,31 +695,25 @@ export class TuneOutCoordinator {
|
|
|
618
695
|
/** The lifecycle state the channel returns to (a tool close ends in 'closed'). */
|
|
619
696
|
nextState: 'open' | 'closed' = 'open',
|
|
620
697
|
): { ok: true } | { ok: false; error: string } {
|
|
621
|
-
const
|
|
622
|
-
if (!
|
|
698
|
+
const ending = this.channelRegistry.getTuneOutState(serverId, channelId);
|
|
699
|
+
if (!ending) {
|
|
623
700
|
// Its reader is the resident's tune_out: cancel_tuneout finds the channel first.
|
|
624
701
|
return { ok: false, error: this.notTunedOut(this.quotedShown(serverId, channelId), 'you') };
|
|
625
702
|
}
|
|
626
|
-
this.clearTimers(serverId, channelId);
|
|
627
|
-
this.divertedSinceInvocation.delete(this.key(serverId, channelId));
|
|
628
703
|
|
|
629
|
-
|
|
630
|
-
// store order, then those still in the deferred queue (held mid-turn,
|
|
631
|
-
// not yet written), oldest first. That's arrival order, except when a
|
|
632
|
-
// held message stays queued while a later one is stored: a store write
|
|
633
|
-
// refused at a turn-start flush leaves it queued, and that turn's
|
|
634
|
-
// assembly can then store later coalesced messages directly.
|
|
635
|
-
const epochId = ended.params.epochId;
|
|
704
|
+
const epochId = ending.params.epochId;
|
|
636
705
|
const ofEpoch = (m: Pick<StoredMessage, 'metadata'>): boolean =>
|
|
637
706
|
(m.metadata as { tuneOut?: { epochId?: string } } | undefined)?.tuneOut?.epochId === epochId;
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
707
|
+
const storedRows: HeldRow[] = this.hooks.getStoredMessages();
|
|
708
|
+
const queuedRows: HeldRow[] = this.hooks.getDeferredMessages?.() ?? [];
|
|
709
|
+
const earlier = releaseRecordsOf([...storedRows, ...queuedRows], epochId);
|
|
710
|
+
const dumpedEarlier = keyedBy(earlier);
|
|
711
|
+
const unreleased = (m: HeldRow): boolean => ofEpoch(m) && !dumpedEarlier(m);
|
|
712
|
+
const held = queuedRows.filter(unreleased);
|
|
713
|
+
const backlog: HeldRow[] = [...storedRows.filter(unreleased), ...held];
|
|
643
714
|
// Whole messages: a cap below one shows none, where slice(-0.5) would
|
|
644
715
|
// start at 0 and take them all.
|
|
645
|
-
const cap = Math.floor(
|
|
716
|
+
const cap = Math.floor(ending.params.backlogCap);
|
|
646
717
|
const authorOf = (m: Pick<StoredMessage, 'participant' | 'metadata'>): string =>
|
|
647
718
|
suppliedValue((m.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? m.participant);
|
|
648
719
|
// A line names its author as the resident knows them and marks what a
|
|
@@ -653,7 +724,7 @@ export class TuneOutCoordinator {
|
|
|
653
724
|
const historyTool = this.hooks.historyTool ? this.hooks.historyTool() : 'fetch_history';
|
|
654
725
|
// The channel as the resident and the subconscious are shown it.
|
|
655
726
|
const name = this.shown(serverId, channelId);
|
|
656
|
-
const lifecycle = `[${NOTICE_TAG.tuneOut}: ${name} cancelled — ${reason}, ${
|
|
727
|
+
const lifecycle = `[${NOTICE_TAG.tuneOut}: ${name} cancelled — ${reason}, ${ending.wakeCount} wake${ending.wakeCount === 1 ? '' : 's'}]`;
|
|
657
728
|
const render = (lines: string[], truncated: number): string => {
|
|
658
729
|
const attrs = [
|
|
659
730
|
`channel=${this.quotedShown(serverId, channelId)}`,
|
|
@@ -725,14 +796,30 @@ export class TuneOutCoordinator {
|
|
|
725
796
|
m.content.some((b) => b.type !== 'text') || (newestCut && m === shown[0])),
|
|
726
797
|
];
|
|
727
798
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
799
|
+
const records = [...earlier];
|
|
800
|
+
if (earlier.length === 0 || backlog.length > 0) {
|
|
801
|
+
const record: ReleaseRecord = { ...keysOf(backlog), total: backlog.length, truncated, report: keysOf(heldForReport) };
|
|
802
|
+
this.hooks.addMessage(
|
|
803
|
+
'user',
|
|
804
|
+
[{ type: 'text', text: render(lines, truncated) }],
|
|
805
|
+
{ system: true, kind: 'tune-out-cancel', channelId, epochId, released: record },
|
|
806
|
+
);
|
|
807
|
+
records.push(record);
|
|
808
|
+
}
|
|
809
|
+
if (this.hooks.commitDeliveries?.() === false) {
|
|
810
|
+
throw new Error(`the backlog of ${this.quotedShown(serverId, channelId)} could not be made durable, so its tune-out stays in force`);
|
|
811
|
+
}
|
|
812
|
+
this.channelRegistry.cancelTuneOut(serverId, channelId, nextState, source);
|
|
813
|
+
this.clearTimers(serverId, channelId);
|
|
814
|
+
this.divertedSinceInvocation.delete(this.key(serverId, channelId));
|
|
815
|
+
const reportedEarlier = keyedBy(earlier.map((r) => r.report));
|
|
816
|
+
const release = {
|
|
817
|
+
truncated: records.reduce((n, r) => n + r.truncated, 0),
|
|
818
|
+
heldForReport: [
|
|
819
|
+
...[...storedRows, ...queuedRows].filter((m) => ofEpoch(m) && reportedEarlier(m)),
|
|
820
|
+
...(records.length > earlier.length ? heldForReport : []),
|
|
821
|
+
],
|
|
822
|
+
};
|
|
736
823
|
// The subconscious's own words, under its own name, verbatim.
|
|
737
824
|
const subName = this.hooks.subconsciousName();
|
|
738
825
|
if (subconsciousNote && subName) {
|
|
@@ -766,12 +853,10 @@ export class TuneOutCoordinator {
|
|
|
766
853
|
// and when it wrote no note and the dump left any message out, since
|
|
767
854
|
// nothing else then covers it. Otherwise its note speaks for what it saw.
|
|
768
855
|
const selfCancel = source === 'subconscious';
|
|
769
|
-
if (subName && (!selfCancel || heldForReport.length > 0 || (!subconsciousNote && truncated > 0))) {
|
|
770
|
-
const truncNote =
|
|
771
|
-
|
|
772
|
-
: ''
|
|
773
|
-
const heldNote = heldForReport.length > 0
|
|
774
|
-
? ` Still held at the cancel, so your view may not have them yet: the ${heldForReport.length} message${heldForReport.length === 1 ? '' : 's'} after this one.`
|
|
856
|
+
if (subName && (!selfCancel || release.heldForReport.length > 0 || (!subconsciousNote && release.truncated > 0))) {
|
|
857
|
+
const truncNote = truncationNote(records);
|
|
858
|
+
const heldNote = release.heldForReport.length > 0
|
|
859
|
+
? ` Still held at the cancel, so your view may not have them yet: the ${release.heldForReport.length} message${release.heldForReport.length === 1 ? '' : 's'} after this one.`
|
|
775
860
|
: '';
|
|
776
861
|
this.hooks.addMessage(
|
|
777
862
|
'user',
|
|
@@ -781,7 +866,7 @@ export class TuneOutCoordinator {
|
|
|
781
866
|
);
|
|
782
867
|
// Each held message is a row of its own, so a per-message cap cuts each
|
|
783
868
|
// one on its own, its author's label included, not the batch's newest.
|
|
784
|
-
for (const m of heldForReport) {
|
|
869
|
+
for (const m of release.heldForReport) {
|
|
785
870
|
this.hooks.addMessage(
|
|
786
871
|
'user',
|
|
787
872
|
[{ type: 'text', text: `${authorOf(m)}:` }, ...m.content],
|
|
@@ -803,9 +888,7 @@ export class TuneOutCoordinator {
|
|
|
803
888
|
const rest = this.unhandedBacklog(this.handOffKey(epochId, serverId, channelId), stored, queued);
|
|
804
889
|
const unread = rest.backlog.length + rest.queued.length;
|
|
805
890
|
if (!selfCancel || unread > 0) {
|
|
806
|
-
const truncNote =
|
|
807
|
-
? ` ${truncated} of ${backlog.length} diverted messages, the oldest, weren't delivered raw to the resident.`
|
|
808
|
-
: '';
|
|
891
|
+
const truncNote = truncationNote(records);
|
|
809
892
|
const lateNote = selfCancel
|
|
810
893
|
? ` ${unread === 1 ? 'This message' : `These ${unread} messages`} arrived after the reader that cancelled was handed its own.`
|
|
811
894
|
: '';
|
|
@@ -818,7 +901,7 @@ export class TuneOutCoordinator {
|
|
|
818
901
|
// there that its reader hasn't looked at are counted again, as the
|
|
819
902
|
// channel's own tune-out's count stood in for theirs.
|
|
820
903
|
this.rebuildTally(serverId, channelId);
|
|
821
|
-
this.hooks.emitTrace({ type: 'tune-out:cancelled', serverId, channelId, epochId, reason, wakeCount:
|
|
904
|
+
this.hooks.emitTrace({ type: 'tune-out:cancelled', serverId, channelId, epochId, reason, wakeCount: ending.wakeCount });
|
|
822
905
|
return { ok: true };
|
|
823
906
|
}
|
|
824
907
|
|
|
@@ -1043,11 +1126,6 @@ export class TuneOutCoordinator {
|
|
|
1043
1126
|
return counts;
|
|
1044
1127
|
}
|
|
1045
1128
|
|
|
1046
|
-
/**
|
|
1047
|
-
* End the all-except hold: durable flip, one release notice with a
|
|
1048
|
-
* per-channel backlog (each capped), the reader's note if any, one wake
|
|
1049
|
-
* for the resident, and the reader's own look at what it had not seen.
|
|
1050
|
-
*/
|
|
1051
1129
|
cancelAllExcept(
|
|
1052
1130
|
source: string,
|
|
1053
1131
|
reason: string,
|
|
@@ -1060,9 +1138,14 @@ export class TuneOutCoordinator {
|
|
|
1060
1138
|
|
|
1061
1139
|
const ofEpoch = (m: Pick<StoredMessage, 'metadata'>): boolean =>
|
|
1062
1140
|
(m.metadata as { tuneOut?: { epochId?: string } } | undefined)?.tuneOut?.epochId === epochId;
|
|
1063
|
-
const
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1141
|
+
const storedRows: HeldRow[] = this.hooks.getStoredMessages();
|
|
1142
|
+
const queuedRows: HeldRow[] = this.hooks.getDeferredMessages?.() ?? [];
|
|
1143
|
+
const all: HeldRow[] = [...storedRows.filter(ofEpoch), ...queuedRows.filter(ofEpoch)];
|
|
1144
|
+
const earlier = releaseRecordsOf([...storedRows, ...queuedRows], epochId);
|
|
1145
|
+
const releasedEarlier = keyedBy(earlier);
|
|
1146
|
+
const fresh = all.filter((m) => !releasedEarlier(m));
|
|
1147
|
+
const byChannel = new Map<string, { serverId: string; channelId: string; backlog: HeldRow[] }>();
|
|
1148
|
+
for (const m of fresh) {
|
|
1066
1149
|
const meta = m.metadata as { channelId?: string; serverId?: string } | undefined;
|
|
1067
1150
|
const serverId = meta?.serverId ?? '';
|
|
1068
1151
|
const channelId = meta?.channelId ?? '(unknown channel)';
|
|
@@ -1072,12 +1155,14 @@ export class TuneOutCoordinator {
|
|
|
1072
1155
|
byChannel.set(k, entry);
|
|
1073
1156
|
}
|
|
1074
1157
|
const historyTool = this.hooks.historyTool?.() ?? null;
|
|
1158
|
+
let truncatedInAll = 0;
|
|
1075
1159
|
const dumps = [...byChannel.values()].map(({ serverId, channelId, backlog }) => {
|
|
1076
1160
|
// Floored here too: a hold stored before entry floored its cap can hold
|
|
1077
1161
|
// a fraction.
|
|
1078
1162
|
const cap = Math.floor(backlogCap);
|
|
1079
1163
|
const shown = cap > 0 ? backlog.slice(-cap) : [];
|
|
1080
1164
|
const truncated = backlog.length - shown.length;
|
|
1165
|
+
truncatedInAll += truncated;
|
|
1081
1166
|
const attrs = [
|
|
1082
1167
|
`channel=${quotedValue(this.shown(serverId, channelId))}`,
|
|
1083
1168
|
`messages=${backlog.length}`,
|
|
@@ -1087,22 +1172,19 @@ export class TuneOutCoordinator {
|
|
|
1087
1172
|
});
|
|
1088
1173
|
const summary = byChannel.size === 0
|
|
1089
1174
|
? 'Nothing was held.'
|
|
1090
|
-
: `${
|
|
1091
|
-
if (
|
|
1175
|
+
: `${fresh.length} message${fresh.length === 1 ? '' : 's'} held in ${byChannel.size} channel${byChannel.size === 1 ? '' : 's'}.`;
|
|
1176
|
+
if (earlier.length === 0 || fresh.length > 0) {
|
|
1177
|
+
const record: ReleaseRecord = { ...keysOf(fresh), total: fresh.length, truncated: truncatedInAll, report: { ids: [], writeIds: [] } };
|
|
1092
1178
|
this.hooks.addMessage(
|
|
1093
1179
|
'user',
|
|
1094
1180
|
[{ type: 'text', text: `[${NOTICE_TAG.tuneOut}: all channels except ${this.shown(except.serverId, except.channelId)} released — ${reason}, ${scope.wakeCount} wake${scope.wakeCount === 1 ? '' : 's'}]\n\n${summary}${dumps.length > 0 ? '\n\n' + dumps.join('\n\n') : ''}` }],
|
|
1095
|
-
{ system: true, kind: 'tune-out-cancel', scope: 'all-except', channelId: except.channelId, epochId },
|
|
1181
|
+
{ system: true, kind: 'tune-out-cancel', scope: 'all-except', channelId: except.channelId, epochId, released: record },
|
|
1096
1182
|
);
|
|
1097
|
-
this.hooks.commitDeliveries?.();
|
|
1098
|
-
this.scopeReleased = epochId;
|
|
1099
1183
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
// delivers the release again, where the other order lost it outright.
|
|
1184
|
+
if (this.hooks.commitDeliveries?.() === false) {
|
|
1185
|
+
throw new Error('the all-except release could not be made durable, so the hold stays in force');
|
|
1186
|
+
}
|
|
1104
1187
|
this.writeScope(null);
|
|
1105
|
-
this.scopeReleased = undefined;
|
|
1106
1188
|
this.clearScopeTimers();
|
|
1107
1189
|
for (const [key, pending] of this.pendingWakes) {
|
|
1108
1190
|
const [serverId, channelId] = key.split('\0') as [string, string];
|
|
@@ -1335,6 +1417,31 @@ export class TuneOutCoordinator {
|
|
|
1335
1417
|
this.pendingWakes.set(key, { count: 1, timer });
|
|
1336
1418
|
}
|
|
1337
1419
|
|
|
1420
|
+
private cancelPastBudget(serverId: string, channelId: string, maxWakes: number, from: 'wake' | 'start' = 'wake'): void {
|
|
1421
|
+
const key = this.key(serverId, channelId);
|
|
1422
|
+
const existing = this.expiryTimers.get(key);
|
|
1423
|
+
if (existing) clearTimeout(existing);
|
|
1424
|
+
const end = (first: boolean): void => {
|
|
1425
|
+
try {
|
|
1426
|
+
this.expiryTimers.set(key, setTimeout(() => end(false), EXPIRY_RETRY_MS));
|
|
1427
|
+
if (!this.cancel(serverId, channelId, 'tune-out-max-wakes', `wake budget exhausted (${maxWakes})`).ok) {
|
|
1428
|
+
this.clearTimers(serverId, channelId);
|
|
1429
|
+
}
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
if (first && from === 'start') {
|
|
1432
|
+
if (this.channelRegistry.getTuneOutState(serverId, channelId)) {
|
|
1433
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} was past its wake budget at start, and its cancel failed, so it stays tuned out and its cancel is tried again in ${EXPIRY_RETRY_MS / 1000} s:`, err);
|
|
1434
|
+
} else {
|
|
1435
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} was past its wake budget at start and its cancel ended it, but a step after the end failed:`, err);
|
|
1436
|
+
}
|
|
1437
|
+
} else {
|
|
1438
|
+
this.timerFailed('wake', serverId, channelId, err);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
end(true);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1338
1445
|
/** One coalesced wake: counted durably against the epoch that holds the channel. */
|
|
1339
1446
|
private fireWake(serverId: string, channelId: string, count: number): void {
|
|
1340
1447
|
if (this.channelRegistry.getTuneOutState(serverId, channelId)) {
|
|
@@ -1342,12 +1449,7 @@ export class TuneOutCoordinator {
|
|
|
1342
1449
|
const counted = this.channelRegistry.recordTuneOutWake(serverId, channelId);
|
|
1343
1450
|
if (!counted) return; // cancelled inside the window
|
|
1344
1451
|
if (counted.wakeCount > counted.params.maxWakes) {
|
|
1345
|
-
this.
|
|
1346
|
-
serverId,
|
|
1347
|
-
channelId,
|
|
1348
|
-
'tune-out-max-wakes',
|
|
1349
|
-
`wake budget exhausted (${counted.params.maxWakes})`,
|
|
1350
|
-
);
|
|
1452
|
+
this.cancelPastBudget(serverId, channelId, counted.params.maxWakes);
|
|
1351
1453
|
return;
|
|
1352
1454
|
}
|
|
1353
1455
|
this.invokeSubconscious(serverId, channelId, 'wake', count);
|
package/src/tune-out/tools.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { StoredMessage } from '@animalabs/context-manager';
|
|
|
14
14
|
* never leak bare prose to the default locus.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { ToolDefinition } from '../types/index.js';
|
|
17
|
+
import type { AgentConfig, ToolDefinition } from '../types/index.js';
|
|
18
18
|
|
|
19
19
|
export const SUBCONSCIOUS_TOOL_NAMES = [
|
|
20
20
|
'deliver_summary',
|
|
@@ -143,6 +143,17 @@ interface SubconsciousConfigBase {
|
|
|
143
143
|
* shared prefix).
|
|
144
144
|
*/
|
|
145
145
|
allowChannelSpeech?: boolean;
|
|
146
|
+
/** The reader's model's context window, as AgentConfig.physicalWindowTokens,
|
|
147
|
+
* in place of the primary's. Left out or null, a reader on the primary's
|
|
148
|
+
* model has the primary's; on another model, the persistent reader has
|
|
149
|
+
* none and a fork has the primary's. */
|
|
150
|
+
physicalWindowTokens?: AgentConfig['physicalWindowTokens'];
|
|
151
|
+
/** The reader's model's thinking binding, as AgentConfig.thinkingBinding,
|
|
152
|
+
* in place of the primary's. Left out, null or false, a reader on the
|
|
153
|
+
* primary's model has the primary's; on another model, the persistent
|
|
154
|
+
* reader has none and a fork has the primary's. `{ binds: false }` gives
|
|
155
|
+
* a reader none. */
|
|
156
|
+
thinkingBinding?: AgentConfig['thinkingBinding'];
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
export interface PersistentReaderConfig extends SubconsciousConfigBase {
|
|
@@ -156,7 +167,8 @@ export interface PersistentReaderConfig extends SubconsciousConfigBase {
|
|
|
156
167
|
/** Model id; defaults to the primary agent's (same-model side-process).
|
|
157
168
|
* Another id, compared exactly, makes the subconscious leave out the
|
|
158
169
|
* fields that describe the primary's model, its window and its thinking
|
|
159
|
-
* binding (SUBCONSCIOUS_MODEL_FIELDS);
|
|
170
|
+
* binding (SUBCONSCIOUS_MODEL_FIELDS); `physicalWindowTokens` and
|
|
171
|
+
* `thinkingBinding` set its own. */
|
|
160
172
|
model?: string;
|
|
161
173
|
/**
|
|
162
174
|
* The voice/criteria mode block — recipe-side and co-authored with the
|
package/src/types/agent.ts
CHANGED
|
@@ -539,7 +539,8 @@ export interface InferenceOptions {
|
|
|
539
539
|
* `supportsInjectedMessages` is what the stream declared with the round's
|
|
540
540
|
* tool calls (membrane's `ToolContext.supportsInjectedMessages`): whether it
|
|
541
541
|
* can carry messages injected with that round's results. It's absent when
|
|
542
|
-
* the stream doesn't say, as
|
|
542
|
+
* the stream doesn't say, which reads as a stream that can't, and when there
|
|
543
|
+
* is no stream, as for the direct API.
|
|
543
544
|
*/
|
|
544
545
|
export type AgentState =
|
|
545
546
|
| { status: 'idle' }
|
package/src/types/framework.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ProcessEvent } from './events.js';
|
|
|
6
6
|
import type { McplServerConfig, InferenceRoutingPolicy } from '../mcpl/types.js';
|
|
7
7
|
import type { ConversationRouterConfig } from '../mcpl/conversation-router.js';
|
|
8
8
|
import type { GateOptions } from '../gate/types.js';
|
|
9
|
+
import type { InferenceBoundsOptions } from '../inference-bounds.js';
|
|
9
10
|
|
|
10
11
|
// Re-export trace types
|
|
11
12
|
export type { TraceEvent, TraceEventListener } from './trace.js';
|
|
@@ -299,6 +300,30 @@ export interface FrameworkConfig {
|
|
|
299
300
|
/** Custom error policy */
|
|
300
301
|
errorPolicy?: ErrorPolicy;
|
|
301
302
|
|
|
303
|
+
/**
|
|
304
|
+
* How long a model call may go without progress before it is cut. A
|
|
305
|
+
* resident's streamed call, and an MCPL server's, gets membrane's progress
|
|
306
|
+
* bounds where its adapter's stream reports content (Anthropic, Bedrock),
|
|
307
|
+
* and on every adapter a flat bound from its last provider event: the
|
|
308
|
+
* larger of a setup term (the larger of `beforeContentMs` and
|
|
309
|
+
* `betweenEventsMs`) plus the request's `maxTokens` at
|
|
310
|
+
* `thinkTokensPerSecond`, and `flatMinimumMs`. An auxiliary call
|
|
311
|
+
* (compression, the reply judge) and an MCPL server's non-streamed call get
|
|
312
|
+
* a bound on the whole call from its send, the setup term plus that
|
|
313
|
+
* allowance (the larger of the setup term and `flatMinimumMs` when it
|
|
314
|
+
* names no `maxTokens`), unless its
|
|
315
|
+
* caller passed a `progressTimeoutMs` of its own; where an adapter's
|
|
316
|
+
* `complete()` streams under the hood, membrane renews it at each parsed
|
|
317
|
+
* provider event. A call a bound cuts fails
|
|
318
|
+
* with type `timeout`, and on an agent's turn DefaultErrorPolicy gives each
|
|
319
|
+
* such call one fresh call, except in a turn that re-arms an exhausted one,
|
|
320
|
+
* where `exhaustedTurnRearmMs` says what a cut does. One setting covers
|
|
321
|
+
* every agent. `false` sets
|
|
322
|
+
* none of these bounds. Default: membrane's PROGRESS_BOUNDS and
|
|
323
|
+
* FLAT_MINIMUM_MS.
|
|
324
|
+
*/
|
|
325
|
+
inferenceBounds?: false | InferenceBoundsOptions;
|
|
326
|
+
|
|
302
327
|
/**
|
|
303
328
|
* Host verdict on a failed inference: "this cannot succeed until later".
|
|
304
329
|
* Consulted before the error policy and before the built-in
|
|
@@ -337,20 +362,32 @@ export interface FrameworkConfig {
|
|
|
337
362
|
* channel isn't sent again: one whose turn was woken for a channel id that
|
|
338
363
|
* names no one registered channel and never found one, and whose request
|
|
339
364
|
* names none of its own. A failure that states a wait is held for that wait
|
|
340
|
-
* first, as any turn's is, and a
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* of the re-armed turn's rounds has run
|
|
345
|
-
* failure gets no retries from the error
|
|
346
|
-
* failure the next delay follows, until the
|
|
347
|
-
* any other kind ends the re-arming.
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
365
|
+
* first, as any turn's is, and a turn that ends on a deadline, whether the
|
|
366
|
+
* host set it or membrane marks one as run out (the runtime's or undici's
|
|
367
|
+
* on a sent request, the Anthropic adapter's watchdogs, or the Anthropic
|
|
368
|
+
* SDK's timeout, which covers its connect timeout too), isn't re-armed,
|
|
369
|
+
* save the exception below. Until one of the re-armed turn's rounds has run
|
|
370
|
+
* or had its tool calls cut off, its failure gets no retries from the error
|
|
371
|
+
* policy: on another transient failure the next delay follows, until the
|
|
372
|
+
* list runs out, and a failure of any other kind ends the re-arming. The
|
|
373
|
+
* one exception is a re-armed turn's send that fails with a deadline marked
|
|
374
|
+
* as run out, membrane's or a bound the host sets on the call
|
|
375
|
+
* (`inferenceBounds`), as when a stream stalls. Such a cut counts as a
|
|
376
|
+
* transient failure, so a turn that ends on it is re-armed as for a
|
|
377
|
+
* transient failure, unless it follows another such cut with no transient
|
|
378
|
+
* failure, answered round or message said to the agent between them: two
|
|
379
|
+
* such cuts in a row end the re-arming, and the second gets no retry from
|
|
380
|
+
* the error policy, even in a re-armed turn that got partway. A transient
|
|
381
|
+
* failure or an answered round in any of the agent's turns clears an
|
|
382
|
+
* earlier cut, and so does a message said to the agent, or the agent's end.
|
|
383
|
+
* Every such cut counts, including one the error policy retries or a host's
|
|
384
|
+
* provider hold takes. A wake for the agent while it waits starts its turn
|
|
385
|
+
* at once, with the re-armed trigger merged in. That turn is the
|
|
386
|
+
* wake's: the error policy retries its failures. A completed turn starts the
|
|
387
|
+
* count again, and so does a message said to the agent: not a system message
|
|
388
|
+
* or notice; not an edit, reaction or deletion, unless an edit is the first
|
|
389
|
+
* version of a message the agent sees; not a message its own view leaves
|
|
390
|
+
* out, such as one a tune-out holds from it; and not the agent's own
|
|
354
391
|
* schedule, the heartbeat's tick or reminder, a self-wake timer it set, or a
|
|
355
392
|
* script's wake. A notice a module or an MCPL server stores as a message
|
|
356
393
|
* counts. A message in the residents' shared slot counts for each resident
|
|
@@ -558,7 +595,10 @@ export interface InferencePolicy {
|
|
|
558
595
|
*/
|
|
559
596
|
export interface ErrorPolicy {
|
|
560
597
|
/**
|
|
561
|
-
* Handle an inference error.
|
|
598
|
+
* Handle an inference error. `attempt` is how many times the turn's
|
|
599
|
+
* request has failed before this one; for a deadline expiry (a progress
|
|
600
|
+
* bound's cut, or one membrane marks), how many times that call's request
|
|
601
|
+
* has expired in a row before this one.
|
|
562
602
|
*/
|
|
563
603
|
onInferenceError(
|
|
564
604
|
error: Error,
|
package/src/types/module.ts
CHANGED
|
@@ -120,7 +120,10 @@ export interface Module {
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* Start the module.
|
|
123
|
-
* Called when the
|
|
123
|
+
* Called when the module is added: create() adds the modules it is given,
|
|
124
|
+
* so for those it runs before create() returns. Its own removal and the
|
|
125
|
+
* framework's stop() wait for it to settle before the module's stop()
|
|
126
|
+
* runs, so it mustn't await either.
|
|
124
127
|
*/
|
|
125
128
|
start(ctx: ModuleContext): Promise<void>;
|
|
126
129
|
|
|
@@ -132,6 +135,8 @@ export interface Module {
|
|
|
132
135
|
* a host wires up after create() returns, such as a WorkspaceModule's
|
|
133
136
|
* initStore(). A stop() that saves state can therefore run before that
|
|
134
137
|
* wiring, and shouldn't then save over state it was handed and hasn't used.
|
|
138
|
+
* It isn't called before the module's start() has settled, so it can end
|
|
139
|
+
* what start() set up.
|
|
135
140
|
*/
|
|
136
141
|
stop(): Promise<void>;
|
|
137
142
|
|
|
@@ -259,6 +264,41 @@ export interface AgentSettingsExtension {
|
|
|
259
264
|
|
|
260
265
|
/**
|
|
261
266
|
* Context provided to modules for interacting with the framework.
|
|
267
|
+
*
|
|
268
|
+
* Once the module's `stop()` has completed, through `removeModule()` or the
|
|
269
|
+
* framework's `stop()`, its context has no effect on the framework:
|
|
270
|
+
* - each push is dropped with a log line, as a push after the framework's
|
|
271
|
+
* `stop()` is;
|
|
272
|
+
* - `setState`, `registerLogState`, `appendToLog`, `editLogItem`,
|
|
273
|
+
* `editMessage`, `removeMessage`, `notifyOps`, `registerSpeechHandler` and
|
|
274
|
+
* `unregisterSpeechHandler` do nothing; `addMessage` returns `''` and adds
|
|
275
|
+
* nothing; `callTool` resolves with an error result and runs nothing;
|
|
276
|
+
* `getModule` returns `null`; and `onTrace` subscribes nothing, while the
|
|
277
|
+
* subscriptions made through the context end. None of these throws, and
|
|
278
|
+
* the first such call of each method logs one line naming the module and
|
|
279
|
+
* the method;
|
|
280
|
+
* - `findMessageByExternalId` answers without updating its mapping, and logs
|
|
281
|
+
* nothing.
|
|
282
|
+
*
|
|
283
|
+
* Its reads (`getState`, `getLog`, `getLogLength`, `getMessage`,
|
|
284
|
+
* `queryMessages`, `getAgents`, `getActiveTools`, `findMessageByExternalId`)
|
|
285
|
+
* answer, and fail, as before; one that meets a held write may record that
|
|
286
|
+
* the write has landed, as any reader's does. The framework calls none of the
|
|
287
|
+
* module's `onProcess`, `onAgentSpeech` and `onToolBatchComplete` again; it
|
|
288
|
+
* drops whole a response of its that hadn't been applied by then, with a line
|
|
289
|
+
* naming what the response asked for if it asked for anything; and it leaves
|
|
290
|
+
* its `gatherContext` answer out of a turn whose answers it hadn't merged by
|
|
291
|
+
* then, with a line if it held injections. After `removeModule()`, the result
|
|
292
|
+
* its `handleToolCall` returns for a call it was running when its `stop()`
|
|
293
|
+
* completed is still delivered while the framework runs. The framework's
|
|
294
|
+
* `stop()` closes its event queue before it stops any module, so through the
|
|
295
|
+
* framework's `stop()` that result is dropped, with the line any tool result
|
|
296
|
+
* arriving after the framework's `stop()` gets. A result it pushes through
|
|
297
|
+
* its context after its `stop()` completes is dropped, as its other pushes
|
|
298
|
+
* are. A module whose `stop()` fails stays registered, and its context keeps
|
|
299
|
+
* working.
|
|
300
|
+
* References the module already holds, to other modules or to its own
|
|
301
|
+
* timers, stay as they are.
|
|
262
302
|
*/
|
|
263
303
|
export interface ModuleContext {
|
|
264
304
|
/**
|
|
@@ -366,8 +406,7 @@ export interface ModuleContext {
|
|
|
366
406
|
* and a tool round moves when it injects messages into a stream that
|
|
367
407
|
* declares it carries them. So one that waited for its target's end (a
|
|
368
408
|
* run's end, or a fork's last turn), one stored while it was idle and
|
|
369
|
-
* never sent, one stored at a tool round that ended the turn
|
|
370
|
-
* injected into a stream that didn't declare it carries injections go to
|
|
409
|
+
* never sent, and one stored at a tool round that ended the turn go to
|
|
371
410
|
* the primary. The framework's own
|
|
372
411
|
* writes for a run or a fork are unchanged: they stay in its store,
|
|
373
412
|
* unless they're notices the primary already takes in their place (a
|