@animalabs/agent-framework 0.21.1 → 0.22.1
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 +54 -4
- package/dist/src/agent.d.ts +17 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +105 -43
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +41 -4
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +22 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +64 -9
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +4 -0
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +215 -38
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/framework.d.ts +569 -59
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3091 -542
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +21 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +40 -7
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +26 -3
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +28 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +17 -0
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +31 -7
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +4 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +1 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +85 -22
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +32 -4
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +46 -13
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +5 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +41 -11
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +2 -1
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +20 -9
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +29 -6
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +57 -16
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +32 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +76 -11
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +5 -2
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +25 -7
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +47 -1
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
- package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
- package/dist/src/mcpl/tool-pattern-check.js +79 -0
- package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +15 -5
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +39 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -0
- package/dist/src/mcpl/visible-content.js +54 -0
- package/dist/src/mcpl/visible-content.js.map +1 -0
- package/dist/src/module-registry.d.ts +90 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +260 -18
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
- package/dist/src/modules/workspace/branch-intent.js +83 -0
- package/dist/src/modules/workspace/branch-intent.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.js +259 -0
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +84 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -0
- package/dist/src/modules/workspace/effects.js +368 -0
- package/dist/src/modules/workspace/effects.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +155 -17
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1101 -424
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +288 -0
- package/dist/src/modules/workspace/observe.d.ts.map +1 -0
- package/dist/src/modules/workspace/observe.js +825 -0
- package/dist/src/modules/workspace/observe.js.map +1 -0
- package/dist/src/modules/workspace/reconcile.d.ts +238 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
- package/dist/src/modules/workspace/reconcile.js +909 -0
- package/dist/src/modules/workspace/reconcile.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +6 -59
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +6 -257
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +77 -10
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/operator-log.d.ts +41 -0
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/provider-waits.d.ts +216 -0
- package/dist/src/provider-waits.d.ts.map +1 -0
- package/dist/src/provider-waits.js +482 -0
- package/dist/src/provider-waits.js.map +1 -0
- package/dist/src/record-journal.d.ts +127 -0
- package/dist/src/record-journal.d.ts.map +1 -0
- package/dist/src/record-journal.js +202 -0
- package/dist/src/record-journal.js.map +1 -0
- package/dist/src/refusal-reactions.d.ts +14 -12
- package/dist/src/refusal-reactions.d.ts.map +1 -1
- package/dist/src/refusal-reactions.js +14 -12
- package/dist/src/refusal-reactions.js.map +1 -1
- package/dist/src/shutdown.d.ts +13 -0
- package/dist/src/shutdown.d.ts.map +1 -0
- package/dist/src/shutdown.js +46 -0
- package/dist/src/shutdown.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +74 -0
- package/dist/src/silent-heartbeat.d.ts.map +1 -0
- package/dist/src/silent-heartbeat.js +77 -0
- package/dist/src/silent-heartbeat.js.map +1 -0
- package/dist/src/tool-presentation.d.ts +50 -0
- package/dist/src/tool-presentation.d.ts.map +1 -0
- package/dist/src/tool-presentation.js +235 -0
- package/dist/src/tool-presentation.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +142 -6
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +357 -36
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +13 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +21 -1
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +18 -2
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +16 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +12 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +26 -4
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +87 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/batched-wake-locus.test.d.ts +2 -0
- package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
- package/dist/test/batched-wake-locus.test.js +166 -0
- package/dist/test/batched-wake-locus.test.js.map +1 -0
- package/dist/test/code-execution.test.js +386 -2
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/component-defaults.test.d.ts +2 -0
- package/dist/test/component-defaults.test.d.ts.map +1 -0
- package/dist/test/component-defaults.test.js +54 -0
- package/dist/test/component-defaults.test.js.map +1 -0
- package/dist/test/conversation-routing.test.js +57 -1
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/discord-awareness-outbox.test.js +3 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -1
- package/dist/test/ephemeral-settle.test.js +3 -0
- package/dist/test/ephemeral-settle.test.js.map +1 -1
- package/dist/test/event-gate.test.js +118 -0
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +975 -0
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/framework.test.js +160 -1
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
- package/dist/test/helpers/coalescing-fixture.js +3 -2
- package/dist/test/helpers/coalescing-fixture.js.map +1 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
- package/dist/test/helpers/pytc-stop-rows.js +143 -0
- package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.js +142 -14
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
- package/dist/test/inference-failure-bounds.test.d.ts +2 -0
- package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
- package/dist/test/inference-failure-bounds.test.js +1201 -0
- package/dist/test/inference-failure-bounds.test.js.map +1 -0
- package/dist/test/late-events-after-stop.test.d.ts +2 -0
- package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
- package/dist/test/late-events-after-stop.test.js +122 -0
- package/dist/test/late-events-after-stop.test.js.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +141 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-coalescing.test.js +97 -22
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
- package/dist/test/mcpl-config-diagnostics.test.js +452 -0
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
- package/dist/test/mcpl-empty-content.test.d.ts +2 -0
- package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
- package/dist/test/mcpl-empty-content.test.js +368 -0
- package/dist/test/mcpl-empty-content.test.js.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.js +2 -1
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
- package/dist/test/mcpl-reference-fetcher.test.js +232 -4
- package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.js +69 -2
- package/dist/test/mcpl-reference-fields.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +103 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -1
- package/dist/test/ops-alert.test.js +2 -1
- package/dist/test/ops-alert.test.js.map +1 -1
- package/dist/test/placed-reaction-baseline.test.d.ts +2 -0
- package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/placed-reaction-baseline.test.js +312 -0
- package/dist/test/placed-reaction-baseline.test.js.map +1 -0
- package/dist/test/prose-stream-router.test.js +41 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/provider-acceleration-cooldown.test.js +5 -1
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
- package/dist/test/provider-waits.test.d.ts +2 -0
- package/dist/test/provider-waits.test.d.ts.map +1 -0
- package/dist/test/provider-waits.test.js +2196 -0
- package/dist/test/provider-waits.test.js.map +1 -0
- package/dist/test/reaction-baseline-platform.test.d.ts +2 -0
- package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
- package/dist/test/reaction-baseline-platform.test.js +124 -0
- package/dist/test/reaction-baseline-platform.test.js.map +1 -0
- package/dist/test/record-journal.test.d.ts +6 -0
- package/dist/test/record-journal.test.d.ts.map +1 -0
- package/dist/test/record-journal.test.js +269 -0
- package/dist/test/record-journal.test.js.map +1 -0
- package/dist/test/refusal-reaction-baseline.test.js +10 -3
- package/dist/test/refusal-reaction-baseline.test.js.map +1 -1
- package/dist/test/save-recent-image.test.js +240 -2
- package/dist/test/save-recent-image.test.js.map +1 -1
- package/dist/test/shutdown-errors.test.d.ts +2 -0
- package/dist/test/shutdown-errors.test.d.ts.map +1 -0
- package/dist/test/shutdown-errors.test.js +662 -0
- package/dist/test/shutdown-errors.test.js.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.js +451 -0
- package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
- package/dist/test/silent-heartbeat.test.js +49 -38
- package/dist/test/silent-heartbeat.test.js.map +1 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
- package/dist/test/stop-with-work-in-flight.test.js +672 -0
- package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
- package/dist/test/surgery-marks-receipt.test.d.ts +2 -0
- package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
- package/dist/test/surgery-marks-receipt.test.js +540 -0
- package/dist/test/surgery-marks-receipt.test.js.map +1 -0
- package/dist/test/tool-batch-hook.test.d.ts +8 -0
- package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
- package/dist/test/tool-batch-hook.test.js +275 -0
- package/dist/test/tool-batch-hook.test.js.map +1 -0
- package/dist/test/tool-presentation.test.d.ts +2 -0
- package/dist/test/tool-presentation.test.d.ts.map +1 -0
- package/dist/test/tool-presentation.test.js +268 -0
- package/dist/test/tool-presentation.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.js +2545 -19
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/tool-result-history.test.js +2 -1
- package/dist/test/tool-result-history.test.js.map +1 -1
- package/dist/test/turn-trigger-provenance.test.js +35 -0
- package/dist/test/turn-trigger-provenance.test.js.map +1 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
- package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
- package/dist/test/unconvertible-tool-result.test.js +358 -0
- package/dist/test/unconvertible-tool-result.test.js.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.js +252 -0
- package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
- package/dist/test/workspace-three-way.test.d.ts +12 -0
- package/dist/test/workspace-three-way.test.d.ts.map +1 -0
- package/dist/test/workspace-three-way.test.js +3194 -0
- package/dist/test/workspace-three-way.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +124 -45
- package/src/api/server.ts +43 -4
- package/src/code-execution/py-runner.ts +66 -9
- package/src/code-execution/runtime-py.ts +215 -38
- package/src/framework.ts +3191 -544
- package/src/gate/event-gate.ts +59 -7
- package/src/index.ts +18 -2
- package/src/mcpl/channel-registry.ts +24 -3
- package/src/mcpl/conversation-router.ts +34 -1
- package/src/mcpl/feature-set-manager.ts +36 -11
- package/src/mcpl/index.ts +4 -0
- package/src/mcpl/prose-stream-router.ts +79 -23
- package/src/mcpl/push-coalescer.ts +62 -13
- package/src/mcpl/push-handler.ts +41 -12
- package/src/mcpl/reference-fetcher.ts +18 -8
- package/src/mcpl/references.ts +57 -13
- package/src/mcpl/server-connection.ts +95 -13
- package/src/mcpl/server-registry.ts +24 -7
- package/src/mcpl/tool-lifecycle.ts +77 -2
- package/src/mcpl/tool-pattern-check.ts +106 -0
- package/src/mcpl/types.ts +15 -5
- package/src/mcpl/visible-content.ts +57 -0
- package/src/module-registry.ts +269 -25
- package/src/modules/workspace/branch-intent.ts +123 -0
- package/src/modules/workspace/disk-agreement.ts +325 -0
- package/src/modules/workspace/effects.ts +362 -0
- package/src/modules/workspace/index.ts +1098 -446
- package/src/modules/workspace/observe.ts +865 -0
- package/src/modules/workspace/reconcile.ts +1157 -0
- package/src/modules/workspace/sync.ts +6 -336
- package/src/modules/workspace/types.ts +75 -10
- package/src/operator-log.ts +30 -0
- package/src/provider-waits.ts +523 -0
- package/src/record-journal.ts +246 -0
- package/src/refusal-reactions.ts +14 -12
- package/src/shutdown.ts +49 -0
- package/src/silent-heartbeat.ts +141 -0
- package/src/tool-presentation.ts +188 -0
- package/src/tool-result-guard.ts +384 -34
- package/src/tool-result-history.ts +25 -1
- package/src/types/agent.ts +18 -2
- package/src/types/events.ts +16 -0
- package/src/types/framework.ts +12 -0
- package/src/types/module.ts +27 -4
- package/src/types/trace.ts +94 -3
package/src/module-registry.ts
CHANGED
|
@@ -24,6 +24,13 @@ import type {
|
|
|
24
24
|
TraceEventListener,
|
|
25
25
|
} from './types/index.js';
|
|
26
26
|
import type { Agent } from './agent.js';
|
|
27
|
+
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
|
|
28
|
+
|
|
29
|
+
/** How the registry queues a module's event: its push names the module. A
|
|
30
|
+
* `ProcessQueue` serves too, ignoring the name. */
|
|
31
|
+
export interface ModuleQueue {
|
|
32
|
+
push(event: ProcessEvent, moduleName?: string): void;
|
|
33
|
+
}
|
|
27
34
|
|
|
28
35
|
const MODULE_STATE_PREFIX = 'modules/';
|
|
29
36
|
|
|
@@ -46,6 +53,15 @@ export function isStateExistsError(err: unknown): boolean {
|
|
|
46
53
|
*/
|
|
47
54
|
const MAX_CONTEXT_TIMEOUT_MS = 30_000;
|
|
48
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Bound on each module's onToolBatchComplete. The hook runs inside the
|
|
58
|
+
* framework's sequential event loop, so a wedged module would stall every
|
|
59
|
+
* agent's events, not just its own round; past this the round goes on without
|
|
60
|
+
* it. A module with its own semantic deadline (the workspace scan's) sets it
|
|
61
|
+
* below this one, so it can record the miss itself.
|
|
62
|
+
*/
|
|
63
|
+
export const TOOL_BATCH_HOOK_TIMEOUT_MS = 30_000;
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* Registered speech handler.
|
|
51
67
|
*/
|
|
@@ -60,11 +76,15 @@ interface SpeechHandler {
|
|
|
60
76
|
* Registry for managing modules.
|
|
61
77
|
*/
|
|
62
78
|
export class ModuleRegistry {
|
|
79
|
+
/** Each module's tool-batch hook still running, past its bound or not. */
|
|
80
|
+
private batchHooksRunning = new Map<string, Promise<unknown>>();
|
|
81
|
+
|
|
63
82
|
private modules: Map<string, Module> = new Map();
|
|
83
|
+
private stopping = new Map<Module, Promise<void>>();
|
|
64
84
|
private moduleContexts: Map<string, ModuleContextImpl> = new Map();
|
|
65
85
|
private speechHandlers: SpeechHandler[] = [];
|
|
66
86
|
private store: JsStore;
|
|
67
|
-
private queue:
|
|
87
|
+
private queue: ModuleQueue;
|
|
68
88
|
private getAgents: () => Agent[];
|
|
69
89
|
private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
|
|
70
90
|
private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
|
|
@@ -72,7 +92,7 @@ export class ModuleRegistry {
|
|
|
72
92
|
private getMessageFn: (id: MessageId) => StoredMessage | null;
|
|
73
93
|
private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
|
|
74
94
|
|
|
75
|
-
private pushEventFn: (event: ProcessEvent) => void;
|
|
95
|
+
private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
|
|
76
96
|
private onTraceFn: (listener: TraceEventListener) => () => void;
|
|
77
97
|
private callToolFn: (call: ToolCall) => Promise<ToolResult>;
|
|
78
98
|
/** Framework ops-alert channel, surfaced to modules via
|
|
@@ -87,7 +107,7 @@ export class ModuleRegistry {
|
|
|
87
107
|
|
|
88
108
|
constructor(
|
|
89
109
|
store: JsStore,
|
|
90
|
-
queue:
|
|
110
|
+
queue: ModuleQueue,
|
|
91
111
|
options: {
|
|
92
112
|
getAgents: () => Agent[];
|
|
93
113
|
addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
|
|
@@ -95,7 +115,8 @@ export class ModuleRegistry {
|
|
|
95
115
|
removeMessage: (id: MessageId) => void;
|
|
96
116
|
getMessage: (id: MessageId) => StoredMessage | null;
|
|
97
117
|
queryMessages: (filter: MessageQuery) => MessageQueryResult;
|
|
98
|
-
|
|
118
|
+
/** Called with the pushing module's name. */
|
|
119
|
+
pushEvent: (event: ProcessEvent, moduleName?: string) => void;
|
|
99
120
|
onTrace: (listener: TraceEventListener) => () => void;
|
|
100
121
|
callTool: (call: ToolCall) => Promise<ToolResult>;
|
|
101
122
|
notifyOps?: (kind: string, agentName: string, message: string, data?: Record<string, unknown>) => void;
|
|
@@ -173,9 +194,7 @@ export class ModuleRegistry {
|
|
|
173
194
|
throw new Error(`Module not found: ${name}`);
|
|
174
195
|
}
|
|
175
196
|
|
|
176
|
-
await
|
|
177
|
-
this.modules.delete(name);
|
|
178
|
-
this.moduleContexts.delete(name);
|
|
197
|
+
await this.stopModule(module);
|
|
179
198
|
}
|
|
180
199
|
|
|
181
200
|
/**
|
|
@@ -261,7 +280,20 @@ export class ModuleRegistry {
|
|
|
261
280
|
}
|
|
262
281
|
|
|
263
282
|
/**
|
|
264
|
-
*
|
|
283
|
+
* A module tool's call for a path that stores or sends its result: the
|
|
284
|
+
* model's dispatch, a script's inner call, the `utils` meta-tool for a
|
|
285
|
+
* model. The result is checked and read once, in its JSON form
|
|
286
|
+
* ({@link storableToolResult}).
|
|
287
|
+
*/
|
|
288
|
+
async storableToolCall(call: ToolCall): Promise<ToolResult> {
|
|
289
|
+
return storableToolResult(call, await this.handleToolCall(call));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle a tool call by routing to the appropriate module: the module's own
|
|
294
|
+
* result, unchecked, as it returned it, for a caller that uses the result
|
|
295
|
+
* in process (`ModuleContext.callTool`, `executeToolCall`, or a host
|
|
296
|
+
* calling the registry). A module's throw is answered as an error result.
|
|
265
297
|
*/
|
|
266
298
|
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
267
299
|
// Parse module--tool name
|
|
@@ -297,23 +329,37 @@ export class ModuleRegistry {
|
|
|
297
329
|
try {
|
|
298
330
|
return await module.handleToolCall(moduleCall);
|
|
299
331
|
} catch (error) {
|
|
300
|
-
return
|
|
301
|
-
success: false,
|
|
302
|
-
error: error instanceof Error ? error.message : String(error),
|
|
303
|
-
isError: true,
|
|
304
|
-
};
|
|
332
|
+
return thrownResult(error);
|
|
305
333
|
}
|
|
306
334
|
}
|
|
307
335
|
|
|
308
336
|
/**
|
|
309
|
-
* Stop all modules.
|
|
337
|
+
* Stop all modules. A caller may bound its wait without cancelling cleanup.
|
|
338
|
+
* The default waits for each module's own shutdown contract.
|
|
310
339
|
*/
|
|
311
|
-
async stopAll(): Promise<void> {
|
|
312
|
-
|
|
313
|
-
await
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
340
|
+
async stopAll(timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS): Promise<void> {
|
|
341
|
+
validateShutdownTimeout(timeoutMs);
|
|
342
|
+
await waitForShutdown(Array.from(this.modules.values(), module => ({
|
|
343
|
+
label: `module:${module.name}`, promise: this.stopModule(module),
|
|
344
|
+
})), timeoutMs);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private stopModule(module: Module): Promise<void> {
|
|
348
|
+
const existing = this.stopping.get(module);
|
|
349
|
+
if (existing) return existing;
|
|
350
|
+
const attempt = Promise.resolve().then(() => module.stop()).then(() => {
|
|
351
|
+
if (this.modules.get(module.name) === module) {
|
|
352
|
+
this.modules.delete(module.name);
|
|
353
|
+
this.moduleContexts.delete(module.name);
|
|
354
|
+
this.speechHandlers = this.speechHandlers.filter(handler => handler.moduleName !== module.name);
|
|
355
|
+
}
|
|
356
|
+
this.stopping.delete(module);
|
|
357
|
+
}, error => {
|
|
358
|
+
this.stopping.delete(module);
|
|
359
|
+
throw error;
|
|
360
|
+
});
|
|
361
|
+
this.stopping.set(module, attempt);
|
|
362
|
+
return attempt;
|
|
317
363
|
}
|
|
318
364
|
|
|
319
365
|
/**
|
|
@@ -397,6 +443,57 @@ export class ModuleRegistry {
|
|
|
397
443
|
return injections;
|
|
398
444
|
}
|
|
399
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Tell modules an agent's tool batch completed (see
|
|
448
|
+
* Module.onToolBatchComplete). Hooks run in parallel, each awaited up to
|
|
449
|
+
* `timeoutMs`; a hook that throws or times out is logged and skipped, so a
|
|
450
|
+
* wedged module can't hold the round. Returns the modules that didn't
|
|
451
|
+
* finish, for the caller's trace.
|
|
452
|
+
*/
|
|
453
|
+
async notifyToolBatchComplete(
|
|
454
|
+
agentName: string,
|
|
455
|
+
timeoutMs = TOOL_BATCH_HOOK_TIMEOUT_MS,
|
|
456
|
+
): Promise<Array<{ module: string; error: string }>> {
|
|
457
|
+
const failures: Array<{ module: string; error: string }> = [];
|
|
458
|
+
const promises: Promise<void>[] = [];
|
|
459
|
+
for (const module of this.modules.values()) {
|
|
460
|
+
if (!module.onToolBatchComplete) continue;
|
|
461
|
+
// A hook still running from an earlier batch (it outlasted its bound)
|
|
462
|
+
// isn't called again or waited for: a wedged module costs one bound,
|
|
463
|
+
// not one per batch.
|
|
464
|
+
if (this.batchHooksRunning.has(module.name)) {
|
|
465
|
+
failures.push({ module: module.name, error: 'still running from an earlier batch' });
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
// Called from a promise, so a hook that throws synchronously, before
|
|
469
|
+
// returning its promise, is caught below like any other failure
|
|
470
|
+
// instead of escaping the round (the result would never be provided).
|
|
471
|
+
const call = Promise.resolve().then(() => module.onToolBatchComplete!(agentName));
|
|
472
|
+
this.batchHooksRunning.set(module.name, call);
|
|
473
|
+
const settled = (): void => {
|
|
474
|
+
if (this.batchHooksRunning.get(module.name) === call) this.batchHooksRunning.delete(module.name);
|
|
475
|
+
};
|
|
476
|
+
call.then(settled, settled);
|
|
477
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
478
|
+
promises.push(
|
|
479
|
+
Promise.race([
|
|
480
|
+
call,
|
|
481
|
+
new Promise<void>((_, rej) => {
|
|
482
|
+
timer = setTimeout(() => rej(new Error(`onToolBatchComplete timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
483
|
+
}),
|
|
484
|
+
])
|
|
485
|
+
.catch((err: unknown) => {
|
|
486
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
487
|
+
failures.push({ module: module.name, error });
|
|
488
|
+
console.error(`[${module.name}] onToolBatchComplete:`, err);
|
|
489
|
+
})
|
|
490
|
+
.finally(() => clearTimeout(timer)),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
await Promise.all(promises);
|
|
494
|
+
return failures;
|
|
495
|
+
}
|
|
496
|
+
|
|
400
497
|
/**
|
|
401
498
|
* Dispatch speech to registered handlers.
|
|
402
499
|
*/
|
|
@@ -447,7 +544,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
447
544
|
private removeMessageFn: (id: MessageId) => void;
|
|
448
545
|
private getMessageFn: (id: MessageId) => StoredMessage | null;
|
|
449
546
|
private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
|
|
450
|
-
private pushEventFn: (event: ProcessEvent) => void;
|
|
547
|
+
private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
|
|
451
548
|
private onTraceFn: (listener: TraceEventListener) => () => void;
|
|
452
549
|
private callToolFn: (call: ToolCall) => Promise<ToolResult>;
|
|
453
550
|
|
|
@@ -458,7 +555,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
458
555
|
moduleName: string,
|
|
459
556
|
store: JsStore,
|
|
460
557
|
stateId: string,
|
|
461
|
-
queue:
|
|
558
|
+
queue: ModuleQueue,
|
|
462
559
|
registry: ModuleRegistry,
|
|
463
560
|
isRestart: boolean,
|
|
464
561
|
getAgents: () => Agent[],
|
|
@@ -467,14 +564,15 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
467
564
|
removeMessage: (id: MessageId) => void,
|
|
468
565
|
getMessage: (id: MessageId) => StoredMessage | null,
|
|
469
566
|
queryMessages: (filter: MessageQuery) => MessageQueryResult,
|
|
470
|
-
pushEvent: (event: ProcessEvent) => void,
|
|
567
|
+
pushEvent: (event: ProcessEvent, moduleName?: string) => void,
|
|
471
568
|
onTrace: (listener: TraceEventListener) => () => void,
|
|
472
569
|
callTool: (call: ToolCall) => Promise<ToolResult>
|
|
473
570
|
) {
|
|
474
571
|
this.moduleName = moduleName;
|
|
475
572
|
this.store = store;
|
|
476
573
|
this.stateId = stateId;
|
|
477
|
-
this
|
|
574
|
+
// The deprecated handle: a push names this module, as pushEvent() does.
|
|
575
|
+
this.queue = { push: (event) => queue.push(event, moduleName) };
|
|
478
576
|
this.registry = registry;
|
|
479
577
|
this.isRestart = isRestart;
|
|
480
578
|
this.getAgentsFn = getAgents;
|
|
@@ -657,7 +755,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
657
755
|
}
|
|
658
756
|
|
|
659
757
|
pushEvent(event: ProcessEvent): void {
|
|
660
|
-
this.pushEventFn(event);
|
|
758
|
+
this.pushEventFn(event, this.moduleName);
|
|
661
759
|
}
|
|
662
760
|
|
|
663
761
|
private persistExternalIdMap(): void {
|
|
@@ -711,3 +809,149 @@ class ProcessStateImpl implements ProcessState {
|
|
|
711
809
|
this.ctx.pushEvent(event);
|
|
712
810
|
}
|
|
713
811
|
}
|
|
812
|
+
|
|
813
|
+
/** A result's flags, which consumers read for their truth: booleans here. */
|
|
814
|
+
const RESULT_FLAGS = ['success', 'isError', 'endTurn'] as const;
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* A module's tool result as the framework can store, send and trace it. A
|
|
818
|
+
* module runs in the host's process, so it can return what JSON can't hold,
|
|
819
|
+
* such as a cyclic object, a BigInt or a `toJSON` that throws, no result
|
|
820
|
+
* object at all, or one whose getters or Proxy traps throw, or answer
|
|
821
|
+
* differently each time they're read. Everything after this (the stored
|
|
822
|
+
* history, the request sent to the model, traces, the tool-result guard's
|
|
823
|
+
* audit record, a script's result) would throw on it, and the agent's turn
|
|
824
|
+
* would never settle.
|
|
825
|
+
*
|
|
826
|
+
* So the result is read once, here, and this never throws. Each field a
|
|
827
|
+
* consumer reads is read by name, so a result class's getters count, and any
|
|
828
|
+
* other own field is kept, except a `toJSON`: consumers read a result's
|
|
829
|
+
* fields, never a `toJSON` of the result. What's returned is the JSON form of
|
|
830
|
+
* those fields, parsed, so every consumer holds plain data and no module code
|
|
831
|
+
* runs again. It's taken where the framework stores or sends a result
|
|
832
|
+
* ({@link ModuleRegistry.storableToolCall}). A caller that uses the result
|
|
833
|
+
* in process (`ModuleContext.callTool`, `executeToolCall`) gets the module's
|
|
834
|
+
* own result, as the module returned it ({@link ModuleRegistry.handleToolCall}).
|
|
835
|
+
* The flags are booleans, read for their truth as consumers read them.
|
|
836
|
+
* `error` is its text form, which is all a consumer reads of it, so an error
|
|
837
|
+
* value JSON can't hold costs nothing. An error that says nothing (null,
|
|
838
|
+
* false or empty) is left out, so the result reads as having no error text.
|
|
839
|
+
*
|
|
840
|
+
* A result that can't be read or converted becomes an error result naming the
|
|
841
|
+
* tool, keeping the module's own error text and its `endTurn` when they could
|
|
842
|
+
* be read. So does data that is a function or a symbol, which JSON leaves out
|
|
843
|
+
* whole.
|
|
844
|
+
*/
|
|
845
|
+
export function storableToolResult(call: ToolCall, result: unknown): ToolResult {
|
|
846
|
+
return checkedToolResult(call, result).result;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* {@link storableToolResult}, saying whether the module's result was usable:
|
|
851
|
+
* read and converted (`usable`), or replaced by the error result naming the
|
|
852
|
+
* tool. A caller that stores the result and hands the module's own result
|
|
853
|
+
* back, as `puppetToolCall` does, hands back the error result instead when
|
|
854
|
+
* there was nothing usable to store.
|
|
855
|
+
*/
|
|
856
|
+
export function checkedToolResult(call: ToolCall, result: unknown): { result: ToolResult; usable: boolean } {
|
|
857
|
+
// What's been read so far, kept by an error result if a later read throws.
|
|
858
|
+
let ownText: string | undefined;
|
|
859
|
+
let endTurn = false;
|
|
860
|
+
const unusable = (problem: string): { result: ToolResult; usable: boolean } => {
|
|
861
|
+
const own = ownText ? `, with this error: ${ownText}` : '.';
|
|
862
|
+
const error = `Tool '${call.name}' ${problem}. The call itself completed${own}`;
|
|
863
|
+
console.error(`[tools] ${call.callerAgentName ? `agent ${call.callerAgentName}: ` : ''}${error}`);
|
|
864
|
+
return { result: { success: false, isError: true, error, ...(endTurn ? { endTurn: true } : {}) }, usable: false };
|
|
865
|
+
};
|
|
866
|
+
const reasonOf = (error: unknown) => thrownText(error).split('\n')[0] || 'no reason given';
|
|
867
|
+
try {
|
|
868
|
+
if (!result || typeof result !== 'object' || Array.isArray(result) || result instanceof Error) {
|
|
869
|
+
const what = result === undefined || result === null ? String(result)
|
|
870
|
+
: Array.isArray(result) ? 'an array' : result instanceof Error ? 'an Error' : `a ${typeof result}`;
|
|
871
|
+
if (result instanceof Error) ownText = errorText(result);
|
|
872
|
+
return unusable(`returned ${what} instead of a result object, so there's nothing to store or send`);
|
|
873
|
+
}
|
|
874
|
+
const candidate = result as Record<string, unknown>;
|
|
875
|
+
// No prototype, so an own field named like an inherited one is a field.
|
|
876
|
+
const fields: Record<string, unknown> = Object.create(null);
|
|
877
|
+
const read = (key: string) => { if (key in candidate) fields[key] = candidate[key]; };
|
|
878
|
+
// endTurn first and the error's text next, so an error result keeps
|
|
879
|
+
// whichever of them could be read when a later read throws.
|
|
880
|
+
read('endTurn');
|
|
881
|
+
endTurn = Boolean(fields.endTurn);
|
|
882
|
+
read('error');
|
|
883
|
+
ownText = errorText(fields.error);
|
|
884
|
+
for (const key of ['success', 'isError', 'data']) read(key);
|
|
885
|
+
for (const key of Object.keys(candidate)) {
|
|
886
|
+
if (key !== 'toJSON' && !(key in fields)) fields[key] = candidate[key];
|
|
887
|
+
}
|
|
888
|
+
// The error is kept as its text, which consumers read; its value isn't.
|
|
889
|
+
delete fields.error;
|
|
890
|
+
// The flags are kept as their truth, which is all consumers read of them,
|
|
891
|
+
// before anything is serialized, so a flag's own value (a BigInt, a
|
|
892
|
+
// function, an object with a toJSON) neither breaks nor runs anything.
|
|
893
|
+
for (const flag of RESULT_FLAGS) {
|
|
894
|
+
if (fields[flag] !== undefined) fields[flag] = Boolean(fields[flag]);
|
|
895
|
+
}
|
|
896
|
+
// An error result reads as its error, so only a non-error result's data matters.
|
|
897
|
+
if (!fields.isError && (typeof fields.data === 'function' || typeof fields.data === 'symbol')) {
|
|
898
|
+
return unusable(`returned a ${typeof fields.data} as its data, which JSON leaves out, so there's nothing to store or send`);
|
|
899
|
+
}
|
|
900
|
+
let json: string;
|
|
901
|
+
try {
|
|
902
|
+
json = JSON.stringify(fields);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
return unusable(`returned a result that can't be converted to JSON, so it can't be stored or sent (${reasonOf(error)})`);
|
|
905
|
+
}
|
|
906
|
+
const storable = JSON.parse(json) as Record<string, unknown>;
|
|
907
|
+
if (ownText !== undefined) storable.error = ownText;
|
|
908
|
+
return { result: storable as unknown as ToolResult, usable: true };
|
|
909
|
+
} catch (error) {
|
|
910
|
+
return unusable(`returned a result that can't be read, so it can't be stored or sent (${reasonOf(error)})`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** A result's `error` as text, always a string or none: a string as it is,
|
|
915
|
+
* an `Error`'s message read the same way (a module's value, so not always a
|
|
916
|
+
* string), an object's own `message`, any other value's JSON, and a
|
|
917
|
+
* primitive JSON can't hold (a BigInt, a symbol) as its String(). None for a
|
|
918
|
+
* value that says nothing: absent, null, false or empty, which consumers
|
|
919
|
+
* then report as "Unknown error". */
|
|
920
|
+
function errorText(error: unknown): string | undefined {
|
|
921
|
+
if (error === undefined || error === null || error === false || error === '') return undefined;
|
|
922
|
+
if (typeof error === 'string') return error;
|
|
923
|
+
if (error instanceof Error) return errorText(error.message);
|
|
924
|
+
const message = (error as { message?: unknown }).message;
|
|
925
|
+
if (typeof message === 'string' && message) return message;
|
|
926
|
+
try {
|
|
927
|
+
const json = JSON.stringify(error) as string | undefined;
|
|
928
|
+
if (json !== undefined) return json;
|
|
929
|
+
} catch {
|
|
930
|
+
// A cyclic or BigInt-holding value: nothing faithful to show as JSON.
|
|
931
|
+
}
|
|
932
|
+
if (typeof error !== 'object' && typeof error !== 'function') return thrownText(error);
|
|
933
|
+
return 'an error value with no text form';
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** A module's throw, answered as an error result: its text, or none when it
|
|
937
|
+
* has none, so consumers say "Unknown error". */
|
|
938
|
+
export function thrownResult(error: unknown): ToolResult {
|
|
939
|
+
const text = thrownText(error);
|
|
940
|
+
return { success: false, isError: true, ...(text ? { error: text } : {}) };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** A thrown value's whole text, always a string, for a result's error, and
|
|
944
|
+
* for a value that has none too. An `Error`'s message is a module's value,
|
|
945
|
+
* so it may not be a string: one that isn't is given its String(), and an
|
|
946
|
+
* absent one is empty. String() throws on an object without a prototype,
|
|
947
|
+
* and such a value is named as having no text form. */
|
|
948
|
+
function thrownText(value: unknown): string {
|
|
949
|
+
try {
|
|
950
|
+
if (!(value instanceof Error)) return String(value);
|
|
951
|
+
const message: unknown = value.message;
|
|
952
|
+
if (typeof message === 'string') return message;
|
|
953
|
+
return message === undefined || message === null ? '' : String(message);
|
|
954
|
+
} catch {
|
|
955
|
+
return 'a value with no text form';
|
|
956
|
+
}
|
|
957
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branch-local intent about workspace paths: what the workspace on THIS branch
|
|
3
|
+
* deliberately did or still owes, kept in a per-mount Chronicle tree state so
|
|
4
|
+
* it rewinds with the workspace tree it qualifies (a branch switch that brings
|
|
5
|
+
* back a deleted file brings back its missing tombstone too).
|
|
6
|
+
*
|
|
7
|
+
* - tombstone: the workspace deleted the path while disk kept a copy (no
|
|
8
|
+
* autoMaterialize, or a refused unlink). The hash is the deleted entry's,
|
|
9
|
+
* so even a legacy entry with no disk evidence keeps a durable intent.
|
|
10
|
+
* - origin 'store': the entry was created through workspace tools and disk
|
|
11
|
+
* has never agreed with it.
|
|
12
|
+
* - conflict: disk and workspace diverged in a way no rule may resolve
|
|
13
|
+
* silently; kept until a path sync, a forced materialize, or convergence.
|
|
14
|
+
*
|
|
15
|
+
* Each record is a small JSON blob addressed from the tree, so reading one is
|
|
16
|
+
* treeGet + getBlob and writing one is storeBlob + treeSet.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { JsStore } from '@animalabs/chronicle';
|
|
20
|
+
|
|
21
|
+
export type ConflictKind =
|
|
22
|
+
/** Disk, workspace and the last agreement all differ. */
|
|
23
|
+
| 'both-changed'
|
|
24
|
+
/** Disk deleted a file whose workspace copy differs from the last agreement. */
|
|
25
|
+
| 'deleted-on-disk'
|
|
26
|
+
/** Disk changed a file the workspace deleted. */
|
|
27
|
+
| 'changed-after-workspace-delete'
|
|
28
|
+
/** Disk holds a file at the path of a workspace-created entry, with other content. */
|
|
29
|
+
| 'store-origin-collision'
|
|
30
|
+
/** A legacy entry whose disk copy differs, with no evidence of which side changed. */
|
|
31
|
+
| 'unknown-provenance'
|
|
32
|
+
/** A module write or adoption was interrupted, and disk holds neither outcome. */
|
|
33
|
+
| 'interrupted';
|
|
34
|
+
|
|
35
|
+
/** The disk side of a conflict, as observed when it was recorded. */
|
|
36
|
+
export interface DiskCounterpart {
|
|
37
|
+
hash: string;
|
|
38
|
+
size: number;
|
|
39
|
+
mtimeMs: number;
|
|
40
|
+
/** Blob hash of the stored copy (text within limits); absent when only referenced. */
|
|
41
|
+
stored?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ConflictRecord {
|
|
45
|
+
kind: ConflictKind;
|
|
46
|
+
/** When the conflict was recorded (ms). */
|
|
47
|
+
at: number;
|
|
48
|
+
/** null when disk had no file. */
|
|
49
|
+
disk: DiskCounterpart | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BranchIntent {
|
|
53
|
+
tombstone?: { hash: string };
|
|
54
|
+
origin?: 'store';
|
|
55
|
+
conflict?: ConflictRecord;
|
|
56
|
+
/**
|
|
57
|
+
* Why a recorded intent can't be read (its blob missing from the store, or
|
|
58
|
+
* not an intent): a tombstone or conflict it held is unknown, never absent.
|
|
59
|
+
* Never written; a put replaces the damaged record.
|
|
60
|
+
*/
|
|
61
|
+
unreadable?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isEmpty(intent: BranchIntent): boolean {
|
|
65
|
+
return intent.tombstone === undefined && intent.origin === undefined && intent.conflict === undefined &&
|
|
66
|
+
intent.unreadable === undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A recorded intent's blob, read; a damaged one says why it can't be. */
|
|
70
|
+
function readIntent(store: JsStore, blobHash: string): BranchIntent {
|
|
71
|
+
const blob = store.getBlob(blobHash);
|
|
72
|
+
if (!blob) return { unreadable: "its record's blob is missing from the store" };
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(blob.toString('utf-8')) as unknown;
|
|
75
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as BranchIntent;
|
|
76
|
+
} catch {
|
|
77
|
+
// reported below
|
|
78
|
+
}
|
|
79
|
+
return { unreadable: 'its record is not a branch intent' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class BranchIntents {
|
|
83
|
+
constructor(private readonly store: JsStore, readonly treeStateId: string) {}
|
|
84
|
+
|
|
85
|
+
get(path: string): BranchIntent | null {
|
|
86
|
+
const entry = this.store.treeGet(this.treeStateId, path);
|
|
87
|
+
if (!entry) return null;
|
|
88
|
+
return readIntent(this.store, entry.blobHash);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Replace a path's intent; an empty intent removes the record. */
|
|
92
|
+
put(path: string, intent: BranchIntent | null): void {
|
|
93
|
+
if (intent?.unreadable !== undefined) {
|
|
94
|
+
const { unreadable: _damaged, ...rest } = intent;
|
|
95
|
+
intent = rest;
|
|
96
|
+
}
|
|
97
|
+
if (!intent || isEmpty(intent)) {
|
|
98
|
+
if (this.store.treeGet(this.treeStateId, path)) this.store.treeRemove(this.treeStateId, path);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const bytes = Buffer.from(JSON.stringify(intent), 'utf-8');
|
|
102
|
+
const blobHash = this.store.storeBlob(bytes, 'application/json');
|
|
103
|
+
this.store.treeSet(this.treeStateId, path, { blobHash, size: bytes.byteLength, mode: 0 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Update one path's intent in place. */
|
|
107
|
+
update(path: string, change: (current: BranchIntent) => BranchIntent): void {
|
|
108
|
+
this.put(path, change({ ...(this.get(path) ?? {}) }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Every recorded intent under a prefix ('' for the whole mount). */
|
|
112
|
+
list(prefix = ''): Array<[string, BranchIntent]> {
|
|
113
|
+
const out: Array<[string, BranchIntent]> = [];
|
|
114
|
+
for (const entry of this.store.treeList(this.treeStateId, prefix ? prefix + '/' : undefined)) {
|
|
115
|
+
out.push([entry.path, readIntent(this.store, entry.blobHash)]);
|
|
116
|
+
}
|
|
117
|
+
if (prefix) {
|
|
118
|
+
const exact = this.get(prefix);
|
|
119
|
+
if (exact) out.push([prefix, exact]);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
}
|