@animalabs/agent-framework 0.32.0 → 0.33.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 +204 -14
- package/dist/src/admission/admit.d.ts +18 -0
- package/dist/src/admission/admit.d.ts.map +1 -1
- package/dist/src/admission/admit.js +16 -3
- package/dist/src/admission/admit.js.map +1 -1
- package/dist/src/agent.d.ts +185 -78
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +279 -102
- 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 +50 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +4 -20
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +24 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/config-checks.d.ts +7 -0
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +14 -0
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/context-manager-errors.d.ts +25 -0
- package/dist/src/context-manager-errors.d.ts.map +1 -0
- package/dist/src/context-manager-errors.js +31 -0
- package/dist/src/context-manager-errors.js.map +1 -0
- package/dist/src/context-options.d.ts +67 -0
- package/dist/src/context-options.d.ts.map +1 -0
- package/dist/src/context-options.js +85 -0
- package/dist/src/context-options.js.map +1 -0
- package/dist/src/deferred-import.d.ts +93 -0
- package/dist/src/deferred-import.d.ts.map +1 -0
- package/dist/src/deferred-import.js +177 -0
- package/dist/src/deferred-import.js.map +1 -0
- package/dist/src/dendrite/audience.d.ts +223 -0
- package/dist/src/dendrite/audience.d.ts.map +1 -0
- package/dist/src/dendrite/audience.js +202 -0
- package/dist/src/dendrite/audience.js.map +1 -0
- package/dist/src/dendrite/index.d.ts +5 -3
- package/dist/src/dendrite/index.d.ts.map +1 -1
- package/dist/src/dendrite/index.js +2 -1
- package/dist/src/dendrite/index.js.map +1 -1
- package/dist/src/dendrite/presets.d.ts +10 -5
- package/dist/src/dendrite/presets.d.ts.map +1 -1
- package/dist/src/dendrite/presets.js +17 -5
- package/dist/src/dendrite/presets.js.map +1 -1
- package/dist/src/dendrite/registry.d.ts +62 -7
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +179 -10
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/dendrite/types.d.ts +91 -8
- package/dist/src/dendrite/types.d.ts.map +1 -1
- package/dist/src/foreign-rounds.d.ts +69 -0
- package/dist/src/foreign-rounds.d.ts.map +1 -0
- package/dist/src/foreign-rounds.js +152 -0
- package/dist/src/foreign-rounds.js.map +1 -0
- package/dist/src/framework.d.ts +1976 -818
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +7566 -3422
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +85 -15
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +421 -47
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/rule-scope.d.ts +30 -0
- package/dist/src/gate/rule-scope.d.ts.map +1 -0
- package/dist/src/gate/rule-scope.js +41 -0
- package/dist/src/gate/rule-scope.js.map +1 -0
- package/dist/src/gate/suppress-script-worker.d.ts +2 -0
- package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
- package/dist/src/gate/suppress-script-worker.js +103 -0
- package/dist/src/gate/suppress-script-worker.js.map +1 -0
- package/dist/src/gate/suppress-scripts.d.ts +105 -0
- package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
- package/dist/src/gate/suppress-scripts.js +403 -0
- package/dist/src/gate/suppress-scripts.js.map +1 -0
- package/dist/src/gate/suppression-review.d.ts +143 -0
- package/dist/src/gate/suppression-review.d.ts.map +1 -0
- package/dist/src/gate/suppression-review.js +463 -0
- package/dist/src/gate/suppression-review.js.map +1 -0
- package/dist/src/gate/suppression-stats.d.ts +125 -0
- package/dist/src/gate/suppression-stats.d.ts.map +1 -0
- package/dist/src/gate/suppression-stats.js +214 -0
- package/dist/src/gate/suppression-stats.js.map +1 -0
- package/dist/src/gate/types.d.ts +135 -7
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/gate/wake-onboarding.d.ts +119 -0
- package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
- package/dist/src/gate/wake-onboarding.js +277 -0
- package/dist/src/gate/wake-onboarding.js.map +1 -0
- package/dist/src/held-writes.d.ts +31 -23
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +44 -59
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsonrpc-envelope.d.ts +69 -0
- package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
- package/dist/src/jsonrpc-envelope.js +66 -0
- package/dist/src/jsonrpc-envelope.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +131 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +290 -46
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +2 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +6 -6
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -22
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +105 -15
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +9 -7
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +13 -10
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +134 -56
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.js +3 -0
- package/dist/src/mcpl/tool-classes.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +8 -2
- 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 +24 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -18
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +45 -8
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +237 -86
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +59 -0
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +432 -67
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +6 -5
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +15 -22
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +1 -1
- package/dist/src/modules/workspace/index.d.ts +28 -7
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +76 -22
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -1
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -1
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/operator-log.d.ts +36 -3
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js +2 -2
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/presentation.d.ts +746 -0
- package/dist/src/presentation.d.ts.map +1 -0
- package/dist/src/presentation.js +1435 -0
- package/dist/src/presentation.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +15 -11
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +16 -12
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/read-marks.d.ts +192 -0
- package/dist/src/read-marks.d.ts.map +1 -0
- package/dist/src/read-marks.js +242 -0
- package/dist/src/read-marks.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +91 -6
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +273 -29
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +22 -3
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +5 -9
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +40 -7
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/stored-injections.d.ts +2 -2
- package/dist/src/stored-injections.js +2 -2
- package/dist/src/stored-removal.d.ts +17 -4
- package/dist/src/stored-removal.d.ts.map +1 -1
- package/dist/src/stored-removal.js +29 -5
- package/dist/src/stored-removal.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +12 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +42 -8
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +1159 -79
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +3709 -363
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +114 -17
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +87 -27
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/tune-out/window-anchor.d.ts +143 -0
- package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
- package/dist/src/tune-out/window-anchor.js +202 -0
- package/dist/src/tune-out/window-anchor.js.map +1 -0
- package/dist/src/types/agent.d.ts +82 -20
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +2 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +32 -12
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +180 -69
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +510 -36
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +32 -3
- package/src/agent.ts +323 -120
- package/src/api/mcp-server-core.ts +54 -17
- package/src/api/server.ts +4 -23
- package/src/code-execution/isolate/scripts.ts +28 -3
- package/src/config-checks.ts +14 -0
- package/src/context-manager-errors.ts +37 -0
- package/src/context-options.ts +96 -0
- package/src/deferred-import.ts +239 -0
- package/src/dendrite/audience.ts +315 -0
- package/src/dendrite/index.ts +14 -2
- package/src/dendrite/presets.ts +18 -6
- package/src/dendrite/registry.ts +209 -12
- package/src/dendrite/types.ts +92 -8
- package/src/foreign-rounds.ts +199 -0
- package/src/framework.ts +7804 -3546
- package/src/gate/event-gate.ts +428 -65
- package/src/gate/index.ts +3 -0
- package/src/gate/rule-scope.ts +45 -0
- package/src/gate/suppress-script-worker.ts +89 -0
- package/src/gate/suppress-scripts.ts +414 -0
- package/src/gate/suppression-review.ts +517 -0
- package/src/gate/suppression-stats.ts +314 -0
- package/src/gate/types.ts +141 -7
- package/src/gate/wake-onboarding.ts +309 -0
- package/src/held-writes.ts +49 -60
- package/src/index.ts +9 -2
- package/src/jsonrpc-envelope.ts +81 -0
- package/src/mcpl/channel-registry.ts +353 -85
- package/src/mcpl/checkpoint-manager.ts +3 -2
- package/src/mcpl/feature-set-manager.ts +2 -1
- package/src/mcpl/inbound-source.ts +2 -1
- package/src/mcpl/inference-router.ts +7 -4
- package/src/mcpl/push-coalescer.ts +193 -34
- package/src/mcpl/push-handler.ts +10 -5
- package/src/mcpl/server-connection.ts +141 -60
- package/src/mcpl/tool-classes.ts +3 -0
- package/src/mcpl/types.ts +8 -2
- package/src/module-registry.ts +73 -17
- package/src/modules/discord/index.ts +229 -81
- package/src/modules/history/index.ts +439 -70
- package/src/modules/history/semantic.ts +15 -21
- package/src/modules/workspace/disk-agreement.ts +1 -1
- package/src/modules/workspace/index.ts +85 -23
- package/src/notice-tags.ts +10 -1
- package/src/operator-log.ts +36 -3
- package/src/presentation.ts +1676 -0
- package/src/prose-drafts.ts +16 -12
- package/src/read-marks.ts +328 -0
- package/src/recovery/offline-branch.ts +346 -32
- package/src/recovery/recover-cli.ts +21 -3
- package/src/send-tools.ts +40 -7
- package/src/stored-injections.ts +2 -2
- package/src/stored-removal.ts +31 -7
- package/src/tool-result-guard.ts +41 -7
- package/src/tune-out/coordinator.ts +4089 -457
- package/src/tune-out/tools.ts +160 -43
- package/src/tune-out/window-anchor.ts +277 -0
- package/src/types/agent.ts +86 -20
- package/src/types/events.ts +2 -0
- package/src/types/framework.ts +32 -12
- package/src/types/module.ts +181 -68
- package/src/types/trace.ts +542 -40
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import WebSocket from 'ws';
|
|
15
15
|
import { escapeLineSeparators, readLines } from '../line-reader.js';
|
|
16
16
|
import type { ApiRequest, ApiResponse, ApiEvent } from './types.js';
|
|
17
|
+
import { jsonKind, readEnvelope } from '../jsonrpc-envelope.js';
|
|
18
|
+
import { logValue } from '../mcpl/log-value.js';
|
|
17
19
|
|
|
18
20
|
// ============================================================================
|
|
19
21
|
// MCP Protocol Types
|
|
@@ -21,7 +23,8 @@ import type { ApiRequest, ApiResponse, ApiEvent } from './types.js';
|
|
|
21
23
|
|
|
22
24
|
interface JsonRpcRequest {
|
|
23
25
|
jsonrpc: '2.0';
|
|
24
|
-
|
|
26
|
+
/** null is allowed, though JSON-RPC 2.0 discourages it. */
|
|
27
|
+
id: string | number | null;
|
|
25
28
|
method: string;
|
|
26
29
|
params?: unknown;
|
|
27
30
|
}
|
|
@@ -184,20 +187,58 @@ export class McpServer {
|
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
private handleInput(line: string): void {
|
|
190
|
+
let message: unknown;
|
|
187
191
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Check if this is a notification (no id) or a request (has id)
|
|
191
|
-
if (!('id' in message) || message.id === undefined) {
|
|
192
|
-
// This is a notification - handle silently without response
|
|
193
|
-
// Common notifications: notifications/initialized, notifications/cancelled
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// This is a request - process and respond
|
|
198
|
-
this.handleRequest(message as JsonRpcRequest);
|
|
192
|
+
message = JSON.parse(line);
|
|
199
193
|
} catch {
|
|
200
194
|
this.sendError(null, -32700, 'Parse error');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// A request is an object (JSON-RPC 2.0 §4), so any other JSON value is an
|
|
198
|
+
// invalid request, answered with id null (§5). An array is JSON-RPC's
|
|
199
|
+
// batch, which this server doesn't take.
|
|
200
|
+
if (Array.isArray(message)) {
|
|
201
|
+
this.sendError(null, -32600, "Invalid Request: this server doesn't accept batches, so send each message on a line of its own");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (message === null || typeof message !== 'object') {
|
|
205
|
+
this.sendError(null, -32600, `Invalid Request: a message must be an object, not ${jsonKind(message)}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The envelope is read once, here (src/jsonrpc-envelope.ts says how).
|
|
210
|
+
const envelope = readEnvelope(message as Record<string, unknown>);
|
|
211
|
+
switch (envelope.kind) {
|
|
212
|
+
case 'request':
|
|
213
|
+
void this.handleRequest(message as JsonRpcRequest);
|
|
214
|
+
return;
|
|
215
|
+
case 'invalid':
|
|
216
|
+
this.sendError(envelope.id, envelope.error.code, envelope.error.message);
|
|
217
|
+
return;
|
|
218
|
+
case 'response': {
|
|
219
|
+
// This server sends its client no requests, so a response can't be
|
|
220
|
+
// matched to one, and it's never answered. A client that couldn't
|
|
221
|
+
// read a line this server sent, whatever it was, may answer it with
|
|
222
|
+
// id null, or under MCP with no id. The error member, if it has one,
|
|
223
|
+
// is printed through logValue, which escapes and bounds the client's
|
|
224
|
+
// text, and so is the id when it's a string or a number; any other
|
|
225
|
+
// id is named by its kind.
|
|
226
|
+
const { id, error } = message as { id?: unknown; error?: unknown };
|
|
227
|
+
const which = !Object.hasOwn(message as object, 'id') ? 'with no id'
|
|
228
|
+
: `whose id is ${typeof id === 'string' || typeof id === 'number' ? logValue(id) : jsonKind(id)}`;
|
|
229
|
+
console.error(`[agent-framework-mcp] ignored a response ${which}, since this server sends no requests` +
|
|
230
|
+
(error !== undefined ? `; its error: ${logValue(error)}` : ''));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
case 'notification':
|
|
234
|
+
// notifications/initialized, notifications/cancelled and the like
|
|
235
|
+
// need no answer.
|
|
236
|
+
return;
|
|
237
|
+
case 'neither':
|
|
238
|
+
// Not a Request object, which needs a method, nor a response: an
|
|
239
|
+
// invalid request, as a value that isn't an object is.
|
|
240
|
+
this.sendError(null, -32600, 'Invalid Request: a message needs a method or an id');
|
|
241
|
+
return;
|
|
201
242
|
}
|
|
202
243
|
}
|
|
203
244
|
|
|
@@ -217,10 +258,6 @@ export class McpServer {
|
|
|
217
258
|
});
|
|
218
259
|
break;
|
|
219
260
|
|
|
220
|
-
case 'initialized':
|
|
221
|
-
// Client acknowledgment, no response needed
|
|
222
|
-
break;
|
|
223
|
-
|
|
224
261
|
case 'tools/list':
|
|
225
262
|
this.sendResult(request.id, { tools: this.getTools() });
|
|
226
263
|
break;
|
|
@@ -635,7 +672,7 @@ export class McpServer {
|
|
|
635
672
|
});
|
|
636
673
|
}
|
|
637
674
|
|
|
638
|
-
private sendResult(id: string | number, result: unknown): void {
|
|
675
|
+
private sendResult(id: string | number | null, result: unknown): void {
|
|
639
676
|
const response: JsonRpcResponse = {
|
|
640
677
|
jsonrpc: '2.0',
|
|
641
678
|
id,
|
package/src/api/server.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { createServer, IncomingMessage, ServerResponse } from 'node:http';
|
|
|
11
11
|
import { timingSafeEqual } from 'node:crypto';
|
|
12
12
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
13
13
|
import type { AgentFramework } from '../framework.js';
|
|
14
|
-
import {
|
|
14
|
+
import { ResumeBlockedError } from '../framework.js';
|
|
15
15
|
import { isReleaseModel } from '../provider-waits.js';
|
|
16
16
|
import type { TraceEvent, ProcessEvent } from '../types/index.js';
|
|
17
17
|
import type {
|
|
@@ -601,12 +601,14 @@ export class ApiServer {
|
|
|
601
601
|
// during a live turn or once stop() has begun, and stop() waits for one
|
|
602
602
|
// already running. It moves the store once any maintenance already
|
|
603
603
|
// running has finished, so the move can come after this call returns.
|
|
604
|
+
// It writes the switch to the operator log as a checkout over this
|
|
605
|
+
// surface.
|
|
604
606
|
// The event names this switch's own move, from the branch read here to
|
|
605
607
|
// the one it was asked for: another switch can move the store again
|
|
606
608
|
// before this one continues.
|
|
607
609
|
let switched = false;
|
|
608
610
|
try {
|
|
609
|
-
await this.framework.switchBranch(params.name);
|
|
611
|
+
await this.framework.switchBranch(params.name, { requester: { via: 'api' } });
|
|
610
612
|
switched = true;
|
|
611
613
|
} finally {
|
|
612
614
|
// A later step can fail after the store has moved: clients still hear
|
|
@@ -655,15 +657,6 @@ export class ApiServer {
|
|
|
655
657
|
|
|
656
658
|
const { settled, ...result } = this.framework.undoLastTurn(params.agentName);
|
|
657
659
|
await settled;
|
|
658
|
-
|
|
659
|
-
if (result.undone) {
|
|
660
|
-
this.broadcast('branch:switched', {
|
|
661
|
-
from: result.fromBranch,
|
|
662
|
-
to: result.toBranch,
|
|
663
|
-
reason: 'undo',
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
|
|
667
660
|
return result;
|
|
668
661
|
}
|
|
669
662
|
|
|
@@ -674,15 +667,6 @@ export class ApiServer {
|
|
|
674
667
|
|
|
675
668
|
const { settled, ...result } = this.framework.redo(params.agentName);
|
|
676
669
|
await settled;
|
|
677
|
-
|
|
678
|
-
if (result.redone) {
|
|
679
|
-
this.broadcast('branch:switched', {
|
|
680
|
-
from: result.fromBranch,
|
|
681
|
-
to: result.toBranch,
|
|
682
|
-
reason: 'redo',
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
670
|
return result;
|
|
687
671
|
}
|
|
688
672
|
|
|
@@ -1080,9 +1064,6 @@ export class ApiServer {
|
|
|
1080
1064
|
verdicts: error.verdicts,
|
|
1081
1065
|
hostMode: this.framework.getHostModeStatus(),
|
|
1082
1066
|
});
|
|
1083
|
-
} else if (error instanceof QuiesceBlockedError) {
|
|
1084
|
-
// A refusal, as resume()'s is: the host's mode is as it was.
|
|
1085
|
-
respond(409, { error: error.message, hostMode: this.framework.getHostModeStatus() });
|
|
1086
1067
|
} else {
|
|
1087
1068
|
respond(500, { error: error instanceof Error ? error.message : String(error) });
|
|
1088
1069
|
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { NOTICE_TAG } from '../../notice-tags.js';
|
|
30
30
|
import { ChildCapacity, ScriptRun, type CallAnswer, type Ending, type ScriptRunOptions, type ToolAnswer } from './runner.js';
|
|
31
31
|
import type { CallSummary } from './calls.js';
|
|
32
|
+
import type { ReadMark } from '../../read-marks.js';
|
|
32
33
|
import type { OutputBounds, OutputWindow } from './output.js';
|
|
33
34
|
import type { PreludeTool } from './prelude.js';
|
|
34
35
|
import { glimpse } from './render.js';
|
|
@@ -51,7 +52,7 @@ export interface ScriptCallContext {
|
|
|
51
52
|
/** The turn the script's run call was made in, when one was: the turn its words were written in, which a later turn's wait doesn't change. */
|
|
52
53
|
startTurnId?: number;
|
|
53
54
|
/** What its agent had read when the run call started the script (the host's consumed watermark), given at run: what the script's words were written having read, which a later round's wait doesn't change. */
|
|
54
|
-
startReadThrough?:
|
|
55
|
+
startReadThrough?: ReadMark | null;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export interface ScriptsHost {
|
|
@@ -119,7 +120,7 @@ interface Script {
|
|
|
119
120
|
/** The code it runs. */
|
|
120
121
|
code: string;
|
|
121
122
|
run: ScriptRun;
|
|
122
|
-
context: { origin?: unknown; startReadThrough?:
|
|
123
|
+
context: { origin?: unknown; startReadThrough?: ReadMark | null };
|
|
123
124
|
/** The turn its run call was made in, when one was: its agent's turns end it by that. */
|
|
124
125
|
turnId: number | undefined;
|
|
125
126
|
startedAt: number;
|
|
@@ -182,7 +183,7 @@ export class AgentScripts {
|
|
|
182
183
|
agentName: string,
|
|
183
184
|
code: string,
|
|
184
185
|
askedMs?: number,
|
|
185
|
-
options: { origin?: unknown; watch?: ScriptWatch; startReadThrough?:
|
|
186
|
+
options: { origin?: unknown; watch?: ScriptWatch; startReadThrough?: ReadMark | null } = {},
|
|
186
187
|
): Promise<ScriptReturn> {
|
|
187
188
|
// Listed first, so a module whose tool list throws refuses the run with nothing taken.
|
|
188
189
|
let tools: readonly PreludeTool[];
|
|
@@ -361,6 +362,30 @@ export class AgentScripts {
|
|
|
361
362
|
return [...this.scripts.values()].filter((s) => s.agentName === agentName && !s.gone && !s.run.ended).length;
|
|
362
363
|
}
|
|
363
364
|
|
|
365
|
+
/**
|
|
366
|
+
* The marks this agent's running scripts started with (their
|
|
367
|
+
* `startReadThrough`), which each one's sends are credited with having
|
|
368
|
+
* read: a redo on the agent's window gives them again, as it gives the
|
|
369
|
+
* agent's own (Agent.heldReadMarks), over the rows it puts back.
|
|
370
|
+
*/
|
|
371
|
+
readMarksOf(agentName: string): ReadMark[] {
|
|
372
|
+
const marks: ReadMark[] = [];
|
|
373
|
+
for (const s of this.scripts.values()) {
|
|
374
|
+
const mark = s.context.startReadThrough;
|
|
375
|
+
if (mark && s.agentName === agentName && !s.gone && !s.run.ended) marks.push(mark);
|
|
376
|
+
}
|
|
377
|
+
return marks;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Put each mark `replacements` maps in place of the start mark of this agent's scripts that hold it (readMarksOf), as Agent.replaceReadMarks does the agent's own: their later calls carry it. */
|
|
381
|
+
replaceReadMarks(agentName: string, replacements: ReadonlyMap<ReadMark, ReadMark>): void {
|
|
382
|
+
for (const s of this.scripts.values()) {
|
|
383
|
+
const mark = s.context.startReadThrough;
|
|
384
|
+
const next = mark && s.agentName === agentName ? replacements.get(mark) : undefined;
|
|
385
|
+
if (next) s.context.startReadThrough = next;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
364
389
|
/** Running scripts that no call is watching: those acting outside any turn. */
|
|
365
390
|
leftRunning(): number {
|
|
366
391
|
return [...this.scripts.values()].filter((s) => !s.run.ended && s.watch === null).length;
|
package/src/config-checks.ts
CHANGED
|
@@ -34,6 +34,20 @@ export function assertBooleanSetting(setting: string, value: unknown, whatItDoes
|
|
|
34
34
|
throw new Error(`${setting} must be ${wanted} not ${kindOf(value)}: ${whatItDoes}`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Refuse a setting that holds anything but one of `allowed`. null counts as
|
|
39
|
+
* not set, as leaving it out does. `whatEachDoes` ends the refusal: what each
|
|
40
|
+
* value does, and what leaving it out does. A string that isn't one of them is
|
|
41
|
+
* named as another string, never quoted.
|
|
42
|
+
*/
|
|
43
|
+
export function assertOneOfSetting(setting: string, value: unknown, allowed: readonly string[], whatEachDoes: string): void {
|
|
44
|
+
if (value === undefined || value === null || (typeof value === 'string' && allowed.includes(value))) return;
|
|
45
|
+
const choices = allowed.map((a) => `'${a}'`);
|
|
46
|
+
const wanted = choices.length === 1 ? choices[0] : `${choices.slice(0, -1).join(', ')} or ${choices.at(-1)}`;
|
|
47
|
+
const kind = typeof value === 'string' ? 'another string' : kindOf(value);
|
|
48
|
+
throw new Error(`${setting} must be ${wanted}, not ${kind}: ${whatEachDoes}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
52
|
* Whether a settings object holds settings to check: false when it's null,
|
|
39
53
|
* false or left out, which count as not set. Each of those has every setting
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-manager's write-outcome errors, read so that this package still
|
|
3
|
+
* loads on a context-manager that has neither.
|
|
4
|
+
*
|
|
5
|
+
* WriteLandedError and WriteOutcomeUnknownError are 0.25.0's: 0.24.0 has
|
|
6
|
+
* neither. This package is ESM, and a named import of a name its module
|
|
7
|
+
* doesn't export is a SyntaxError when the module graph is linked, so a
|
|
8
|
+
* named import of either would fail the package's load on 0.24.0 before
|
|
9
|
+
* create() could say which context-manager it needs (it refuses one
|
|
10
|
+
* without a window index, naming ^0.25.0). A namespace import reads an
|
|
11
|
+
* absent name as undefined, and a stand-in takes its place: a class
|
|
12
|
+
* nothing throws, so an `instanceof` against it is false, as it is for
|
|
13
|
+
* every error that release throws.
|
|
14
|
+
*
|
|
15
|
+
* Import these two from here, not from '@animalabs/context-manager'
|
|
16
|
+
* (test/presentation-mandatory.test.ts scans for that).
|
|
17
|
+
*/
|
|
18
|
+
import * as contextManager from '@animalabs/context-manager';
|
|
19
|
+
|
|
20
|
+
type Exports = typeof contextManager;
|
|
21
|
+
const exported: Partial<Pick<Exports, 'WriteLandedError' | 'WriteOutcomeUnknownError'>> = contextManager;
|
|
22
|
+
|
|
23
|
+
/** A stand-in for a class the installed context-manager lacks. */
|
|
24
|
+
function absent<C>(name: string): C {
|
|
25
|
+
return class extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(`${name} isn't in the installed @animalabs/context-manager`);
|
|
28
|
+
this.name = name;
|
|
29
|
+
}
|
|
30
|
+
} as unknown as C;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const WriteLandedError: Exports['WriteLandedError'] = exported.WriteLandedError ?? absent('WriteLandedError');
|
|
34
|
+
export type WriteLandedError = contextManager.WriteLandedError;
|
|
35
|
+
export const WriteOutcomeUnknownError: Exports['WriteOutcomeUnknownError'] =
|
|
36
|
+
exported.WriteOutcomeUnknownError ?? absent('WriteOutcomeUnknownError');
|
|
37
|
+
export type WriteOutcomeUnknownError = contextManager.WriteOutcomeUnknownError;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the framework opens a resident's context and the persistent
|
|
3
|
+
* subconscious's (#291, AF-5): which slot each reads, and what its view
|
|
4
|
+
* leaves out. The framework opens its live agents with these, and opens an
|
|
5
|
+
* ended one's context again with the same view (reopenAgentContext). A host
|
|
6
|
+
* that opens one of these contexts itself, to audit a store say, opens it
|
|
7
|
+
* with them too, so what it reads is what the agent's compiles read (the
|
|
8
|
+
* requests then send other agents' rounds as a transcript, `foreignRounds`,
|
|
9
|
+
* which `transcribeForeignRounds` applies to what the context reads):
|
|
10
|
+
* `ContextManager.open({ store, ...subconsciousContextOptions({ serves,
|
|
11
|
+
* name, reAnchorFraction, maxMessageTokens }) })`, with `serves` the
|
|
12
|
+
* primary's name, `name` and `reAnchorFraction` the host's
|
|
13
|
+
* `subconscious.name` and `subconscious.reAnchorFraction`, and
|
|
14
|
+
* `maxMessageTokens` the primary's `strategy.maxMessageTokens`, which the
|
|
15
|
+
* framework mirrors.
|
|
16
|
+
*
|
|
17
|
+
* - A resident reads the residents' shared slot (no `isolate`), its context
|
|
18
|
+
* state under `agents/<name>`. Its view leaves out what a tune-out holds
|
|
19
|
+
* (heldFromResidents), what a suppress rule hid (suppressedRow), and the
|
|
20
|
+
* other residents' stored context injections (isAnotherAgentsInjection):
|
|
21
|
+
* their context, which they share the slot with it but never its
|
|
22
|
+
* conversation.
|
|
23
|
+
* - The subconscious keeps its rows in a slot of its own,
|
|
24
|
+
* `subconscious/<serves>`, and reads the residents' slot beside it
|
|
25
|
+
* (`auxiliaryMessageViews: [{}]`), held traffic included, which is what it
|
|
26
|
+
* reads for. With the presentation index the residents' rows enter its
|
|
27
|
+
* window by its observe edge's rule, and the merged slot is still the
|
|
28
|
+
* record its window is presented from. Its view leaves out only the
|
|
29
|
+
* residents' stored context injections and what a suppress rule hid,
|
|
30
|
+
* which renders for no reader, this one included. Its strategy is a windowed
|
|
31
|
+
* passthrough, anchored at the oldest active tune-out's start, or at an
|
|
32
|
+
* earlier start kept in force: a cancelled tune-out's, kept while another
|
|
33
|
+
* stays active and across a restart, until the next enter sets the oldest
|
|
34
|
+
* active one's (window-anchor.ts).
|
|
35
|
+
*
|
|
36
|
+
* Each call returns a new object and, for the subconscious, a new strategy:
|
|
37
|
+
* a strategy keeps state for the manager it is opened with.
|
|
38
|
+
*/
|
|
39
|
+
import { WindowedPassthroughStrategy, type ContextManagerConfig, type StoredMessage } from '@animalabs/context-manager';
|
|
40
|
+
import { heldFromResidents, suppressedRow } from './dendrite/audience.js';
|
|
41
|
+
import { persistentReaderName } from './tune-out/tools.js';
|
|
42
|
+
import { isAnotherAgentsInjection } from './stored-injections.js';
|
|
43
|
+
|
|
44
|
+
/** ContextManager.open's options over a store the caller gives: everything but the store. */
|
|
45
|
+
export type ContextManagerOpenOptions = Omit<Extract<ContextManagerConfig, { store: unknown }>, 'store'>;
|
|
46
|
+
|
|
47
|
+
/** What a resident's view leaves out: held traffic, suppressed rows, and another agent's stored context injections. */
|
|
48
|
+
export function residentViewFilter(name: string): (message: StoredMessage) => boolean {
|
|
49
|
+
return (message) => !heldFromResidents(message.metadata as Record<string, unknown> | undefined)
|
|
50
|
+
&& !suppressedRow(message.metadata as Record<string, unknown> | undefined)
|
|
51
|
+
&& !isAnotherAgentsInjection(message.metadata, name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What the subconscious's view leaves out: the residents' stored context injections and suppressed rows, and nothing it reads for. */
|
|
55
|
+
export function subconsciousViewFilter(name: string): (message: StoredMessage) => boolean {
|
|
56
|
+
return (message) => !suppressedRow(message.metadata as Record<string, unknown> | undefined)
|
|
57
|
+
&& !isAnotherAgentsInjection(message.metadata, name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The options a resident named `agentName` is opened with: the residents' slot, through its view. The caller gives its strategy. */
|
|
61
|
+
export function residentContextOptions(agentName: string): ContextManagerOpenOptions {
|
|
62
|
+
return {
|
|
63
|
+
namespace: `agents/${agentName}`,
|
|
64
|
+
viewFilter: residentViewFilter(agentName),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The options the persistent subconscious serving `serves` is opened with:
|
|
70
|
+
* its own slot, the residents' merged beside it, its view, and a new
|
|
71
|
+
* windowed passthrough strategy with `reAnchorFraction` and
|
|
72
|
+
* `maxMessageTokens` (the framework mirrors the primary's). `name` is the
|
|
73
|
+
* name the subconscious runs under, its configuration's `name`, which the
|
|
74
|
+
* host gives it: pass that. Left out, it is the name the framework runs a
|
|
75
|
+
* reader the host didn't name under (persistentReaderName), so a host that
|
|
76
|
+
* left its reader unnamed opens the view the live one reads; this export
|
|
77
|
+
* doesn't choose a name of its own.
|
|
78
|
+
*/
|
|
79
|
+
export function subconsciousContextOptions(options: {
|
|
80
|
+
serves: string;
|
|
81
|
+
name?: string;
|
|
82
|
+
reAnchorFraction?: number;
|
|
83
|
+
maxMessageTokens?: number;
|
|
84
|
+
}): ContextManagerOpenOptions {
|
|
85
|
+
const name = persistentReaderName(options);
|
|
86
|
+
return {
|
|
87
|
+
namespace: `subconscious/${options.serves}`,
|
|
88
|
+
isolate: true,
|
|
89
|
+
auxiliaryMessageViews: [{}],
|
|
90
|
+
viewFilter: subconsciousViewFilter(name),
|
|
91
|
+
strategy: new WindowedPassthroughStrategy({
|
|
92
|
+
reAnchorFraction: options.reAnchorFraction,
|
|
93
|
+
maxMessageTokens: options.maxMessageTokens,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deferred-write queue an earlier release left on disk, read once at
|
|
3
|
+
* boot (#291, AF-6).
|
|
4
|
+
*
|
|
5
|
+
* Releases before #291's presentation index held a context write in a queue
|
|
6
|
+
* while its agent's turn was alive or the host was quiesced, and stored it
|
|
7
|
+
* at that agent's next boundary. While quiesced, and from a write that
|
|
8
|
+
* reached or may have reached its record until the queue drained, the queue
|
|
9
|
+
* was mirrored to `recovery/deferred-writes.json` beside the store, or, with
|
|
10
|
+
* no recovery path, to the store's `framework/deferred-writes` slot. This
|
|
11
|
+
* release stores every write at once and keeps no queue, so a record such a
|
|
12
|
+
* release left is read here, and the framework stores each of its writes
|
|
13
|
+
* once at boot (AgentFramework.importPersistedDeferredWrites), passes on or
|
|
14
|
+
* drops, logged, a write for an agent gone or one refused as malformed,
|
|
15
|
+
* keeps for the next boot one it can't store or check, with what follows it
|
|
16
|
+
* for that agent, and rewrites the record with only those. Like that
|
|
17
|
+
* importer, this module is for one release: the release
|
|
18
|
+
* after it deletes both. A host boots this release until its record is
|
|
19
|
+
* empty (the `deferred-import` alert says when writes stay queued), and
|
|
20
|
+
* doesn't upgrade past it from a release that kept a queue.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here touches a store. It reads the record's value, refuses one
|
|
23
|
+
* that isn't a queue, drops entries its readers can't handle, keeps one entry
|
|
24
|
+
* per id, and orders them as they arrived.
|
|
25
|
+
*/
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import type { ContentBlock } from '@animalabs/membrane';
|
|
28
|
+
import type { MessageMetadata } from '@animalabs/context-manager';
|
|
29
|
+
|
|
30
|
+
/** One write an earlier release queued, as its record held it. */
|
|
31
|
+
export interface PersistedDeferredWrite {
|
|
32
|
+
/** Its durable identity: a copy of it that landed carries it as `metadata.deferredWriteId`. */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Where it came in arrival order, 1..N over the entries kept. */
|
|
35
|
+
seq: number;
|
|
36
|
+
participant: string;
|
|
37
|
+
content: ContentBlock[];
|
|
38
|
+
metadata?: MessageMetadata;
|
|
39
|
+
/** The agent it was for. Absent means the primary. */
|
|
40
|
+
forAgent?: string;
|
|
41
|
+
/** When the earlier release first held it, in ms since the epoch, where the record says. */
|
|
42
|
+
heldAt?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Set where a write of it threw and its store couldn't be read to tell
|
|
45
|
+
* whether it landed: before it is stored, its target's whole branch is
|
|
46
|
+
* searched for its id.
|
|
47
|
+
*/
|
|
48
|
+
landing?: 'unknown';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What a queue's record held, as the importer takes it. */
|
|
52
|
+
export interface PersistedQueue {
|
|
53
|
+
/** Its writes, in arrival order. */
|
|
54
|
+
entries: PersistedDeferredWrite[];
|
|
55
|
+
/**
|
|
56
|
+
* Per target agent, where its store stood when the oldest write handed to
|
|
57
|
+
* it and not yet acknowledged was handed over: the boot dedup scans from
|
|
58
|
+
* there (0 for an agent not named, the whole store).
|
|
59
|
+
*/
|
|
60
|
+
scanFrom: Map<string, number>;
|
|
61
|
+
/**
|
|
62
|
+
* Whether reading it dropped, renumbered or named anything (a malformed
|
|
63
|
+
* entry, a repeated id, a missing or invalid seq, an entry given an id),
|
|
64
|
+
* so a record cleared or written again doesn't name those again.
|
|
65
|
+
*/
|
|
66
|
+
changed: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The ids this read gave entries that had none. A read gives each a new
|
|
69
|
+
* one, so until the record holds it, a copy stored under it is one no
|
|
70
|
+
* later read finds by its id.
|
|
71
|
+
*/
|
|
72
|
+
given: ReadonlySet<string>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Where a queue's record is: the recovery file, or the store's slot. */
|
|
76
|
+
export interface DeferredWritesRecord {
|
|
77
|
+
/** The recovery file the value was read from; absent where it was the slot's. */
|
|
78
|
+
file?: string;
|
|
79
|
+
/** The store's slot for it (framework/deferred-writes). */
|
|
80
|
+
slotId: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A stored JSON value's kind, for a refusal that names what it found without its content. */
|
|
84
|
+
function jsonKind(value: unknown): string {
|
|
85
|
+
return value === null ? 'null'
|
|
86
|
+
: Array.isArray(value) ? 'an array'
|
|
87
|
+
: typeof value === 'object' ? 'an object'
|
|
88
|
+
: `a ${typeof value}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The refusal of a boot whose queue record can't be taken: it could not be
|
|
93
|
+
* read (`cause`), or it holds something other than a queue (`problem`
|
|
94
|
+
* alone). Either way the record is left as it is, and the error says what
|
|
95
|
+
* lets the framework start.
|
|
96
|
+
*/
|
|
97
|
+
export function deferredWritesRefusal(record: DeferredWritesRecord, problem: string, cause?: unknown): Error {
|
|
98
|
+
const { file, slotId } = record;
|
|
99
|
+
return new Error(
|
|
100
|
+
`the deferred context writes (${file ?? slotId}) ${problem}. The framework doesn't start without them: an earlier ` +
|
|
101
|
+
`version left them queued, and they are stored at boot, ahead of anything written once the modules have ` +
|
|
102
|
+
`started. ` +
|
|
103
|
+
(file
|
|
104
|
+
? `The file is unchanged, and the framework starts once ${cause === undefined
|
|
105
|
+
? 'it holds a queue of writes, such as {"version":1,"pending":[]} for none. Removing it reads the queue ' +
|
|
106
|
+
`from ${slotId} instead, which the file's writes don't keep, so it imports whatever older ` +
|
|
107
|
+
`queue is there, or none`
|
|
108
|
+
: 'it can be read'}.`
|
|
109
|
+
: cause === undefined
|
|
110
|
+
? `They're unchanged, and starting again won't help until they're repaired.`
|
|
111
|
+
: `They're unchanged, and the framework starts once they can be read.`),
|
|
112
|
+
cause === undefined ? undefined : { cause },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read a queue record's value: the recovery file's document, or the slot's
|
|
118
|
+
* value where there is no file (`record.file` absent).
|
|
119
|
+
*
|
|
120
|
+
* - Nothing stored is an empty queue: no file and nothing in the slot. A file
|
|
121
|
+
* that holds null isn't a queue, since no release writes one.
|
|
122
|
+
* - The forms releases wrote are read: a bare array (the slot's first form),
|
|
123
|
+
* and `{ pending, scanFrom? }` (version 1 and 2 files, version 2 slots).
|
|
124
|
+
* Anything else is refused (deferredWritesRefusal).
|
|
125
|
+
* - An entry the importer can't store is dropped with a line naming it: one
|
|
126
|
+
* whose participant isn't text, whose content isn't an array, that holds a
|
|
127
|
+
* block that isn't an object, or whose `forAgent` is present but not a
|
|
128
|
+
* name. A release wrote `forAgent` only as a non-empty name, so another
|
|
129
|
+
* value is damage or a hand edit, and the agent it meant can't be known.
|
|
130
|
+
* - One entry per id: an id that comes again is dropped with a line, the
|
|
131
|
+
* first entry with it kept. An entry with no id is given one, with a line
|
|
132
|
+
* (`given`, and the queue reads as `changed`): a copy of it that landed
|
|
133
|
+
* carries the id it lost, so the boot dedup can't find it, and it may be
|
|
134
|
+
* stored again. Each read gives it a new id, so the importer writes the
|
|
135
|
+
* record with the ids given before it stores any of them.
|
|
136
|
+
* - `heldAt` is kept where it is a time a Date can hold.
|
|
137
|
+
* - Arrival order: entries whose seq is a positive whole number up to
|
|
138
|
+
* Number.MAX_SAFE_INTEGER by seq, then, in record order, those without one
|
|
139
|
+
* (a record from before seq, in write order already) or with another value,
|
|
140
|
+
* which is named. They are numbered 1..N in that order.
|
|
141
|
+
*/
|
|
142
|
+
export function readPersistedDeferredWrites(
|
|
143
|
+
raw: unknown,
|
|
144
|
+
record: DeferredWritesRecord,
|
|
145
|
+
log: (line: string) => void,
|
|
146
|
+
): PersistedQueue {
|
|
147
|
+
const empty: PersistedQueue = { entries: [], scanFrom: new Map(), changed: false, given: new Set() };
|
|
148
|
+
if (!record.file && (raw === null || raw === undefined)) return empty;
|
|
149
|
+
const doc = (Array.isArray(raw) ? { pending: raw } : raw !== null && typeof raw === 'object' ? raw : undefined) as
|
|
150
|
+
{ pending?: unknown; scanFrom?: unknown } | undefined;
|
|
151
|
+
if (!doc || !Array.isArray(doc.pending)) {
|
|
152
|
+
throw deferredWritesRefusal(record, doc
|
|
153
|
+
? `hold an object whose pending is ${doc.pending === undefined ? 'missing' : jsonKind(doc.pending)}, not a list of writes`
|
|
154
|
+
: `hold ${jsonKind(raw)}, not a queue of writes`);
|
|
155
|
+
}
|
|
156
|
+
const data = doc.pending as unknown[];
|
|
157
|
+
if (data.length === 0) return empty;
|
|
158
|
+
const scanFrom = new Map<string, number>();
|
|
159
|
+
if (doc.scanFrom && typeof doc.scanFrom === 'object') {
|
|
160
|
+
for (const [k, v] of Object.entries(doc.scanFrom as Record<string, unknown>)) {
|
|
161
|
+
if (typeof v === 'number' && Number.isFinite(v)) scanFrom.set(k, Math.max(0, Math.floor(v)));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
type Read = { participant: string; content: ContentBlock[]; metadata?: MessageMetadata; forAgent?: string; id?: unknown; seq?: unknown; heldAt?: unknown; landing?: unknown };
|
|
165
|
+
const wellFormed = (m: unknown): m is Read => {
|
|
166
|
+
if (!m || typeof m !== 'object') return false;
|
|
167
|
+
const { participant, content, forAgent } = m as { participant?: unknown; content?: unknown; forAgent?: unknown };
|
|
168
|
+
return typeof participant === 'string'
|
|
169
|
+
&& Array.isArray(content) && content.every((b) => !!b && typeof b === 'object')
|
|
170
|
+
&& (forAgent === undefined || (typeof forAgent === 'string' && forAgent !== ''));
|
|
171
|
+
};
|
|
172
|
+
const malformed = data.filter((m) => !wellFormed(m));
|
|
173
|
+
for (const m of malformed) {
|
|
174
|
+
// What the entry holds there: a value that isn't text by its type.
|
|
175
|
+
const field = (key: string): string => {
|
|
176
|
+
const value = m && typeof m === 'object' ? (m as Record<string, unknown>)[key] : undefined;
|
|
177
|
+
if (typeof value === 'string') return value;
|
|
178
|
+
if (value === undefined) return `no ${key}`;
|
|
179
|
+
return `${key} ${value === null ? 'null' : `of type ${typeof value}`}`;
|
|
180
|
+
};
|
|
181
|
+
log(`[host-mode] a recovered deferred context write is malformed and was dropped: ${field('id')} (${field('participant')})`);
|
|
182
|
+
}
|
|
183
|
+
const seen = new Set<string>();
|
|
184
|
+
const given = new Set<string>();
|
|
185
|
+
const kept: Array<Omit<PersistedDeferredWrite, 'seq'> & { seq?: unknown }> = [];
|
|
186
|
+
let duplicates = 0;
|
|
187
|
+
for (const m of data.filter(wellFormed)) {
|
|
188
|
+
const hasId = typeof m.id === 'string' && m.id !== '';
|
|
189
|
+
const id = hasId ? (m.id as string) : randomUUID();
|
|
190
|
+
if (!hasId) {
|
|
191
|
+
given.add(id);
|
|
192
|
+
log(`[host-mode] a recovered deferred context write (${m.participant}) has no id; it is given ${id}`);
|
|
193
|
+
}
|
|
194
|
+
if (seen.has(id)) {
|
|
195
|
+
duplicates++;
|
|
196
|
+
log(`[host-mode] a recovered deferred context write repeats id ${id} (${m.participant}) and was dropped; the first entry with that id is kept`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
seen.add(id);
|
|
200
|
+
kept.push({
|
|
201
|
+
id,
|
|
202
|
+
seq: m.seq,
|
|
203
|
+
participant: m.participant,
|
|
204
|
+
content: m.content,
|
|
205
|
+
...(m.metadata !== undefined ? { metadata: m.metadata } : {}),
|
|
206
|
+
...(m.forAgent !== undefined ? { forAgent: m.forAgent } : {}),
|
|
207
|
+
// A Date holds times within 8.64e15 ms of the epoch.
|
|
208
|
+
...(typeof m.heldAt === 'number' && Math.abs(m.heldAt) <= 8.64e15 ? { heldAt: m.heldAt } : {}),
|
|
209
|
+
...(m.landing === 'unknown' ? { landing: 'unknown' as const } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const validSeq = (seq: unknown): seq is number => Number.isSafeInteger(seq) && (seq as number) > 0;
|
|
213
|
+
const unnumbered = kept.filter((m) => !validSeq(m.seq));
|
|
214
|
+
for (const m of unnumbered) {
|
|
215
|
+
if (m.seq === undefined) continue;
|
|
216
|
+
// A value that isn't a number is named by its type: it could be any size.
|
|
217
|
+
const seq = typeof m.seq === 'number' ? String(m.seq) : m.seq === null ? 'null' : `of type ${typeof m.seq}`;
|
|
218
|
+
log(
|
|
219
|
+
`[host-mode] a recovered deferred context write (${m.id}) has seq ${seq}, not a positive ` +
|
|
220
|
+
`whole number up to Number.MAX_SAFE_INTEGER; it is queued after the others`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const ordered = [...kept.filter((m) => validSeq(m.seq)).sort((a, b) => (a.seq as number) - (b.seq as number)), ...unnumbered];
|
|
224
|
+
return {
|
|
225
|
+
entries: ordered.map((m, i) => ({ ...m, seq: i + 1 })),
|
|
226
|
+
scanFrom,
|
|
227
|
+
changed: malformed.length > 0 || duplicates > 0 || unnumbered.length > 0 || given.size > 0,
|
|
228
|
+
given,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Whether an entry holds a tool block (tool_use or tool_result): of what a
|
|
234
|
+
* release queued, only the puppet's pair, which was the agent's own round,
|
|
235
|
+
* queued behind a turn that had replaced the puppet's reservation.
|
|
236
|
+
*/
|
|
237
|
+
export function isToolBlockEntry(entry: Pick<PersistedDeferredWrite, 'content'>): boolean {
|
|
238
|
+
return entry.content.some((block) => block.type === 'tool_use' || block.type === 'tool_result');
|
|
239
|
+
}
|