@animalabs/agent-framework 0.23.3 → 0.24.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 +212 -42
- package/dist/src/agent.d.ts +65 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +98 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +14 -12
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/clock-rows.d.ts +115 -0
- package/dist/src/clock-rows.d.ts.map +1 -0
- package/dist/src/clock-rows.js +222 -0
- package/dist/src/clock-rows.js.map +1 -0
- package/dist/src/code-execution/engine.d.ts +177 -0
- package/dist/src/code-execution/engine.d.ts.map +1 -0
- package/dist/src/code-execution/engine.js +494 -0
- package/dist/src/code-execution/engine.js.map +1 -0
- package/dist/src/code-execution/index.d.ts +5 -10
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js +5 -9
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/isolate/calls.d.ts +114 -0
- package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/calls.js +145 -0
- package/dist/src/code-execution/isolate/calls.js.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.js +5 -0
- package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
- package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
- package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-main.js +132 -0
- package/dist/src/code-execution/isolate/child-main.js.map +1 -0
- package/dist/src/code-execution/isolate/child.d.ts +44 -0
- package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child.js +315 -0
- package/dist/src/code-execution/isolate/child.js.map +1 -0
- package/dist/src/code-execution/isolate/frames.d.ts +80 -0
- package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/frames.js +210 -0
- package/dist/src/code-execution/isolate/frames.js.map +1 -0
- package/dist/src/code-execution/isolate/output.d.ts +68 -0
- package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/output.js +143 -0
- package/dist/src/code-execution/isolate/output.js.map +1 -0
- package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
- package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/prelude.js +401 -0
- package/dist/src/code-execution/isolate/prelude.js.map +1 -0
- package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
- package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/protocol.js +150 -0
- package/dist/src/code-execution/isolate/protocol.js.map +1 -0
- package/dist/src/code-execution/isolate/render.d.ts +57 -0
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/render.js +214 -0
- package/dist/src/code-execution/isolate/render.js.map +1 -0
- package/dist/src/code-execution/isolate/runner.d.ts +250 -0
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/runner.js +854 -0
- package/dist/src/code-execution/isolate/runner.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/scripts.js +357 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -0
- package/dist/src/code-execution/quickjs.wasm +0 -0
- package/dist/src/code-execution/tool-definition.d.ts +93 -43
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +176 -142
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/deadline-timer.d.ts +3 -0
- package/dist/src/deadline-timer.d.ts.map +1 -1
- package/dist/src/deadline-timer.js +1 -1
- package/dist/src/deadline-timer.js.map +1 -1
- package/dist/src/framework.d.ts +1120 -457
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4635 -2505
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +28 -4
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +89 -13
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -3
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +16 -3
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +8 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +483 -115
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1735 -425
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +19 -39
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +27 -59
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +9 -5
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +50 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +130 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -2
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +6 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +8 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +15 -6
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +168 -53
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -5
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +4 -5
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +97 -10
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +174 -17
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -14
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +227 -50
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +9 -20
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +7 -49
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +4 -4
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +50 -21
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +158 -74
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +33 -0
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +55 -7
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +163 -34
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +22 -6
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +4 -2
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +100 -13
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +8 -13
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +27 -4
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +15 -3
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +27 -5
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +4 -1
- package/dist/src/modules/unknown-tool.d.ts.map +1 -1
- package/dist/src/modules/unknown-tool.js +4 -1
- package/dist/src/modules/unknown-tool.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +3 -1
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
- package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-plan.js +794 -0
- package/dist/src/modules/workspace/edit-plan.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +117 -0
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-text.js +584 -0
- package/dist/src/modules/workspace/edit-text.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +5 -4
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +78 -25
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +493 -62
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2216 -652
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +213 -38
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +416 -36
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +15 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/text.d.ts +32 -0
- package/dist/src/modules/workspace/text.d.ts.map +1 -0
- package/dist/src/modules/workspace/text.js +67 -0
- package/dist/src/modules/workspace/text.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +210 -6
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +3 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/operator-log.d.ts +52 -24
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts +26 -1
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +164 -39
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +23 -4
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +11 -0
- package/dist/src/send-tools.d.ts.map +1 -0
- package/dist/src/send-tools.js +11 -0
- package/dist/src/send-tools.js.map +1 -0
- package/dist/src/timezone.d.ts +4 -2
- package/dist/src/timezone.d.ts.map +1 -1
- package/dist/src/timezone.js +4 -2
- package/dist/src/timezone.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts +46 -7
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +60 -7
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +14 -11
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +17 -14
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +3 -2
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +3 -2
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +21 -0
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +142 -40
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +31 -6
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +66 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +69 -62
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +40 -8
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +111 -10
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/types.d.ts +16 -0
- package/dist/src/usage/types.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +14 -1
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +81 -41
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +6 -8
- package/src/agent.ts +99 -12
- package/src/api/server.ts +14 -12
- package/src/clock-rows.ts +302 -0
- package/src/code-execution/engine.ts +595 -0
- package/src/code-execution/index.ts +5 -17
- package/src/code-execution/isolate/calls.ts +199 -0
- package/src/code-execution/isolate/child-entry.ts +5 -0
- package/src/code-execution/isolate/child-main.ts +132 -0
- package/src/code-execution/isolate/child.ts +327 -0
- package/src/code-execution/isolate/frames.ts +209 -0
- package/src/code-execution/isolate/output.ts +161 -0
- package/src/code-execution/isolate/prelude.ts +409 -0
- package/src/code-execution/isolate/protocol.ts +240 -0
- package/src/code-execution/isolate/render.ts +230 -0
- package/src/code-execution/isolate/runner.ts +911 -0
- package/src/code-execution/isolate/scripts.ts +479 -0
- package/src/code-execution/quickjs.wasm +0 -0
- package/src/code-execution/quickjs.wasm.LICENSES +149 -0
- package/src/code-execution/tool-definition.ts +236 -167
- package/src/deadline-timer.ts +1 -1
- package/src/framework.ts +4978 -2671
- package/src/gate/event-gate.ts +79 -17
- package/src/held-writes.ts +17 -3
- package/src/index.ts +32 -22
- package/src/mcpl/channel-registry.ts +1892 -471
- package/src/mcpl/conversation-router.ts +26 -72
- package/src/mcpl/convert-block.ts +9 -5
- package/src/mcpl/inbound-source.ts +131 -0
- package/src/mcpl/index.ts +4 -2
- package/src/mcpl/log-value.ts +9 -0
- package/src/mcpl/modern-connection.ts +162 -53
- package/src/mcpl/prose-grammar.ts +4 -5
- package/src/mcpl/protocol-family.ts +200 -22
- package/src/mcpl/push-coalescer.ts +306 -62
- package/src/mcpl/push-handler.ts +12 -54
- package/src/mcpl/reference-fetcher.ts +4 -4
- package/src/mcpl/server-connection.ts +180 -79
- package/src/mcpl/tags.ts +36 -0
- package/src/mcpl/tool-result-normalize.ts +2 -1
- package/src/mcpl/transport.ts +162 -33
- package/src/mcpl/types.ts +23 -6
- package/src/module-registry.ts +92 -13
- package/src/modules/discord/index.ts +8 -13
- package/src/modules/history/index.ts +25 -3
- package/src/modules/history/semantic.ts +29 -6
- package/src/modules/index.ts +1 -0
- package/src/modules/unknown-tool.ts +4 -1
- package/src/modules/workspace/branch-intent.ts +3 -1
- package/src/modules/workspace/edit-plan.ts +954 -0
- package/src/modules/workspace/edit-text.ts +618 -0
- package/src/modules/workspace/effects.ts +70 -19
- package/src/modules/workspace/index.ts +2293 -650
- package/src/modules/workspace/observe.ts +465 -56
- package/src/modules/workspace/reconcile.ts +14 -7
- package/src/modules/workspace/text.ts +68 -0
- package/src/modules/workspace/types.ts +211 -6
- package/src/modules/workspace/watcher.ts +3 -2
- package/src/operator-log.ts +47 -19
- package/src/recovery/discord-awareness-outbox.ts +1500 -356
- package/src/recovery/offline-branch.ts +189 -55
- package/src/recovery/recover-cli.ts +25 -4
- package/src/send-tools.ts +11 -0
- package/src/timezone.ts +4 -2
- package/src/tool-image-ledger.ts +62 -8
- package/src/tool-result-guard.ts +18 -15
- package/src/tool-result-history.ts +3 -2
- package/src/tune-out/coordinator.ts +166 -45
- package/src/types/agent.ts +32 -6
- package/src/types/events.ts +66 -9
- package/src/types/framework.ts +69 -62
- package/src/types/module.ts +40 -8
- package/src/types/trace.ts +121 -15
- package/src/usage/types.ts +16 -0
- package/src/usage/usage-tracker.ts +86 -43
- package/dist/src/code-execution/py-runner.d.ts +0 -191
- package/dist/src/code-execution/py-runner.d.ts.map +0 -1
- package/dist/src/code-execution/py-runner.js +0 -655
- package/dist/src/code-execution/py-runner.js.map +0 -1
- package/dist/src/code-execution/runtime-py.d.ts +0 -50
- package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
- package/dist/src/code-execution/runtime-py.js +0 -677
- package/dist/src/code-execution/runtime-py.js.map +0 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
- package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
- package/dist/src/mcpl/prose-stream-router.js +0 -291
- package/dist/src/mcpl/prose-stream-router.js.map +0 -1
- package/src/code-execution/py-runner.ts +0 -791
- package/src/code-execution/runtime-py.ts +0 -677
- package/src/mcpl/prose-stream-router.ts +0 -268
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guest's prelude: JavaScript the engine evaluates before a script, which
|
|
3
|
+
* builds what a script meets from the binding's two globals, `__host(name,
|
|
4
|
+
* argsJson)` and `__output(stream, text)`:
|
|
5
|
+
*
|
|
6
|
+
* - `tools[name](args)`, each tool by its exact name, answering one result
|
|
7
|
+
* shape, `{ content, structuredContent, isError }`. A refusal before
|
|
8
|
+
* dispatch rejects with an Error carrying the refusal's fields. Reading a
|
|
9
|
+
* name that isn't a tool throws at once, naming the close matches.
|
|
10
|
+
* At most MAX_CALLS_IN_FLIGHT are out at once, and one past them waits
|
|
11
|
+
* here, in the script's own memory, until one is answered.
|
|
12
|
+
* - `ALL_TOOLS`, each tool's name, description and input schema.
|
|
13
|
+
* - `text(value)` and `console.log/info/warn/error/debug(...)`, which write
|
|
14
|
+
* the script's output: a string as it is, anything else as JSON.
|
|
15
|
+
* - `wake_agent(payload)`, which asks the host to wake the agent.
|
|
16
|
+
* - `__run(src)`, which runs a script as the body of an async function, so it
|
|
17
|
+
* may `await` at its top level and `return` a value.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here guards a boundary: the guest's whole world is this instance,
|
|
20
|
+
* and every call it makes is checked by the host. Built-ins aren't frozen, so a
|
|
21
|
+
* script that patches one breaks only itself: its calls and its ending are
|
|
22
|
+
* recorded outside the guest.
|
|
23
|
+
*
|
|
24
|
+
* Adapted from pi-codemode's guest prelude (`@earendil-works/pi-codemode`
|
|
25
|
+
* 1.3.0, `dist/runtime/prelude-source.js`): the tools proxy and its
|
|
26
|
+
* close-match error, the output helpers and the error formatting. Its licence,
|
|
27
|
+
* the LICENSE at the root of github.com/earendil-works/pi, which the package's
|
|
28
|
+
* `license: "MIT"` refers to:
|
|
29
|
+
*
|
|
30
|
+
* MIT License. Copyright (c) 2025 Mario Zechner. Permission is hereby granted,
|
|
31
|
+
* free of charge, to any person obtaining a copy of this software and
|
|
32
|
+
* associated documentation files (the "Software"), to deal in the Software
|
|
33
|
+
* without restriction, including without limitation the rights to use, copy,
|
|
34
|
+
* modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
35
|
+
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
36
|
+
* subject to the following conditions: The above copyright notice and this
|
|
37
|
+
* permission notice shall be included in all copies or substantial portions
|
|
38
|
+
* of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
39
|
+
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
40
|
+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
41
|
+
* EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
42
|
+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
43
|
+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
44
|
+
* USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { MAX_CALLS_IN_FLIGHT } from './protocol.js';
|
|
48
|
+
import { cutOf } from './render.js';
|
|
49
|
+
|
|
50
|
+
/** The host call name `wake_agent()` uses. Parentheses can't be in a tool's name, so it never names a tool. */
|
|
51
|
+
export const WAKE_CALL_NAME = '(wake_agent)';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* How many lines QuickJS's async function constructor puts before a script's
|
|
55
|
+
* first line, and the filename it gives the script's code: a script's frames
|
|
56
|
+
* read `<input>:N:C`, with its own line N less this.
|
|
57
|
+
*/
|
|
58
|
+
export const SCRIPT_LINE_OFFSET = 2;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A frame of the script's own code, as QuickJS writes one in a stack: " at
|
|
62
|
+
* NAME (PLACE)", or " at PLACE" for a syntax error, its PLACE at the end
|
|
63
|
+
* and the script's, `<input>:LINE:COLUMN`, its LINE of at most ten digits, as
|
|
64
|
+
* QuickJS writes a line (a 32-bit integer). A NAME may hold any character but
|
|
65
|
+
* a line feed, which the stack's lines are split at, so `[^]` rather than
|
|
66
|
+
* `.`, which stops at a carriage return and U+2028 and U+2029 too. The
|
|
67
|
+
* guest's copy of the rule (errorText) is given these two patterns as they
|
|
68
|
+
* are.
|
|
69
|
+
*/
|
|
70
|
+
export const SCRIPT_FRAME = /^\s+at\s[^]*<input>:\d{1,10}:\d+\)?$/;
|
|
71
|
+
/** A script frame's place, at its end: its line is renumbered as the agent wrote it. */
|
|
72
|
+
export const SCRIPT_PLACE = /<input>:(\d{1,10})(:\d+\)?)$/;
|
|
73
|
+
/**
|
|
74
|
+
* SCRIPT_FRAME as it reads a line within a whole stack, from the line's
|
|
75
|
+
* start (sticky) to the line feed or the stack's end that closes it, so that
|
|
76
|
+
* no string is made for the line: a line holds no line feed, so there `\s`
|
|
77
|
+
* is `[^\S\n]` and `[^]` is `[^\n]`. A test holds the two to one reading.
|
|
78
|
+
*/
|
|
79
|
+
const SCRIPT_FRAME_IN_STACK = /[^\S\n]+at[^\S\n][^\n]*<input>:\d{1,10}:\d+\)?(?=\n|$)/y;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A failure's frames as the agent reads them, one per line: the script's own,
|
|
83
|
+
* numbered as the agent wrote its lines, leaving out the prelude's frames and
|
|
84
|
+
* the engine's native ones. QuickJS gives an Error's stack as its frames
|
|
85
|
+
* alone, and nothing here adds the failure's name and message to it, so the
|
|
86
|
+
* script's frames are the lines shaped as its frames are (SCRIPT_FRAME): a
|
|
87
|
+
* line of a stack the script or its Error.prepareStackTrace set is one if it
|
|
88
|
+
* is shaped so, and isn't otherwise. A function's name holding a line feed
|
|
89
|
+
* splits its frame in two, and each half is read on its own, so the frame is
|
|
90
|
+
* lost unless a half is itself shaped as a frame. Code the script compiles at
|
|
91
|
+
* run time is named `<input>` as the script is, so the rule takes its frames
|
|
92
|
+
* for the script's: an eval's read at its string's lines less two, and a
|
|
93
|
+
* Function constructor's at its body's own lines, plus one for each line
|
|
94
|
+
* break in its parameters, since the constructor puts its parameters and two
|
|
95
|
+
* lines before the body, as the prelude's AsyncFunction does. The prelude's
|
|
96
|
+
* errorText applies this rule in the guest, to an Error the script logs,
|
|
97
|
+
* leading with the name and message, while a string the script logs, an
|
|
98
|
+
* Error's `stack` among them, shows as QuickJS or the script wrote it; the
|
|
99
|
+
* child applies the rule here to every failure's stack. The two are one rule:
|
|
100
|
+
* the guest is given the pair of patterns as they are, and the walk here
|
|
101
|
+
* reads SCRIPT_FRAME as SCRIPT_FRAME_IN_STACK and renumbers a place itself,
|
|
102
|
+
* as SCRIPT_PLACE says, so a change to one has to be made to all three, and
|
|
103
|
+
* tests hold them together.
|
|
104
|
+
*/
|
|
105
|
+
export function scriptFrames(stack: string, max = Infinity): string {
|
|
106
|
+
// Two walks of the stack, a line at a time, making no string for a line
|
|
107
|
+
// that isn't a frame, or for a frame the cut doesn't keep: a stack the
|
|
108
|
+
// script set can be any length, as many lines as characters or one line as
|
|
109
|
+
// long as the stack, and beside it the child holds only what the cut keeps.
|
|
110
|
+
// The first walk measures the frames' text; the second takes what the cut
|
|
111
|
+
// keeps of it, all of it if it fits `max`, or else its first and last
|
|
112
|
+
// halves (bounded's cut, cutOf).
|
|
113
|
+
let length = -1;
|
|
114
|
+
eachFrame(stack, (start, place, line, rest, end) => {
|
|
115
|
+
length += 1 + (place - start) + LABEL_CHARS + digits(line) + (end - rest);
|
|
116
|
+
});
|
|
117
|
+
if (length < 0) return '';
|
|
118
|
+
const fits = length <= max;
|
|
119
|
+
const half = fits ? length : Math.floor(max / 2);
|
|
120
|
+
const tailFrom = length - (fits ? 0 : half);
|
|
121
|
+
const head: string[] = [];
|
|
122
|
+
const tail: string[] = [];
|
|
123
|
+
let at = 0;
|
|
124
|
+
// The part of the frames' text from `at` that `text` gives from `from` to
|
|
125
|
+
// `to`: what of it falls in the head or the tail goes there.
|
|
126
|
+
const put = (text: string, from: number, to: number): void => {
|
|
127
|
+
const n = to - from;
|
|
128
|
+
if (at < half) head.push(text.slice(from, from + Math.min(n, half - at)));
|
|
129
|
+
if (at + n > tailFrom) tail.push(text.slice(from + Math.max(0, tailFrom - at), to));
|
|
130
|
+
at += n;
|
|
131
|
+
};
|
|
132
|
+
eachFrame(stack, (start, place, line, rest, end) => {
|
|
133
|
+
if (at > 0) put('\n', 0, 1);
|
|
134
|
+
put(stack, start, place);
|
|
135
|
+
const n = LABEL_CHARS + digits(line);
|
|
136
|
+
if (at < half || at + n > tailFrom) put(`script:${line}`, 0, n);
|
|
137
|
+
else at += n;
|
|
138
|
+
put(stack, rest, end);
|
|
139
|
+
});
|
|
140
|
+
return fits ? head.join('') : cutOf(head.join(''), tail.join(''), length);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `script:`, which a frame's place is renumbered with. */
|
|
144
|
+
const LABEL_CHARS = 7;
|
|
145
|
+
|
|
146
|
+
/** How many digits a whole number from 1 to 9,999,999,999 is written with. */
|
|
147
|
+
function digits(n: number): number {
|
|
148
|
+
let count = 1;
|
|
149
|
+
for (let rest = n; rest >= 10; rest = Math.floor(rest / 10)) count += 1;
|
|
150
|
+
return count;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Each of the script's frames in `stack`, in order, as scriptFrames reads
|
|
155
|
+
* them, with no string made for a line: where its line starts, where its
|
|
156
|
+
* place starts, its place's line as the agent wrote it, where the rest of its
|
|
157
|
+
* place starts (`:COLUMN`, and a closing parenthesis), and where its line
|
|
158
|
+
* ends.
|
|
159
|
+
*/
|
|
160
|
+
function eachFrame(stack: string, frame: (start: number, place: number, line: number, rest: number, end: number) => void): void {
|
|
161
|
+
for (let start = 0; start <= stack.length; ) {
|
|
162
|
+
let end = stack.indexOf('\n', start);
|
|
163
|
+
if (end === -1) end = stack.length;
|
|
164
|
+
SCRIPT_FRAME_IN_STACK.lastIndex = start;
|
|
165
|
+
if (SCRIPT_FRAME_IN_STACK.test(stack)) {
|
|
166
|
+
// A frame ends in its place, and no later `<input>:` fits in what
|
|
167
|
+
// follows it there. Its line has at most ten digits, so the number is
|
|
168
|
+
// exact and digits() ends, as it wouldn't on the Infinity that a few
|
|
169
|
+
// hundred digits parse to.
|
|
170
|
+
const place = stack.lastIndexOf('<input>:', end);
|
|
171
|
+
let line = 0;
|
|
172
|
+
let rest = place + 8;
|
|
173
|
+
for (let c = stack.charCodeAt(rest); c >= 48 && c <= 57; c = stack.charCodeAt(++rest)) line = line * 10 + (c - 48);
|
|
174
|
+
frame(start, place, Math.max(1, line - SCRIPT_LINE_OFFSET), rest, end);
|
|
175
|
+
}
|
|
176
|
+
start = end + 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** One tool as the prelude lists it in ALL_TOOLS. */
|
|
181
|
+
export interface PreludeTool {
|
|
182
|
+
name: string;
|
|
183
|
+
description: string;
|
|
184
|
+
/** The tool's input schema, as the agent's own tool list gives it. */
|
|
185
|
+
inputSchema?: unknown;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The prelude, for these tools. The child, not the prelude, refuses a wake
|
|
190
|
+
* past the script's cap (child.ts). The tool list goes in as
|
|
191
|
+
* a JSON string the prelude parses, so a schema's `__proto__` key stays a key,
|
|
192
|
+
* as it would not in an object literal, and the prelude is one string the
|
|
193
|
+
* engine evaluates as global code.
|
|
194
|
+
*/
|
|
195
|
+
export function preludeSource(tools: readonly PreludeTool[]): string {
|
|
196
|
+
const toolsJson = JSON.stringify(tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema ?? null })));
|
|
197
|
+
return `(function (host, out, toolList) {
|
|
198
|
+
"use strict";
|
|
199
|
+
const stringify = JSON.stringify;
|
|
200
|
+
const ErrorCtor = Error;
|
|
201
|
+
const TypeErrorCtor = TypeError;
|
|
202
|
+
const scriptOffset = ${SCRIPT_LINE_OFFSET};
|
|
203
|
+
|
|
204
|
+
// QuickJS stacks list frames only: lead with "Name: message", as V8 does, and
|
|
205
|
+
// keep only the script's own frames, leaving out this prelude's, the
|
|
206
|
+
// engine's native ones and the line that started the script, each numbered
|
|
207
|
+
// as the agent wrote its lines. The child applies the same rule to every
|
|
208
|
+
// failure's stack (scriptFrames), reading each line in place.
|
|
209
|
+
const scriptFrame = ${SCRIPT_FRAME};
|
|
210
|
+
const scriptPlace = ${SCRIPT_PLACE};
|
|
211
|
+
function errorText(error) {
|
|
212
|
+
const head = error.message ? String(error.name) + ": " + String(error.message) : String(error.name);
|
|
213
|
+
const stack = typeof error.stack === "string" ? error.stack : "";
|
|
214
|
+
const frames = stack.split("\\n")
|
|
215
|
+
.filter((line) => scriptFrame.test(line))
|
|
216
|
+
.map((line) => line.replace(scriptPlace, (m, n, rest) => "script:" + Math.max(1, Number(n) - scriptOffset) + rest));
|
|
217
|
+
return [head, ...frames].join("\\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function format(value) {
|
|
221
|
+
if (typeof value === "string") return value;
|
|
222
|
+
if (value instanceof ErrorCtor) return errorText(value);
|
|
223
|
+
try {
|
|
224
|
+
const json = stringify(value);
|
|
225
|
+
return json === undefined ? String(value) : json;
|
|
226
|
+
} catch {
|
|
227
|
+
return String(value);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function write(stream, value) {
|
|
232
|
+
let rendered;
|
|
233
|
+
try {
|
|
234
|
+
rendered = format(value);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw new TypeErrorCtor(error instanceof ErrorCtor ? error.message : String(error));
|
|
237
|
+
}
|
|
238
|
+
out(stream, rendered + "\\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A refusal before dispatch arrives as its JSON, { name, message, ...fields }.
|
|
242
|
+
function asError(reason) {
|
|
243
|
+
if (reason instanceof ErrorCtor) return reason;
|
|
244
|
+
const fields = reason !== null && typeof reason === "object" ? reason : { message: String(reason) };
|
|
245
|
+
const error = new ErrorCtor(typeof fields.message === "string" ? fields.message : "the call was refused");
|
|
246
|
+
for (const key of Object.keys(fields)) {
|
|
247
|
+
if (key === "name" || key === "message" || key === "stack") continue;
|
|
248
|
+
Object.defineProperty(error, key, { value: fields[key], writable: true, enumerable: true, configurable: true });
|
|
249
|
+
}
|
|
250
|
+
if (typeof fields.name === "string" && fields.name) error.name = fields.name;
|
|
251
|
+
return error;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function toJson(name, args) {
|
|
255
|
+
let json;
|
|
256
|
+
try {
|
|
257
|
+
json = stringify(args);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new TypeErrorCtor(name + "'s arguments aren't JSON (" + (error instanceof ErrorCtor ? error.message : String(error)) + "); nothing was sent");
|
|
260
|
+
}
|
|
261
|
+
if (json === undefined) throw new TypeErrorCtor(name + "'s arguments aren't JSON; nothing was sent");
|
|
262
|
+
return json;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// At most maxInFlight of the script's tool calls are out at once. One past
|
|
266
|
+
// them waits here, in the script's own memory, until one is answered, and
|
|
267
|
+
// its place passes straight to it: never refused, only paced.
|
|
268
|
+
const maxInFlight = ${MAX_CALLS_IN_FLIGHT};
|
|
269
|
+
let inFlight = 0;
|
|
270
|
+
const waiting = [];
|
|
271
|
+
function release() {
|
|
272
|
+
const next = waiting.shift();
|
|
273
|
+
if (next) next();
|
|
274
|
+
else inFlight -= 1;
|
|
275
|
+
}
|
|
276
|
+
function call(name, args) {
|
|
277
|
+
let json;
|
|
278
|
+
try {
|
|
279
|
+
json = args === undefined ? "{}" : toJson(name, args);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return Promise.reject(error);
|
|
282
|
+
}
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
const go = () => {
|
|
285
|
+
let pending;
|
|
286
|
+
try {
|
|
287
|
+
pending = host(name, json);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
release();
|
|
290
|
+
reject(asError(error));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
pending.then(
|
|
294
|
+
(value) => { release(); resolve(value); },
|
|
295
|
+
(reason) => { release(); reject(asError(reason)); },
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
if (inFlight < maxInFlight) {
|
|
299
|
+
inFlight += 1;
|
|
300
|
+
go();
|
|
301
|
+
} else {
|
|
302
|
+
waiting.push(go);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// wake_agent() takes no place among the calls in flight. The child refuses
|
|
308
|
+
// a payload too large, and a wake past the script's cap, before any goes out.
|
|
309
|
+
function wake(payload) {
|
|
310
|
+
let json;
|
|
311
|
+
try {
|
|
312
|
+
json = toJson("wake_agent()", payload === undefined ? null : payload);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return Promise.reject(error);
|
|
315
|
+
}
|
|
316
|
+
return host(${JSON.stringify(WAKE_CALL_NAME)}, json).then((value) => value, (reason) => { throw asError(reason); });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const tools = Object.create(null);
|
|
320
|
+
const allTools = [];
|
|
321
|
+
for (const { name, description, inputSchema } of toolList) {
|
|
322
|
+
if (name in tools) continue;
|
|
323
|
+
tools[name] = (args) => call(name, args);
|
|
324
|
+
allTools.push(Object.freeze({ name, description, inputSchema }));
|
|
325
|
+
}
|
|
326
|
+
Object.freeze(tools);
|
|
327
|
+
Object.freeze(allTools);
|
|
328
|
+
|
|
329
|
+
// A name that isn't a tool throws at once, naming the close matches, rather
|
|
330
|
+
// than failing later as "not a function". \`in\` still answers false.
|
|
331
|
+
const comparable = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
332
|
+
const names = allTools.map((tool) => tool.name);
|
|
333
|
+
const toolsProxy = new Proxy(tools, {
|
|
334
|
+
get(object, property, receiver) {
|
|
335
|
+
if (typeof property !== "string" || property in object || property in Object.prototype || property === "then" || property === "toJSON") {
|
|
336
|
+
return Reflect.get(object, property, receiver);
|
|
337
|
+
}
|
|
338
|
+
const wanted = comparable(property);
|
|
339
|
+
const exact = names.filter((name) => comparable(name) === wanted);
|
|
340
|
+
const close = exact.length > 0 ? exact : names.filter((name) => wanted && (comparable(name).includes(wanted) || wanted.includes(comparable(name))));
|
|
341
|
+
let message = "tools[" + stringify(property) + "] is not one of your tools.";
|
|
342
|
+
if (close.length > 0) message += " Did you mean " + close.slice(0, 5).map((name) => "tools[" + stringify(name) + "]").join(", ") + "?";
|
|
343
|
+
else if (names.length === 0) message += " You have no tools.";
|
|
344
|
+
else if (names.length <= 20) message += " Your tools: " + names.join(", ") + ".";
|
|
345
|
+
else message += " ALL_TOOLS lists them.";
|
|
346
|
+
message += " Nothing was sent.";
|
|
347
|
+
throw new TypeErrorCtor(message);
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const consoleObject = {};
|
|
352
|
+
for (const level of ["log", "info", "debug"]) consoleObject[level] = (...args) => out(1, args.map(format).join(" ") + "\\n");
|
|
353
|
+
for (const level of ["warn", "error"]) consoleObject[level] = (...args) => out(2, args.map(format).join(" ") + "\\n");
|
|
354
|
+
Object.freeze(consoleObject);
|
|
355
|
+
|
|
356
|
+
const AsyncFunction = (async function () {}).constructor;
|
|
357
|
+
// The script's value, as the JSON text of what it returned, serialized here
|
|
358
|
+
// once: its toJSON and getters run once, and the engine serializes this text
|
|
359
|
+
// as a string, which runs no script code however often it's read. A value
|
|
360
|
+
// JSON can't hold, such as a function, a cycle or a BigInt, is its string form.
|
|
361
|
+
async function run(src) {
|
|
362
|
+
let value;
|
|
363
|
+
try {
|
|
364
|
+
const fn = new AsyncFunction(src);
|
|
365
|
+
value = await fn.call(undefined);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
// An Error goes as it is, a syntax error's included: the child puts its
|
|
368
|
+
// frames as the agent reads them (scriptFrames). Anything else thrown,
|
|
369
|
+
// such as a plain object or a string, fails the script with its JSON or
|
|
370
|
+
// its string form as the message, so the agent reads what was thrown;
|
|
371
|
+
// its stack, made here, holds no frame of the script's.
|
|
372
|
+
if (error instanceof ErrorCtor) throw error;
|
|
373
|
+
const failure = new ErrorCtor(format(error));
|
|
374
|
+
failure.name = "";
|
|
375
|
+
throw failure;
|
|
376
|
+
}
|
|
377
|
+
if (value === undefined) return undefined;
|
|
378
|
+
let json;
|
|
379
|
+
try {
|
|
380
|
+
json = stringify(value);
|
|
381
|
+
} catch {
|
|
382
|
+
json = undefined;
|
|
383
|
+
}
|
|
384
|
+
if (json !== undefined) return json;
|
|
385
|
+
// Said as a string, so the agent reads that the value itself didn't come back.
|
|
386
|
+
let text;
|
|
387
|
+
try {
|
|
388
|
+
text = "JSON can't hold the returned value, so this is its string form: " + String(value);
|
|
389
|
+
} catch {
|
|
390
|
+
text = "JSON can't hold the returned value, and it has no string form";
|
|
391
|
+
}
|
|
392
|
+
return stringify(text);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
Object.defineProperty(globalThis, "tools", { value: toolsProxy, enumerable: true });
|
|
396
|
+
Object.defineProperty(globalThis, "ALL_TOOLS", { value: allTools, enumerable: true });
|
|
397
|
+
Object.defineProperty(globalThis, "console", { value: consoleObject, enumerable: true, configurable: true });
|
|
398
|
+
Object.defineProperty(globalThis, "text", { value: (value) => write(1, value), enumerable: true });
|
|
399
|
+
Object.defineProperty(globalThis, "wake_agent", { value: wake, enumerable: true });
|
|
400
|
+
Object.defineProperty(globalThis, "__run", { value: run });
|
|
401
|
+
try { delete globalThis.__host; delete globalThis.__output; } catch {}
|
|
402
|
+
})(globalThis.__host, globalThis.__output, JSON.parse(${JSON.stringify(toolsJson)}));
|
|
403
|
+
`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The one line `start` evaluates after the prelude: the script, as a string literal, handed to `__run`. */
|
|
407
|
+
export function scriptCall(script: string): string {
|
|
408
|
+
return `await __run(${JSON.stringify(script)});`;
|
|
409
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The messages between a script's parent and its child, and the checks that
|
|
3
|
+
* turn a decoded frame from the child into one of them. Every frame from a
|
|
4
|
+
* child is untrusted: the parent acts only on a
|
|
5
|
+
* message `decodeChildMessage` accepts, and any other ends the script as
|
|
6
|
+
* crashed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { countJsonValues } from './frames.js';
|
|
10
|
+
import type { PreludeTool } from './prelude.js';
|
|
11
|
+
|
|
12
|
+
export { countJsonValues };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The most tool calls a script has out at once, from the call until the
|
|
16
|
+
* script has its answer. One past them waits in the script, in its own memory,
|
|
17
|
+
* until one is answered, and is never refused, so a `Promise.all` over many
|
|
18
|
+
* calls still works, only paced, and what waits is the script's to hold. A
|
|
19
|
+
* child that sends more crashes its run. `wake_agent()` takes no place: its
|
|
20
|
+
* answer holds nothing, and its own cap bounds it.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_CALLS_IN_FLIGHT = 16;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The most JSON a tool call's arguments may be, in UTF-8 bytes: room for a
|
|
26
|
+
* generated file of a few MB of ordinary text as one string (escaping takes
|
|
27
|
+
* more room for quotes, backslashes and control characters). With MAX_ARGUMENT_VALUES it bounds
|
|
28
|
+
* what the host holds for a call while it runs: its arguments' text and their
|
|
29
|
+
* parsed value, at most about twice these bytes each (a string the runtime
|
|
30
|
+
* keeps in two bytes a character) and about 92 bytes a value, the most a
|
|
31
|
+
* value cost in a parse under Node 24 and Bun 1.4.
|
|
32
|
+
*/
|
|
33
|
+
export const MAX_ARGUMENT_BYTES = 4 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
/** The most values a tool call's arguments may hold: each object, array and element counts. */
|
|
36
|
+
export const MAX_ARGUMENT_VALUES = 100_000;
|
|
37
|
+
|
|
38
|
+
/** The most JSON a `wake_agent()` payload may be, in UTF-8 bytes: a wake says what happened, and a longer result is written with a tool. */
|
|
39
|
+
export const MAX_WAKE_PAYLOAD_BYTES = 64 * 1024;
|
|
40
|
+
|
|
41
|
+
/** A cap as the tool's text says it, such as 4 MiB. */
|
|
42
|
+
export const capText = (bytes: number): string => (bytes % (1024 * 1024) === 0 ? `${bytes / (1024 * 1024)} MiB` : `${bytes / 1024} KiB`);
|
|
43
|
+
|
|
44
|
+
/** A `wake_agent()` payload the host won't take, said to the script: nothing was sent. Null when it may go out. */
|
|
45
|
+
export function wakeRefusal(payloadJson: string): { name: 'WakeRefused'; message: string; bytes: number } | null {
|
|
46
|
+
const bytes = Buffer.byteLength(payloadJson, 'utf8');
|
|
47
|
+
if (bytes <= MAX_WAKE_PAYLOAD_BYTES) return null;
|
|
48
|
+
return {
|
|
49
|
+
name: 'WakeRefused',
|
|
50
|
+
message: `wake_agent()'s payload is ${bytes} bytes of JSON, past the ${capText(MAX_WAKE_PAYLOAD_BYTES)} a wake can carry; nothing was sent. Write a longer result with a tool, and wake with where to find it.`,
|
|
51
|
+
bytes,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The longest frame a child sends: a call frame carries its arguments escaped,
|
|
57
|
+
* at most six bytes for each of the 4 MiB the script may send, with room for
|
|
58
|
+
* its name and the rest. The child encodes its frames against it, and the
|
|
59
|
+
* parent refuses a longer one before its body is kept, so one frame being
|
|
60
|
+
* read costs the host at most this much as bytes, and its text and its parse
|
|
61
|
+
* up to twice as much again each.
|
|
62
|
+
*/
|
|
63
|
+
export const MAX_CHILD_FRAME_BYTES = 6 * MAX_ARGUMENT_BYTES + 1024 * 1024;
|
|
64
|
+
|
|
65
|
+
/** A wake past the script's cap, said to the script: nothing was sent. */
|
|
66
|
+
export function wakeLimitRefusal(maxWakes: number): { name: 'WakeRefused'; message: string } {
|
|
67
|
+
// A cap of 0 turns wakes off for every script, so a new one can't either.
|
|
68
|
+
if (maxWakes === 0) {
|
|
69
|
+
return {
|
|
70
|
+
name: 'WakeRefused',
|
|
71
|
+
message: "wake_agent() is off on this host: its operator set maxWakesPerScript to 0, so nothing was sent, and no script here can wake its agent.",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: 'WakeRefused',
|
|
76
|
+
message:
|
|
77
|
+
`wake limit reached: ${maxWakes} per script, set by the host's maxWakesPerScript; nothing was sent, and every later ` +
|
|
78
|
+
'wake_agent() in this script is refused the same way. A new script can wake its agent again.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A call's arguments the host won't take, said to the script: nothing was sent. Null when they may go out. */
|
|
83
|
+
export function argumentsRefusal(tool: string, argsJson: string): { name: 'ArgumentsTooLarge'; message: string; bytes: number } | null {
|
|
84
|
+
const bytes = Buffer.byteLength(argsJson, 'utf8');
|
|
85
|
+
if (bytes > MAX_ARGUMENT_BYTES) {
|
|
86
|
+
return {
|
|
87
|
+
name: 'ArgumentsTooLarge',
|
|
88
|
+
message: `${tool}'s arguments are ${bytes} bytes of JSON, past the ${capText(MAX_ARGUMENT_BYTES)} a call can take; nothing was sent. Send the data in parts, over several calls.`,
|
|
89
|
+
bytes,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (countJsonValues(argsJson, MAX_ARGUMENT_VALUES) > MAX_ARGUMENT_VALUES) {
|
|
93
|
+
return {
|
|
94
|
+
name: 'ArgumentsTooLarge',
|
|
95
|
+
message: `${tool}'s arguments hold more than ${MAX_ARGUMENT_VALUES.toLocaleString('en-US')} values, the most a call can take; nothing was sent. Send the data in parts, or as one string.`,
|
|
96
|
+
bytes,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The error a script gets for a call whose result was too large for its memory: the call took effect. */
|
|
103
|
+
export const RESULT_TOO_LARGE = 'ResultTooLarge';
|
|
104
|
+
|
|
105
|
+
/** Why a stop was asked for: by the agent, or at the script's limit. Only the parent knows it. */
|
|
106
|
+
export type StopReason = 'agent' | 'limit';
|
|
107
|
+
|
|
108
|
+
/** The first frame to the child: the script and everything it runs with. */
|
|
109
|
+
export interface StartMessage {
|
|
110
|
+
t: 'start';
|
|
111
|
+
script: string;
|
|
112
|
+
tools: PreludeTool[];
|
|
113
|
+
/** The WebAssembly memory's maximum: the child's budget. */
|
|
114
|
+
memoryMaxBytes: number;
|
|
115
|
+
maxStackBytes?: number;
|
|
116
|
+
/** The file whose appearance stops the script: its content says nothing. */
|
|
117
|
+
stopFile: string;
|
|
118
|
+
/** How often, at most, waiting output is flushed while the guest runs. */
|
|
119
|
+
flushMs: number;
|
|
120
|
+
/** The most wakes the script may send: the child refuses one past them itself. */
|
|
121
|
+
maxWakes: number;
|
|
122
|
+
/** The parent's pid: the child ends its process once its parent is another, its parent having gone. */
|
|
123
|
+
parentPid: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A tool call's answer, as the child decodes a result frame: `ok` resolves the
|
|
128
|
+
* guest's promise with the parsed `json`, otherwise rejects with it. The JSON
|
|
129
|
+
* stays bytes, for the engine to parse in the guest.
|
|
130
|
+
*/
|
|
131
|
+
export type { ResultFrame as ResultMessage } from './frames.js';
|
|
132
|
+
import type { ResultFrame } from './frames.js';
|
|
133
|
+
|
|
134
|
+
/** A stop for a child whose guest is idle, waiting on calls; a computing guest sees the stop file instead. */
|
|
135
|
+
export interface StopMessage {
|
|
136
|
+
t: 'stop';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type ParentMessage = StartMessage | ResultFrame | StopMessage;
|
|
140
|
+
|
|
141
|
+
/** The guest called a tool, or `wake_agent()`, by the prelude's call name. */
|
|
142
|
+
export interface CallMessage {
|
|
143
|
+
t: 'call';
|
|
144
|
+
id: number;
|
|
145
|
+
name: string;
|
|
146
|
+
/** The arguments as the guest's JSON text. */
|
|
147
|
+
args: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Output the guest wrote since the last such frame. */
|
|
151
|
+
export interface OutMessage {
|
|
152
|
+
t: 'out';
|
|
153
|
+
text: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** An error as the guest describes it. */
|
|
157
|
+
export interface GuestError {
|
|
158
|
+
name: string;
|
|
159
|
+
message: string;
|
|
160
|
+
/** The script's frames, one per line, as the agent reads them (scriptFrames): the name and message aren't in it. */
|
|
161
|
+
stack?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The engine settled a call's result, so its place is free. `unfit` when the
|
|
166
|
+
* result didn't fit the guest's memory, so the script got ResultTooLarge in
|
|
167
|
+
* its place. The parent's record counts a call as the script saw it.
|
|
168
|
+
*/
|
|
169
|
+
export interface SeenMessage {
|
|
170
|
+
t: 'seen';
|
|
171
|
+
id: number;
|
|
172
|
+
unfit?: true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* How the script ended, as the child saw it. A crash is the parent's to see:
|
|
177
|
+
* the child can't report it. A stop's reason is the parent's too, since it
|
|
178
|
+
* asked for the stop.
|
|
179
|
+
*/
|
|
180
|
+
export type EndMessage =
|
|
181
|
+
| { t: 'end'; how: 'finished'; value?: string }
|
|
182
|
+
| { t: 'end'; how: 'failed'; error: GuestError; stalled?: boolean }
|
|
183
|
+
| { t: 'end'; how: 'stopped' };
|
|
184
|
+
|
|
185
|
+
export type ChildMessage = CallMessage | OutMessage | SeenMessage | EndMessage;
|
|
186
|
+
|
|
187
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
188
|
+
/** A call's id: a result frame carries it as an unsigned 32-bit number, so a larger one couldn't name its call. */
|
|
189
|
+
const isCallId = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 0xffffffff;
|
|
190
|
+
|
|
191
|
+
/** The longest tool name the parent takes from a child, in UTF-16 code units. Providers allow 64; this leaves room and bounds the error text. */
|
|
192
|
+
const MAX_NAME_CHARS = 256;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A frame from a child, as a message the parent may act on, or the reason it
|
|
196
|
+
* can't be one. Fields beyond those a message has are ignored.
|
|
197
|
+
*/
|
|
198
|
+
export function decodeChildMessage(value: unknown): ChildMessage | { invalid: string } {
|
|
199
|
+
if (!isRecord(value)) return { invalid: 'a frame from the child was not an object' };
|
|
200
|
+
switch (value.t) {
|
|
201
|
+
case 'call': {
|
|
202
|
+
if (!isCallId(value.id)) return { invalid: "a call's id was not a whole number from 0 to 4294967295" };
|
|
203
|
+
if (typeof value.name !== 'string' || value.name.length === 0 || value.name.length > MAX_NAME_CHARS) {
|
|
204
|
+
return { invalid: "a call's name was not a string of 1 to 256 UTF-16 code units" };
|
|
205
|
+
}
|
|
206
|
+
if (typeof value.args !== 'string') return { invalid: "a call's arguments were not text" };
|
|
207
|
+
return { t: 'call', id: value.id, name: value.name, args: value.args };
|
|
208
|
+
}
|
|
209
|
+
case 'seen': {
|
|
210
|
+
if (!isCallId(value.id)) return { invalid: "a seen frame's id was not a whole number from 0 to 4294967295" };
|
|
211
|
+
return value.unfit === true ? { t: 'seen', id: value.id, unfit: true } : { t: 'seen', id: value.id };
|
|
212
|
+
}
|
|
213
|
+
case 'out': {
|
|
214
|
+
if (typeof value.text !== 'string') return { invalid: 'an output frame did not hold text' };
|
|
215
|
+
return { t: 'out', text: value.text };
|
|
216
|
+
}
|
|
217
|
+
case 'end': {
|
|
218
|
+
if (value.how === 'finished') {
|
|
219
|
+
if (value.value !== undefined && typeof value.value !== 'string') return { invalid: "a finished script's value was not JSON text" };
|
|
220
|
+
return value.value === undefined ? { t: 'end', how: 'finished' } : { t: 'end', how: 'finished', value: value.value };
|
|
221
|
+
}
|
|
222
|
+
if (value.how === 'failed') {
|
|
223
|
+
const error = value.error;
|
|
224
|
+
if (!isRecord(error) || typeof error.name !== 'string' || typeof error.message !== 'string' || (error.stack !== undefined && typeof error.stack !== 'string')) {
|
|
225
|
+
return { invalid: "a failed script's error did not hold a name and a message" };
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
t: 'end',
|
|
229
|
+
how: 'failed',
|
|
230
|
+
error: { name: error.name, message: error.message, ...(error.stack === undefined ? {} : { stack: error.stack }) },
|
|
231
|
+
...(value.stalled === true ? { stalled: true } : {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (value.how === 'stopped') return { t: 'end', how: 'stopped' };
|
|
235
|
+
return { invalid: 'an end frame named no ending' };
|
|
236
|
+
}
|
|
237
|
+
default:
|
|
238
|
+
return { invalid: 'a frame from the child named no message' };
|
|
239
|
+
}
|
|
240
|
+
}
|