@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/agent-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@animalabs/chronicle": "^0.4.0",
|
|
50
50
|
"@animalabs/context-manager": "^0.14.0",
|
|
51
|
-
"@animalabs/membrane": "^0.5.
|
|
51
|
+
"@animalabs/membrane": "^0.5.87",
|
|
52
52
|
"chokidar": "^4.0.3",
|
|
53
53
|
"discord.js": "^14.25.1",
|
|
54
54
|
"ws": "^8.18.0"
|
package/src/agent.ts
CHANGED
|
@@ -2,13 +2,26 @@ import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, Yiel
|
|
|
2
2
|
import { isAbortedResponse } from '@animalabs/membrane';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import type { CacheWireReceipt, KvUnifiedRequestHooks } from './kv-unified-wire.js';
|
|
5
|
-
import { ToolResultGuard
|
|
5
|
+
import { ToolResultGuard } from './tool-result-guard.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
indexSilentTickRows,
|
|
8
|
+
separateSilentHeartbeatTicks,
|
|
9
|
+
silentHeartbeatSeparatorTurn,
|
|
10
|
+
type SilentHeartbeatTick,
|
|
11
|
+
type SilentTickIndex,
|
|
12
|
+
type StoredRowLike,
|
|
13
|
+
} from './silent-heartbeat.js';
|
|
14
|
+
import {
|
|
15
|
+
nonErrorResultText,
|
|
8
16
|
truncateForHistory,
|
|
9
17
|
DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS,
|
|
10
18
|
} from './tool-result-history.js';
|
|
11
19
|
|
|
20
|
+
export interface ActivationRequestOptions {
|
|
21
|
+
/** The silent heartbeat tick this request answers, if any. */
|
|
22
|
+
silentHeartbeat?: SilentHeartbeatTick;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
export interface StartStreamResult {
|
|
13
26
|
stream: YieldingStream;
|
|
14
27
|
request: NormalizedRequest;
|
|
@@ -22,6 +35,7 @@ import type {
|
|
|
22
35
|
CompileResult,
|
|
23
36
|
HotContextSettingsStatus,
|
|
24
37
|
HotContextSettingsUpdate,
|
|
38
|
+
MessageMetadata,
|
|
25
39
|
} from '@animalabs/context-manager';
|
|
26
40
|
import type {
|
|
27
41
|
AgentConfig,
|
|
@@ -653,31 +667,43 @@ export class Agent {
|
|
|
653
667
|
const wireResults = content.flatMap((block) => block.type === 'tool_result'
|
|
654
668
|
// buildToolResultMessages serializes every payload to a string.
|
|
655
669
|
? [{ toolUseId: block.toolUseId, content: block.content as string, isError: block.isError }] : []);
|
|
656
|
-
|
|
670
|
+
// Stored under the house default cap (buildToolResultMessages), as what a
|
|
671
|
+
// stub keeps is too.
|
|
672
|
+
this.toolResultGuard.storeResults(content, wireResults, this._state.toolResults, undefined,
|
|
673
|
+
DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS);
|
|
657
674
|
}
|
|
658
675
|
|
|
659
|
-
// Compile context (with optional injections)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
676
|
+
// Compile the context (with optional injections) into a request. The
|
|
677
|
+
// guard's refusal retry is assembled the same way, afresh. It is compiled
|
|
678
|
+
// only once each withheld stub's annotation has finished, including one
|
|
679
|
+
// that began during the compile, and after a stub edit that failed has
|
|
680
|
+
// been retried; one whose store still fails is compiled as the bare
|
|
681
|
+
// notice.
|
|
682
|
+
let unguardedResults: ReturnType<Agent['buildToolResultMessages']> | undefined;
|
|
683
|
+
const assemble = async (): Promise<NormalizedRequest> => {
|
|
684
|
+
const { messages, systemInjections } = await this.toolResultGuard.afterAnnotations(
|
|
685
|
+
() => this.compileWithInjections(budget, injections));
|
|
686
|
+
// Read once, after the first compile, since results may have become
|
|
687
|
+
// ready while it ran; the refusal retry, assembled while the agent is
|
|
688
|
+
// inferring, sends the same ones.
|
|
689
|
+
unguardedResults ??= this._state.status === 'ready' && !guardedResults
|
|
690
|
+
? this.buildToolResultMessages(this._state.toolResults) : [];
|
|
691
|
+
messages.push(...unguardedResults);
|
|
692
|
+
return {
|
|
693
|
+
messages: this.toolResultGuard.prepareRequest(messages, true),
|
|
694
|
+
system: this.buildSystemPrompt(systemInjections),
|
|
695
|
+
config: {
|
|
696
|
+
model: this.model,
|
|
697
|
+
maxTokens: this.maxTokens,
|
|
698
|
+
...(this.temperature !== undefined && { temperature: this.temperature }),
|
|
699
|
+
...(this.thinking !== undefined && { thinking: this.thinking }),
|
|
700
|
+
},
|
|
701
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
702
|
+
...(this.providerParams && { providerParams: this.providerParams }),
|
|
703
|
+
assistantParticipant: this.name,
|
|
704
|
+
};
|
|
680
705
|
};
|
|
706
|
+
const request = await assemble();
|
|
681
707
|
|
|
682
708
|
const abortController = new AbortController();
|
|
683
709
|
if (options?.signal) {
|
|
@@ -690,7 +716,7 @@ export class Agent {
|
|
|
690
716
|
|
|
691
717
|
// Set state to inferring
|
|
692
718
|
this._inferenceStartedAt = Date.now();
|
|
693
|
-
const inferencePromise = this.doInference(request, abortController.signal);
|
|
719
|
+
const inferencePromise = this.doInference(request, abortController.signal, assemble);
|
|
694
720
|
this._state = { status: 'inferring', promise: inferencePromise, abortController };
|
|
695
721
|
|
|
696
722
|
try {
|
|
@@ -779,20 +805,27 @@ export class Agent {
|
|
|
779
805
|
* it does not touch agent state and does not call the membrane, and
|
|
780
806
|
* `ContextManager.compile` is itself side-effect-free (compression runs in
|
|
781
807
|
* the background). Safe to call regardless of the agent's current status.
|
|
808
|
+
* It doesn't wait for the tool-result guard's annotations, so a preview
|
|
809
|
+
* never waits on a workspace write (rollback, undo and redo report one), and
|
|
810
|
+
* may show a withheld stub before its place is added; the activation waits
|
|
811
|
+
* for them around its call.
|
|
782
812
|
*/
|
|
783
813
|
async buildActivationRequest(
|
|
784
814
|
availableTools: ToolDefinition[],
|
|
785
815
|
injections?: ContextInjection[],
|
|
786
|
-
budget?: TokenBudget
|
|
816
|
+
budget?: TokenBudget,
|
|
817
|
+
compressionTools: ToolDefinition[] = availableTools,
|
|
818
|
+
options: ActivationRequestOptions = {},
|
|
787
819
|
): Promise<NormalizedRequest> {
|
|
788
|
-
//
|
|
820
|
+
// Compression may revisit hidden tools in recorded history. Keep its full
|
|
821
|
+
// definition set separate from the resident's advertised tools: the
|
|
789
822
|
// autobiographical strategy must declare the same tools on its
|
|
790
823
|
// summarizer/compression requests, or transcripts containing tool blocks
|
|
791
824
|
// are refused by Anthropic's reasoning_extraction classifier (labclaude
|
|
792
825
|
// incident, 2026-07-09). Optional chaining: older context-manager
|
|
793
826
|
// versions don't have the hook.
|
|
794
827
|
(this.contextManager as unknown as { setToolDefinitions?: (t: ToolDefinition[]) => void })
|
|
795
|
-
.setToolDefinitions?.(
|
|
828
|
+
.setToolDefinitions?.(compressionTools);
|
|
796
829
|
|
|
797
830
|
const strategy = (this.contextManager as unknown as { getStrategy?: () => unknown })
|
|
798
831
|
.getStrategy?.() as {
|
|
@@ -837,10 +870,24 @@ export class Agent {
|
|
|
837
870
|
}))
|
|
838
871
|
.filter((m) => m.content.length > 0);
|
|
839
872
|
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
|
|
873
|
+
// Silent heartbeat ticks store no prompt row. Render a request-only
|
|
874
|
+
// separator before each tick's first surviving row so the wire formatter
|
|
875
|
+
// cannot merge the tick into the previous assistant message (see
|
|
876
|
+
// silent-heartbeat.ts for what can and cannot be matched). This agent's
|
|
877
|
+
// own first request for a tick ends on that same turn, so the prefix it
|
|
878
|
+
// caches is the one every later compile renders. "First" is per agent: a
|
|
879
|
+
// broadcast tick shares its eventId with every resident.
|
|
880
|
+
const ticks = this.indexSilentHeartbeatTicks();
|
|
881
|
+
messages = separateSilentHeartbeatTicks(messages, ticks.rows, this.name);
|
|
882
|
+
const openingTick = options.silentHeartbeat !== undefined
|
|
883
|
+
&& !ticks.started.has(options.silentHeartbeat.eventId);
|
|
884
|
+
|
|
885
|
+
if (openingTick) {
|
|
886
|
+
messages = [...messages, silentHeartbeatSeparatorTurn()];
|
|
887
|
+
} else if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
|
|
888
|
+
// Safety: ensure messages don't end with an assistant message.
|
|
889
|
+
// Some models reject trailing assistant messages ("prefill not supported"),
|
|
890
|
+
// and after context compression a stale assistant turn can end up last.
|
|
844
891
|
messages = [...messages, {
|
|
845
892
|
participant: 'user',
|
|
846
893
|
content: [{ type: 'text', text: '[Continue]' }],
|
|
@@ -866,6 +913,26 @@ export class Agent {
|
|
|
866
913
|
};
|
|
867
914
|
}
|
|
868
915
|
|
|
916
|
+
/** Stored silent-tick rows, memoized on the store's cached message array
|
|
917
|
+
* (ContextManager returns the same array until the store changes). */
|
|
918
|
+
private silentTickIndex: { source: readonly unknown[]; index: SilentTickIndex } | null = null;
|
|
919
|
+
|
|
920
|
+
private indexSilentHeartbeatTicks(): SilentTickIndex {
|
|
921
|
+
const empty: SilentTickIndex = { rows: new Map(), started: new Set() };
|
|
922
|
+
const cm = this.contextManager as Partial<ContextManager>;
|
|
923
|
+
if (typeof cm.getAllMessages !== 'function') return empty;
|
|
924
|
+
try {
|
|
925
|
+
const stored = cm.getAllMessages() as unknown as readonly StoredRowLike[];
|
|
926
|
+
if (this.silentTickIndex?.source === stored) return this.silentTickIndex.index;
|
|
927
|
+
const index = indexSilentTickRows(stored, this.name);
|
|
928
|
+
this.silentTickIndex = { source: stored, index };
|
|
929
|
+
return index;
|
|
930
|
+
} catch (error) {
|
|
931
|
+
console.error(`[silent-heartbeat] ${this.name}: could not index tick rows; separators omitted:`, error);
|
|
932
|
+
return empty;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
869
936
|
/**
|
|
870
937
|
* Start a yielding stream with context injections.
|
|
871
938
|
* Same as startStream but passes injections through to compile.
|
|
@@ -873,7 +940,9 @@ export class Agent {
|
|
|
873
940
|
async startStreamWithInjections(
|
|
874
941
|
availableTools: ToolDefinition[],
|
|
875
942
|
injections?: ContextInjection[],
|
|
876
|
-
budget?: TokenBudget
|
|
943
|
+
budget?: TokenBudget,
|
|
944
|
+
compressionTools: ToolDefinition[] = availableTools,
|
|
945
|
+
options: ActivationRequestOptions = {},
|
|
877
946
|
): Promise<StartStreamResult> {
|
|
878
947
|
if (this._state.status !== 'idle') {
|
|
879
948
|
throw new Error(`Agent ${this.name} cannot start stream in state ${this._state.status}`);
|
|
@@ -908,7 +977,11 @@ export class Agent {
|
|
|
908
977
|
this.lastStreamRealInputTokens = 0;
|
|
909
978
|
this.lastStreamOutputTokens = 0;
|
|
910
979
|
|
|
911
|
-
|
|
980
|
+
// Compiled only once each withheld stub's annotation has finished,
|
|
981
|
+
// including one that began during the compile, and after a stub edit that
|
|
982
|
+
// failed has been retried (see afterAnnotations).
|
|
983
|
+
const request = await this.toolResultGuard.afterAnnotations(
|
|
984
|
+
() => this.buildActivationRequest(availableTools, injections, budget, compressionTools, options));
|
|
912
985
|
request.messages = this.toolResultGuard.prepareRequest(request.messages, true);
|
|
913
986
|
|
|
914
987
|
const receiptAware = (this.contextManager as unknown as { getStrategy?: () => unknown })
|
|
@@ -1009,7 +1082,7 @@ export class Agent {
|
|
|
1009
1082
|
* Add an assistant response to context.
|
|
1010
1083
|
* Called by framework when stream completes.
|
|
1011
1084
|
*/
|
|
1012
|
-
addAssistantResponse(content: ContentBlock[]): void {
|
|
1085
|
+
addAssistantResponse(content: ContentBlock[], metadata?: MessageMetadata): void {
|
|
1013
1086
|
// A turn whose entire output is thinking blocks produced NOTHING: no
|
|
1014
1087
|
// speech, no tool call. That is what a refusal looks like on the wire —
|
|
1015
1088
|
// the provider returns signed thinking (often with empty text, the
|
|
@@ -1035,7 +1108,7 @@ export class Agent {
|
|
|
1035
1108
|
);
|
|
1036
1109
|
return;
|
|
1037
1110
|
}
|
|
1038
|
-
this.contextManager.addMessage(this.name, content);
|
|
1111
|
+
this.contextManager.addMessage(this.name, content, metadata);
|
|
1039
1112
|
}
|
|
1040
1113
|
|
|
1041
1114
|
/**
|
|
@@ -1126,9 +1199,13 @@ export class Agent {
|
|
|
1126
1199
|
|
|
1127
1200
|
private async doInference(
|
|
1128
1201
|
request: NormalizedRequest,
|
|
1129
|
-
signal
|
|
1202
|
+
signal: AbortSignal | undefined,
|
|
1203
|
+
/** Compiles the context into a request again, for the guard's retry. */
|
|
1204
|
+
reassemble: () => Promise<NormalizedRequest>,
|
|
1130
1205
|
): Promise<InferenceResult> {
|
|
1131
|
-
let response =
|
|
1206
|
+
let response = signal?.aborted
|
|
1207
|
+
? { aborted: true as const, reason: 'user' as const }
|
|
1208
|
+
: await this.membrane.stream(request, { signal });
|
|
1132
1209
|
// Usage of a refused attempt abandoned by the guard; it was billed and
|
|
1133
1210
|
// is added to the retry's usage below rather than overwritten.
|
|
1134
1211
|
let abandonedUsage: { inputTokens: number; outputTokens: number } | undefined;
|
|
@@ -1138,12 +1215,14 @@ export class Agent {
|
|
|
1138
1215
|
const ids = this.toolResultGuard.withhold('unknown');
|
|
1139
1216
|
if (ids) {
|
|
1140
1217
|
abandonedUsage = response.usage;
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
response =
|
|
1218
|
+
// The retry is compiled afresh, like every request: once each stub's
|
|
1219
|
+
// annotation has finished (agent-framework #277), and against the
|
|
1220
|
+
// budget without the batch's reservation, so a stub longer than its
|
|
1221
|
+
// original can't carry the request past the budget.
|
|
1222
|
+
const retry = await reassemble();
|
|
1223
|
+
response = signal?.aborted
|
|
1224
|
+
? { aborted: true, reason: 'user', partialUsage: response.usage }
|
|
1225
|
+
: await this.membrane.stream(retry, { signal });
|
|
1147
1226
|
}
|
|
1148
1227
|
}
|
|
1149
1228
|
|
|
@@ -1224,7 +1303,7 @@ export class Agent {
|
|
|
1224
1303
|
toolUseId: r.id,
|
|
1225
1304
|
content: r.result.isError
|
|
1226
1305
|
? truncateForHistory(r.result.error ?? 'Unknown error', DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS)
|
|
1227
|
-
:
|
|
1306
|
+
: nonErrorResultText(r.result, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS),
|
|
1228
1307
|
isError: r.result.isError,
|
|
1229
1308
|
}));
|
|
1230
1309
|
|
package/src/api/server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { timingSafeEqual } from 'node:crypto';
|
|
|
12
12
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
13
13
|
import type { AgentFramework } from '../framework.js';
|
|
14
14
|
import { ResumeBlockedError } from '../framework.js';
|
|
15
|
+
import { isReleaseModel } from '../provider-waits.js';
|
|
15
16
|
import type { TraceEvent, ProcessEvent } from '../types/index.js';
|
|
16
17
|
import type {
|
|
17
18
|
ApiServerConfig,
|
|
@@ -420,6 +421,37 @@ export class ApiServer {
|
|
|
420
421
|
case 'host.maintenanceTick':
|
|
421
422
|
this.requireAdminToken(params);
|
|
422
423
|
return this.framework.maintenanceTick();
|
|
424
|
+
// A provider's stated retry-after is honoured across restarts; this is
|
|
425
|
+
// the explicit operator override (recorded, so a restart keeps it).
|
|
426
|
+
case 'host.releaseProviderWait': {
|
|
427
|
+
this.requireAdminToken(params);
|
|
428
|
+
const p = (params ?? {}) as { agentName?: unknown; model?: unknown; requester?: unknown };
|
|
429
|
+
if (typeof p.agentName !== 'string' || p.agentName === '') throw new Error('agentName is required');
|
|
430
|
+
// Only an omitted model, or '*', releases every model: a malformed one
|
|
431
|
+
// is refused before anything changes, never widened.
|
|
432
|
+
const model = p.model;
|
|
433
|
+
if (!isReleaseModel(model)) throw new Error("model must be a model name, or '*' (or omitted) for every model");
|
|
434
|
+
// A requester over this surface names itself, so the record says
|
|
435
|
+
// where the name came from ('api:<name>'), and it can't pass for a
|
|
436
|
+
// host command's requester.
|
|
437
|
+
return {
|
|
438
|
+
released: this.framework.releaseProviderWait(
|
|
439
|
+
p.agentName,
|
|
440
|
+
model,
|
|
441
|
+
typeof p.requester === 'string' && p.requester !== '' ? `api:${p.requester}` : 'api',
|
|
442
|
+
),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
// A read, like host.status: the admin token gates the verbs that change
|
|
446
|
+
// something. (The HTTP /hostmode read takes it because that response is
|
|
447
|
+
// readable cross-origin; this surface checks the origin.)
|
|
448
|
+
case 'host.providerWaits': {
|
|
449
|
+
const agentName = params?.agentName;
|
|
450
|
+
if (agentName !== undefined && typeof agentName !== 'string') {
|
|
451
|
+
throw new Error('agentName must be an agent name, or omitted for every agent');
|
|
452
|
+
}
|
|
453
|
+
return { waits: this.framework.providerWaitSnapshot(agentName) };
|
|
454
|
+
}
|
|
423
455
|
|
|
424
456
|
default:
|
|
425
457
|
throw new Error(`Unknown command: ${command}`);
|
|
@@ -543,8 +575,13 @@ export class ApiServer {
|
|
|
543
575
|
this.currentBranch = params.name;
|
|
544
576
|
|
|
545
577
|
// Reversible Discord awareness markers are part of the branch projection.
|
|
546
|
-
//
|
|
547
|
-
|
|
578
|
+
// The switch is acknowledged once it has landed: delivery of the marks it
|
|
579
|
+
// adds and removes starts behind the MCPL data-plane gate, which holds
|
|
580
|
+
// channel traffic that arrives after it until they are attempted, or
|
|
581
|
+
// deferred while their server is disconnected, and a failure is an ops
|
|
582
|
+
// alert rather than a failed switch. (It used to await the awareness
|
|
583
|
+
// sync, which after a large rollback took the drain's whole length.)
|
|
584
|
+
this.framework.startDiscordAwarenessDelivery('branch switch');
|
|
548
585
|
|
|
549
586
|
// Materialize config files from the new branch
|
|
550
587
|
const ws = this.framework.getModule('workspace');
|
|
@@ -598,7 +635,8 @@ export class ApiServer {
|
|
|
598
635
|
|
|
599
636
|
if (result.undone) {
|
|
600
637
|
this.currentBranch = result.toBranch!;
|
|
601
|
-
|
|
638
|
+
// Marks follow behind the data-plane gate; the undo doesn't wait for Discord.
|
|
639
|
+
this.framework.startDiscordAwarenessDelivery('undo', params.agentName);
|
|
602
640
|
this.broadcast('branch:switched', {
|
|
603
641
|
from: result.fromBranch,
|
|
604
642
|
to: result.toBranch,
|
|
@@ -618,7 +656,8 @@ export class ApiServer {
|
|
|
618
656
|
|
|
619
657
|
if (result.redone) {
|
|
620
658
|
this.currentBranch = result.toBranch!;
|
|
621
|
-
|
|
659
|
+
// Marks follow behind the data-plane gate; the redo doesn't wait for Discord.
|
|
660
|
+
this.framework.startDiscordAwarenessDelivery('redo', params.agentName);
|
|
622
661
|
this.broadcast('branch:switched', {
|
|
623
662
|
from: result.fromBranch,
|
|
624
663
|
to: result.toBranch,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* This is a ROBUSTNESS boundary, not a security sandbox — same doctrine as
|
|
11
11
|
* GateScript: the agent already has broader host access through its tools.
|
|
12
12
|
* What this class guarantees is liveness: a wedged or runaway script cannot
|
|
13
|
-
* hang the agent turn (cancel -> grace -> SIGKILL -> respawn) and a crashed
|
|
13
|
+
* hang the agent turn (cancel + SIGINT -> grace -> SIGKILL -> respawn) and a crashed
|
|
14
14
|
* interpreter surfaces as a tool result, never as an unhandled rejection.
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -65,6 +65,8 @@ export interface PyRunnerOptions {
|
|
|
65
65
|
toolCallTimeoutMs?: number;
|
|
66
66
|
/** Whole-script deadline; exceeded -> cancel, grace, kill. */
|
|
67
67
|
scriptTimeoutMs?: number;
|
|
68
|
+
/** How long a script gets to stop after its deadline before the interpreter is killed (default 10s). */
|
|
69
|
+
cancelGraceMs?: number;
|
|
68
70
|
/** Idle interpreter reclaim (state lost), mirroring container reclaim. */
|
|
69
71
|
idleReclaimMs?: number;
|
|
70
72
|
onToolCall: ScriptToolCallHandler;
|
|
@@ -76,6 +78,8 @@ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 270_000;
|
|
|
76
78
|
const DEFAULT_SCRIPT_TIMEOUT_MS = 600_000;
|
|
77
79
|
const DEFAULT_IDLE_RECLAIM_MS = 300_000;
|
|
78
80
|
const CANCEL_GRACE_MS = 10_000;
|
|
81
|
+
/** After the deadline, SIGINT is repeated at this interval until the script reports or is killed. */
|
|
82
|
+
const INTERRUPT_REPEAT_MS = 200;
|
|
79
83
|
|
|
80
84
|
/** The longest delay Node's timers honour (~24.8 days); a longer one fires after ~1 ms. */
|
|
81
85
|
export const MAX_TIMER_MS = 2_147_483_647;
|
|
@@ -92,6 +96,8 @@ interface PendingExec {
|
|
|
92
96
|
resolve: (result: ExecResult) => void;
|
|
93
97
|
deadlineTimer: ReturnType<typeof setTimeout> | null;
|
|
94
98
|
killTimer: ReturnType<typeof setTimeout> | null;
|
|
99
|
+
/** Repeats the deadline SIGINT (see interruptChild). */
|
|
100
|
+
interruptTimer?: ReturnType<typeof setInterval>;
|
|
95
101
|
settled: boolean;
|
|
96
102
|
/** Set once the deadline fired: the result then says the script ran out of time. */
|
|
97
103
|
deadlineMs: number | null;
|
|
@@ -101,6 +107,7 @@ export class PyRunner {
|
|
|
101
107
|
private readonly pythonPath: string;
|
|
102
108
|
private readonly toolCallTimeoutMs: number;
|
|
103
109
|
private readonly scriptTimeoutMs: number;
|
|
110
|
+
private readonly cancelGraceMs: number;
|
|
104
111
|
private readonly idleReclaimMs: number;
|
|
105
112
|
private readonly onToolCall: ScriptToolCallHandler;
|
|
106
113
|
private readonly label: string;
|
|
@@ -114,18 +121,21 @@ export class PyRunner {
|
|
|
114
121
|
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
115
122
|
private execCounter = 0;
|
|
116
123
|
private disposed = false;
|
|
124
|
+
/** An exec has passed the busy check but is still awaiting the interpreter. */
|
|
125
|
+
private starting = false;
|
|
117
126
|
|
|
118
127
|
constructor(options: PyRunnerOptions) {
|
|
119
128
|
this.pythonPath = options.pythonPath ?? 'python3';
|
|
120
129
|
this.toolCallTimeoutMs = options.toolCallTimeoutMs ?? DEFAULT_TOOL_CALL_TIMEOUT_MS;
|
|
121
130
|
this.scriptTimeoutMs = options.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS;
|
|
131
|
+
this.cancelGraceMs = options.cancelGraceMs ?? CANCEL_GRACE_MS;
|
|
122
132
|
this.idleReclaimMs = options.idleReclaimMs ?? DEFAULT_IDLE_RECLAIM_MS;
|
|
123
133
|
this.onToolCall = options.onToolCall;
|
|
124
134
|
this.label = options.label ?? 'pytc';
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
get busy(): boolean {
|
|
128
|
-
return this.pending !== null;
|
|
138
|
+
return this.pending !== null || this.starting;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
/**
|
|
@@ -150,7 +160,7 @@ export class PyRunner {
|
|
|
150
160
|
if (this.disposed) {
|
|
151
161
|
return { stdout: '', stderr: 'code_execution runner disposed', returnCode: 1, aborted: true };
|
|
152
162
|
}
|
|
153
|
-
if (this.
|
|
163
|
+
if (this.busy) {
|
|
154
164
|
return {
|
|
155
165
|
stdout: '',
|
|
156
166
|
stderr: 'RuntimeError: another code_execution script is already running for this agent',
|
|
@@ -160,6 +170,7 @@ export class PyRunner {
|
|
|
160
170
|
}
|
|
161
171
|
this.clearIdleTimer();
|
|
162
172
|
|
|
173
|
+
this.starting = true; // claimed before the await: a same-tick second exec must see it
|
|
163
174
|
try {
|
|
164
175
|
await this.ensureChild();
|
|
165
176
|
} catch (err) {
|
|
@@ -171,6 +182,8 @@ export class PyRunner {
|
|
|
171
182
|
returnCode: 1,
|
|
172
183
|
aborted: true,
|
|
173
184
|
};
|
|
185
|
+
} finally {
|
|
186
|
+
this.starting = false; // `pending` is set below, with no await in between
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
const execId = `e${++this.execCounter}`;
|
|
@@ -193,16 +206,26 @@ export class PyRunner {
|
|
|
193
206
|
// Deadline: ask politely first (script sees CancelledError and its
|
|
194
207
|
// exec_result still flows back), then kill on unresponsiveness.
|
|
195
208
|
this.send({ op: 'cancel', id: execId, reason: 'deadline' });
|
|
209
|
+
// The cancel lands only when the script awaits. Blocking code (time.sleep, a
|
|
210
|
+
// busy loop, a blocking read) never does, so interrupt it as well: the runtime
|
|
211
|
+
// raises KeyboardInterrupt in the script's own code, or schedules the
|
|
212
|
+
// cancellation of a script that is waiting. Repeated, because a waiting script
|
|
213
|
+
// can resume into blocking code before that cancellation runs; the runtime
|
|
214
|
+
// ignores the repeats once the script was interrupted or cancelled.
|
|
215
|
+
this.interruptChild();
|
|
216
|
+
pending.interruptTimer = setInterval(() => this.interruptChild(), INTERRUPT_REPEAT_MS);
|
|
196
217
|
pending.killTimer = setTimeout(() => {
|
|
197
218
|
pending.deadlineMs = null; // this message already says why
|
|
198
219
|
this.settlePending({
|
|
199
220
|
stdout: '',
|
|
200
|
-
stderr:
|
|
221
|
+
stderr:
|
|
222
|
+
`script stopped: it reached its ${formatLimit(deadlineMs)} time limit and did not respond, ` +
|
|
223
|
+
'so it was killed and the interpreter restarted (variables from earlier scripts are gone)',
|
|
201
224
|
returnCode: 1,
|
|
202
225
|
aborted: true,
|
|
203
226
|
});
|
|
204
227
|
this.reclaim('deadline-kill');
|
|
205
|
-
},
|
|
228
|
+
}, this.cancelGraceMs);
|
|
206
229
|
}, deadlineMs);
|
|
207
230
|
// A day-scale background deadline must not hold the process open.
|
|
208
231
|
if (background) pending.deadlineTimer.unref?.();
|
|
@@ -299,7 +322,7 @@ export class PyRunner {
|
|
|
299
322
|
});
|
|
300
323
|
|
|
301
324
|
this.reader = createInterface({ input: child.stdout });
|
|
302
|
-
this.reader.on('line', (line) => this.handleLine(line));
|
|
325
|
+
this.reader.on('line', (line) => this.handleLine(line, child));
|
|
303
326
|
|
|
304
327
|
this.childReady = new Promise<void>((resolve, reject) => {
|
|
305
328
|
const onReady = () => {
|
|
@@ -333,7 +356,7 @@ export class PyRunner {
|
|
|
333
356
|
|
|
334
357
|
private readyResolver: (() => void) | null = null;
|
|
335
358
|
|
|
336
|
-
private handleLine(line: string): void {
|
|
359
|
+
private handleLine(line: string, child: ChildProcessWithoutNullStreams): void {
|
|
337
360
|
let msg: { op?: string; id?: string; name?: string; args?: unknown; stdout?: string; stderr?: string; return_code?: number };
|
|
338
361
|
try {
|
|
339
362
|
msg = JSON.parse(line);
|
|
@@ -358,7 +381,7 @@ export class PyRunner {
|
|
|
358
381
|
this.onToolCall(toolName, args)
|
|
359
382
|
.catch((err) => `Error: ${err instanceof Error ? err.message : String(err)}`)
|
|
360
383
|
.then((result) => {
|
|
361
|
-
this.
|
|
384
|
+
this.reply(child, { op: 'tool_result', id: callId, result }, `result of ${toolName} call ${callId}`);
|
|
362
385
|
});
|
|
363
386
|
return;
|
|
364
387
|
}
|
|
@@ -376,7 +399,7 @@ export class PyRunner {
|
|
|
376
399
|
`wake handler failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
377
400
|
: Promise.resolve('this script is not allowed to wake the agent');
|
|
378
401
|
void refuse.then((error) => {
|
|
379
|
-
this.
|
|
402
|
+
this.reply(child, { op: 'wake_ack', id: wakeId, ...(error ? { error } : {}) }, `ack of wake ${wakeId}`);
|
|
380
403
|
});
|
|
381
404
|
return;
|
|
382
405
|
}
|
|
@@ -414,10 +437,25 @@ export class PyRunner {
|
|
|
414
437
|
}
|
|
415
438
|
if (pending.deadlineTimer) clearTimeout(pending.deadlineTimer);
|
|
416
439
|
if (pending.killTimer) clearTimeout(pending.killTimer);
|
|
440
|
+
if (pending.interruptTimer) clearInterval(pending.interruptTimer);
|
|
417
441
|
this.pending = null;
|
|
418
442
|
pending.resolve(result);
|
|
419
443
|
}
|
|
420
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Answer the interpreter that asked. Call and wake ids restart in every
|
|
447
|
+
* interpreter, so an answer that outlives its interpreter (the script was
|
|
448
|
+
* aborted, killed or crashed, or the interpreter reclaimed) would resolve
|
|
449
|
+
* the same id in the one that replaced it, inside another script: drop it.
|
|
450
|
+
*/
|
|
451
|
+
private reply(child: ChildProcessWithoutNullStreams, msg: unknown, what: string): void {
|
|
452
|
+
if (child !== this.child) {
|
|
453
|
+
console.error(`[pytc:${this.label}] dropped late ${what}: the interpreter that asked for it is gone`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.send(msg);
|
|
457
|
+
}
|
|
458
|
+
|
|
421
459
|
private send(obj: unknown): void {
|
|
422
460
|
const child = this.child;
|
|
423
461
|
if (!child || child.exitCode !== null || child.killed) return;
|
|
@@ -430,6 +468,25 @@ export class PyRunner {
|
|
|
430
468
|
}
|
|
431
469
|
}
|
|
432
470
|
|
|
471
|
+
/**
|
|
472
|
+
* SIGINT the interpreter: blocking code in the running script raises
|
|
473
|
+
* KeyboardInterrupt. Not on Windows, where Node's kill() ends the process
|
|
474
|
+
* outright; there the cancel op and the kill grace remain.
|
|
475
|
+
* process.kill rather than child.kill, which would mark the child `killed`:
|
|
476
|
+
* a killed child gets no more protocol messages and is respawned on the
|
|
477
|
+
* next exec, losing the interpreter state this keeps.
|
|
478
|
+
*/
|
|
479
|
+
private interruptChild(): void {
|
|
480
|
+
const child = this.child;
|
|
481
|
+
if (process.platform === 'win32' || !child || child.exitCode !== null || child.killed) return;
|
|
482
|
+
if (child.pid === undefined) return;
|
|
483
|
+
try {
|
|
484
|
+
process.kill(child.pid, 'SIGINT');
|
|
485
|
+
} catch {
|
|
486
|
+
// already gone: the exit handler settles the exec
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
433
490
|
/** Kill interpreter + clean temp dir. State is lost (by design). */
|
|
434
491
|
private reclaim(reason: string): void {
|
|
435
492
|
if (this.child) {
|