@animalabs/agent-framework 0.26.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -8
- package/dist/src/agent.d.ts +291 -23
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +596 -61
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +5 -4
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +34 -0
- package/dist/src/auto-continue.d.ts.map +1 -0
- package/dist/src/auto-continue.js +36 -0
- package/dist/src/auto-continue.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -1
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +1038 -193
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +5967 -1514
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +405 -46
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +2646 -399
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts +103 -0
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
- package/dist/src/gate/recipe-reconcile.js +325 -0
- package/dist/src/gate/recipe-reconcile.js.map +1 -0
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +229 -8
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +184 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +752 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +433 -113
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1154 -435
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +16 -8
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +29 -15
- 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 +14 -2
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +32 -7
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +26 -10
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +40 -69
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +4 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +14 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +73 -28
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +6 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +3 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +20 -2
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +33 -11
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +177 -145
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +380 -46
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +736 -77
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +93 -4
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +176 -11
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +21 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +311 -0
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
- package/dist/src/mcpl/tool-presence.js +697 -0
- package/dist/src/mcpl/tool-presence.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js +14 -6
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +46 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
- package/dist/src/mcpl/tool-signature.js +321 -0
- package/dist/src/mcpl/tool-signature.js.map +1 -0
- package/dist/src/mcpl/toolset-host.d.ts +251 -0
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
- package/dist/src/mcpl/toolset-host.js +720 -0
- package/dist/src/mcpl/toolset-host.js.map +1 -0
- package/dist/src/mcpl/toolsets-module.d.ts +83 -0
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
- package/dist/src/mcpl/toolsets-module.js +523 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +2 -2
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +3 -2
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +24 -2
- 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 +7 -9
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +21 -9
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +27 -9
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +77 -17
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +2 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +73 -28
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +108 -6
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +273 -57
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/answers.d.ts +22 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +123 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +184 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
- package/dist/src/modules/workspace/direct-write.js +417 -0
- package/dist/src/modules/workspace/direct-write.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +27 -12
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +61 -29
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +10 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +21 -14
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +308 -78
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2103 -720
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +75 -63
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +366 -180
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts +41 -0
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
- package/dist/src/modules/workspace/read-page.js +219 -0
- package/dist/src/modules/workspace/read-page.js.map +1 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +26 -16
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +68 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/result-text.js +94 -0
- package/dist/src/modules/workspace/result-text.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/text.d.ts +29 -12
- package/dist/src/modules/workspace/text.d.ts.map +1 -1
- package/dist/src/modules/workspace/text.js +35 -33
- package/dist/src/modules/workspace/text.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +137 -60
- 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.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +50 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.d.ts +59 -0
- package/dist/src/sent-rows.d.ts.map +1 -0
- package/dist/src/sent-rows.js +145 -0
- package/dist/src/sent-rows.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +17 -10
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stable-hash.d.ts +3 -0
- package/dist/src/stable-hash.d.ts.map +1 -0
- package/dist/src/stable-hash.js +15 -0
- package/dist/src/stable-hash.js.map +1 -0
- package/dist/src/stored-injections.d.ts +99 -0
- package/dist/src/stored-injections.d.ts.map +1 -0
- package/dist/src/stored-injections.js +106 -0
- package/dist/src/stored-injections.js.map +1 -0
- package/dist/src/text-as-stored.d.ts +4 -3
- package/dist/src/text-as-stored.d.ts.map +1 -1
- package/dist/src/text-as-stored.js +4 -3
- package/dist/src/text-as-stored.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +74 -0
- package/dist/src/thinking-drops.d.ts.map +1 -0
- package/dist/src/thinking-drops.js +96 -0
- package/dist/src/thinking-drops.js.map +1 -0
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +35 -1
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts +9 -8
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +9 -8
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +3 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +12 -6
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +44 -17
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +176 -24
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +411 -91
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +17 -9
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +84 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +217 -7
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +33 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +39 -3
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +95 -9
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +91 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/agent.ts +661 -67
- package/src/api/server.ts +5 -4
- package/src/auto-continue.ts +43 -0
- package/src/code-execution/isolate/scripts.ts +11 -3
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +6168 -1540
- package/src/gate/event-gate.ts +2895 -405
- package/src/gate/index.ts +10 -1
- package/src/gate/recipe-reconcile.ts +355 -0
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +234 -9
- package/src/heartbeat/heartbeat.ts +804 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/index.ts +11 -4
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +1222 -468
- package/src/mcpl/conversation-router.ts +34 -16
- package/src/mcpl/convert-block.ts +14 -1
- package/src/mcpl/hook-orchestrator.ts +33 -6
- package/src/mcpl/inbound-source.ts +42 -69
- package/src/mcpl/index.ts +6 -1
- package/src/mcpl/inference-router.ts +79 -34
- package/src/mcpl/prose-grammar.ts +7 -0
- package/src/mcpl/protocol-family.ts +2 -0
- package/src/mcpl/push-coalescer.ts +51 -13
- package/src/mcpl/reference-fetcher.ts +201 -162
- package/src/mcpl/references.ts +851 -77
- package/src/mcpl/server-connection.ts +189 -11
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-lifecycle.ts +21 -0
- package/src/mcpl/tool-presence.ts +716 -0
- package/src/mcpl/tool-result-normalize.ts +18 -6
- package/src/mcpl/tool-signature.ts +316 -0
- package/src/mcpl/toolset-host.ts +727 -0
- package/src/mcpl/toolsets-module.ts +545 -0
- package/src/mcpl/transport.ts +3 -2
- package/src/mcpl/types.ts +25 -2
- package/src/mcpl/visible-content.ts +22 -9
- package/src/module-registry.ts +85 -26
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +70 -30
- package/src/modules/history/semantic.ts +308 -54
- package/src/modules/workspace/answers.ts +133 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +460 -0
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +84 -35
- package/src/modules/workspace/effects.ts +22 -13
- package/src/modules/workspace/index.ts +2142 -723
- package/src/modules/workspace/observe.ts +381 -177
- package/src/modules/workspace/read-page.ts +228 -0
- package/src/modules/workspace/reconcile.ts +27 -15
- package/src/modules/workspace/result-text.ts +121 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/text.ts +58 -32
- package/src/modules/workspace/types.ts +137 -60
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +62 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +162 -0
- package/src/silent-heartbeat.ts +17 -10
- package/src/stable-hash.ts +14 -0
- package/src/stored-injections.ts +131 -0
- package/src/text-as-stored.ts +4 -3
- package/src/thinking-drops.ts +135 -0
- package/src/tool-args.ts +34 -1
- package/src/tool-image-ledger.ts +9 -8
- package/src/tool-presentation.ts +3 -1
- package/src/tool-result-guard.ts +45 -17
- package/src/tool-result-history.ts +471 -90
- package/src/tune-out/coordinator.ts +80 -37
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +220 -7
- package/src/types/events.ts +33 -9
- package/src/types/framework.ts +36 -3
- package/src/types/module.ts +91 -9
- package/src/types/trace.ts +94 -3
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's own interface to MCPL dynamic tools (RFC-014 §8.2): four tools,
|
|
3
|
+
* constant and static, aggregated across every server that presents
|
|
4
|
+
* toolsets.
|
|
5
|
+
*
|
|
6
|
+
* toolsets--open {server?, toolset?, instance?, options?}
|
|
7
|
+
* toolsets--act {server?, tool, instance?, args?}
|
|
8
|
+
* toolsets--look {server?, toolset?, instance?, tool?}
|
|
9
|
+
* toolsets--close {server?, toolset?, instance?}
|
|
10
|
+
*
|
|
11
|
+
* Dynamic tools never enter the API tool list: changing it rebuilds the
|
|
12
|
+
* prompt cache. They're announced in the conversation (in these tools'
|
|
13
|
+
* results, in the results of calls that changed them, in push events and in
|
|
14
|
+
* notices) and called through `toolsets--act`, which this module only
|
|
15
|
+
* resolves (resolveAct): the framework dispatches the call it resolves to
|
|
16
|
+
* down the same path as a direct call of the server's tool. The module is registered when a server is
|
|
17
|
+
* configured with `toolHints: true`; its tools never change after that.
|
|
18
|
+
*
|
|
19
|
+
* Its gatherContext publishes the RFC §8.3 reminder, as a stored `afterUser`
|
|
20
|
+
* injection, when an agent's earlier announcements of an open toolset have
|
|
21
|
+
* left its context (ToolsetHub.observeCompile).
|
|
22
|
+
*
|
|
23
|
+
* Ported from mcpl-cc-bridge's meta-tools.ts, the reference host.
|
|
24
|
+
*/
|
|
25
|
+
import type { EventResponse, Module, ModuleContext, ModuleContextInjection, ProcessState } from '../types/module.js';
|
|
26
|
+
import type { ProcessEvent, ToolCall, ToolDefinition, ToolResult } from '../types/events.js';
|
|
27
|
+
import { unknownToolMessage } from '../modules/unknown-tool.js';
|
|
28
|
+
import { META_TOOL_NAMES, TOOLSETS_MODULE, type ToolsetController, type ToolsetHub } from './toolset-host.js';
|
|
29
|
+
import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
|
|
30
|
+
import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
|
|
31
|
+
import { McplRequestError } from './server-connection.js';
|
|
32
|
+
|
|
33
|
+
/** The namespace a reminder's stored injection carries. */
|
|
34
|
+
export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
|
|
35
|
+
|
|
36
|
+
const META_TOOLS: ToolDefinition[] = [
|
|
37
|
+
{
|
|
38
|
+
name: 'open',
|
|
39
|
+
description:
|
|
40
|
+
'Open a toolset of an MCPL server (a game, a building mode, a card table) so its tools can be called with toolsets--act. ' +
|
|
41
|
+
'Returns what the server shows on opening and the tools available now. server may be left out when one server has the toolset, ' +
|
|
42
|
+
'and toolset when the server has only one; instance names a separate copy where a toolset allows several (two games at once); ' +
|
|
43
|
+
'options as toolsets--look lists them.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
server: { type: 'string', description: 'the MCPL server id' },
|
|
48
|
+
toolset: { type: 'string' },
|
|
49
|
+
instance: { type: 'string', description: 'a name for this copy (toolsets with instances only)' },
|
|
50
|
+
options: { type: 'object', description: 'toolset options, as toolsets--look shows them' },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'act',
|
|
56
|
+
description:
|
|
57
|
+
'Call a tool that was announced in the conversation (a tool of an open toolset, or another dynamic tool); they are not in your tool list. ' +
|
|
58
|
+
'args are the arguments its signature shows; instance picks the open copy when several are open; server is needed only when two servers have the tool. ' +
|
|
59
|
+
'Returns the server\'s result, then any change it made in which tools are available, unless that change already went out as a notice. toolsets--look shows what is available now.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
server: { type: 'string', description: 'the MCPL server id (only needed when two servers have the tool)' },
|
|
64
|
+
tool: { type: 'string' },
|
|
65
|
+
instance: { type: 'string', description: 'which open copy (only needed when several are open)' },
|
|
66
|
+
args: { type: 'object' },
|
|
67
|
+
},
|
|
68
|
+
required: ['tool'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'look',
|
|
73
|
+
description:
|
|
74
|
+
'Show what is open and what can be opened on MCPL servers, with the tools usable now: announcements earlier in the conversation may be out of date. ' +
|
|
75
|
+
'With tool: that tool\'s full description and input schema. With toolset or instance: just that one.',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
server: { type: 'string' },
|
|
80
|
+
toolset: { type: 'string' },
|
|
81
|
+
instance: { type: 'string' },
|
|
82
|
+
tool: { type: 'string' },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'close',
|
|
88
|
+
description: 'Close an open toolset or instance: its tools stop, and for a toolset the server declares, the server is told and stops its events too. Closing a game in progress may count as leaving it.',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
server: { type: 'string', description: 'the MCPL server id (may be left out when one server has the toolset open)' },
|
|
93
|
+
toolset: { type: 'string', description: 'may be left out when only one thing is open on the server' },
|
|
94
|
+
instance: { type: 'string' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
101
|
+
|
|
102
|
+
const SELECTORS = ['server', 'toolset', 'instance', 'tool'] as const;
|
|
103
|
+
|
|
104
|
+
function checkSelectors(args: Record<string, unknown>): void {
|
|
105
|
+
for (const key of SELECTORS) {
|
|
106
|
+
const v = args[key];
|
|
107
|
+
if (v === undefined || v === null || str(v) !== undefined) continue;
|
|
108
|
+
const got = typeof v === 'string' ? 'a blank string' : Array.isArray(v) ? 'a list' : typeof v === 'object' ? 'an object' : `a ${typeof v}`;
|
|
109
|
+
fail(`${key} must be a name, or left out; got ${got}, so nothing was done`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A failure whose outcome is unknown, with how to find it out. */
|
|
114
|
+
export function withLookHint(serverId: string, text: string): string {
|
|
115
|
+
const said = text.trimEnd();
|
|
116
|
+
return `${said}${/[.!?]$/.test(said) ? '' : '.'} ${META_TOOL_NAMES.look} {server:"${serverId}"} asks it what's open, or says it couldn't.`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function requestFailure(serverId: string, did: string, unconfirmed: string, error: unknown): never {
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
if (error instanceof McplRequestError && error.outcome === 'not-sent') return fail(`${serverId} ${did}: ${message}`);
|
|
122
|
+
return fail(withLookHint(serverId, `${serverId} ${unconfirmed}: ${message}`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A result's tool change, for whoever shows it with the result (ToolsetsModule's takerFor). */
|
|
126
|
+
export type TakenChange = { toolsets: ToolsetController; cs: ChangeSet };
|
|
127
|
+
|
|
128
|
+
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
129
|
+
|
|
130
|
+
type Block = { type: string; text?: string; [k: string]: unknown };
|
|
131
|
+
|
|
132
|
+
class MetaError extends Error {}
|
|
133
|
+
const fail = (message: string): never => {
|
|
134
|
+
throw new MetaError(message);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** A text-only result. */
|
|
138
|
+
const textResult = (text: string): ToolResult => ({ success: true, data: text });
|
|
139
|
+
|
|
140
|
+
/** Adjacent text blocks merged, so a result reads as one message. */
|
|
141
|
+
function mergeText(blocks: Block[]): Block[] {
|
|
142
|
+
const out: Block[] = [];
|
|
143
|
+
for (const b of blocks) {
|
|
144
|
+
if (b.type === 'text' && !b.text) continue;
|
|
145
|
+
const prev = out[out.length - 1];
|
|
146
|
+
if (b.type === 'text' && prev?.type === 'text') out[out.length - 1] = { type: 'text', text: `${prev.text}\n${b.text}` };
|
|
147
|
+
else out.push(b);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Content blocks as a result: content when anything isn't text, else its text. */
|
|
153
|
+
function blocksResult(blocks: Block[], isError = false): ToolResult {
|
|
154
|
+
const merged = mergeText(blocks);
|
|
155
|
+
if (isError) {
|
|
156
|
+
const text = merged.map((b) => (b.type === 'text' ? b.text ?? '' : `[${b.type}]`)).join('\n');
|
|
157
|
+
return { success: false, isError: true, error: text || 'the server answered with an error' };
|
|
158
|
+
}
|
|
159
|
+
if (merged.every((b) => b.type === 'text')) return textResult(merged.map((b) => b.text ?? '').join('\n'));
|
|
160
|
+
return { success: true, data: merged, dataIsContent: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const asBlocks = (content: unknown[]): Block[] => content.filter((b): b is Block => isObj(b) && typeof b.type === 'string');
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// ── choosing the server ──
|
|
167
|
+
|
|
168
|
+
function pick(hub: ToolsetHub, server: unknown): ToolsetController {
|
|
169
|
+
const id = str(server);
|
|
170
|
+
const ids = hub.presenting().map((c) => c.serverId);
|
|
171
|
+
if (!id) {
|
|
172
|
+
if (ids.length === 1) return hub.get(ids[0]!)!;
|
|
173
|
+
return fail(ids.length ? `name a server: ${ids.join(', ')}` : NONE_PRESENTS);
|
|
174
|
+
}
|
|
175
|
+
const c = hub.get(id);
|
|
176
|
+
if (!c) return fail(`no server "${id}" presents toolsets; the ones that do: ${ids.join(', ') || 'none'}`);
|
|
177
|
+
if (!c.presence.active) return fail(`${id} sends no tool hints now: its tools are called directly, by name, not through ${META_TOOL_NAMES.act}`);
|
|
178
|
+
return c;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* RFC §8.2: `server` may be left out; the name decides, when exactly one
|
|
183
|
+
* presenting server has it. Two or more: name the server.
|
|
184
|
+
*/
|
|
185
|
+
function owner(hub: ToolsetHub, server: unknown, has: (c: ToolsetController) => boolean, what: string): ToolsetController {
|
|
186
|
+
if (str(server)) return pick(hub, server);
|
|
187
|
+
const owners = hub.presenting().filter(has);
|
|
188
|
+
if (owners.length === 1) return owners[0]!;
|
|
189
|
+
if (owners.length) return fail(`${what} is on several servers (${owners.map((c) => c.serverId).join(', ')}): add server`);
|
|
190
|
+
return fail(hub.presenting().length ? `no server that presents toolsets has ${what}: ${META_TOOL_NAMES.look} shows what they have` : NONE_PRESENTS);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const NONE_PRESENTS = 'no MCPL server presents toolsets now: MCPL tools are called directly, by name';
|
|
194
|
+
|
|
195
|
+
function noToolset(c: ToolsetController, toolset: string): string {
|
|
196
|
+
const names = [...Object.keys(c.presence.declared), ...c.presence.undeclaredGroups];
|
|
197
|
+
return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.join(', ')}` : 'it has none'}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
|
|
201
|
+
export type ActTarget = { ok: true; serverId: string; tool: string; instance?: string; args: Record<string, unknown> } | { ok: false; error: string };
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve a `toolsets--act` call (RFC §8.2) to the dynamic tool it calls,
|
|
205
|
+
* or the reason it can't be sent. The framework then dispatches it exactly as
|
|
206
|
+
* a direct call of that tool, with `_meta["mcpl/instance"]` added (RFC §5.5).
|
|
207
|
+
* Refused here, before anything is sent: a tool no presenting server has, a
|
|
208
|
+
* tool neither the published list nor the server's latest tools/list puts as dynamic (RFC §4.2), an instance that isn't named where
|
|
209
|
+
* several are open, a group the host has closed (RFC §5.4), and arguments
|
|
210
|
+
* that don't fit the definition in force for that instance (RFC §6.1).
|
|
211
|
+
*/
|
|
212
|
+
const hasInstances = (p: ToolPresence, ts: string | undefined): boolean => ts !== undefined && p.isDeclared(ts) && !!p.declared[ts]!.instances;
|
|
213
|
+
|
|
214
|
+
/** The key whose definition act checks a call of `tool` against (RFC §6.1): an instance's own, else the toolset's, else the listed one. */
|
|
215
|
+
function actKey(p: ToolPresence, tool: string, instance: string | undefined): string {
|
|
216
|
+
const ts = p.toolsetOf(tool);
|
|
217
|
+
return ts ? keyOf(ts, hasInstances(p, ts) ? instance : undefined) : keyOf('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
|
|
221
|
+
try {
|
|
222
|
+
const args = isObj(input) ? input : {};
|
|
223
|
+
checkSelectors(args);
|
|
224
|
+
const tool = str(args.tool) ?? fail('name the tool');
|
|
225
|
+
const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
|
|
226
|
+
const p = c.presence;
|
|
227
|
+
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
|
|
228
|
+
if (!p.shownAs(tool, 'dynamic')) fail(`${tool} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
|
|
229
|
+
if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
|
|
230
|
+
const callArgs = (args.args ?? {}) as Record<string, unknown>;
|
|
231
|
+
let instance = str(args.instance);
|
|
232
|
+
const ts = p.toolsetOf(tool);
|
|
233
|
+
const declared = ts !== undefined && p.isDeclared(ts);
|
|
234
|
+
const withInstances = hasInstances(p, ts);
|
|
235
|
+
if (withInstances && !instance) {
|
|
236
|
+
// RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
|
|
237
|
+
const open = p.openRecords(ts);
|
|
238
|
+
if (open.length === 1) instance = open[0]!.instance;
|
|
239
|
+
else if (!open.length) fail(`${tool} belongs to ${ts}, which isn't open: ${p.openHint(ts!)}`);
|
|
240
|
+
else fail(`several ${ts} instances are open (${open.map((o) => o.instance).join(', ')}): say which with instance`);
|
|
241
|
+
}
|
|
242
|
+
if (instance && !withInstances) fail(`${tool} ${ts ? `belongs to ${ts}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
|
|
243
|
+
// RFC §5.4: the host gates calls into an undeclared group it has closed.
|
|
244
|
+
if (ts && !declared && !p.isOpen(ts)) fail(`${tool} is in ${ts}, which isn't open: ${p.openHint(ts)}`);
|
|
245
|
+
// RFC §6.1: arguments are checked against the definition in force for
|
|
246
|
+
// that instance: its own from hints (the legal moves of one game), else
|
|
247
|
+
// the toolset's, else the listed one.
|
|
248
|
+
const key = actKey(p, tool, instance);
|
|
249
|
+
const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
|
|
250
|
+
if (problem) {
|
|
251
|
+
fail(`Invalid arguments for ${tool} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
|
|
252
|
+
`${META_TOOL_NAMES.look} {server:"${c.serverId}", tool:"${tool}"${instance ? `, instance:"${instance}"` : ''}} shows its current definition.`);
|
|
253
|
+
}
|
|
254
|
+
return { ok: true, serverId: c.serverId, tool, ...(instance ? { instance } : {}), args: callArgs };
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof MetaError) return { ok: false, error: error.message };
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export class ToolsetsModule implements Module {
|
|
262
|
+
readonly name = TOOLSETS_MODULE;
|
|
263
|
+
|
|
264
|
+
/** `takerFor`: who shows a call's result to an agent, taking the tool
|
|
265
|
+
* change rendered into it; null when the result may reach none, and its
|
|
266
|
+
* change is left to a notice (RFC-014 §6.1: told once). */
|
|
267
|
+
constructor(
|
|
268
|
+
private readonly hub: ToolsetHub,
|
|
269
|
+
private readonly takerFor: (call: ToolCall) => ((taken: TakenChange) => void) | null,
|
|
270
|
+
) {}
|
|
271
|
+
|
|
272
|
+
async start(_ctx: ModuleContext): Promise<void> {}
|
|
273
|
+
async stop(): Promise<void> {}
|
|
274
|
+
|
|
275
|
+
getTools(): ToolDefinition[] {
|
|
276
|
+
return META_TOOLS;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async onProcess(_event: ProcessEvent, _state: ProcessState): Promise<EventResponse> {
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** RFC §8.3: the reminder due for this agent, stored where compile() would place it. */
|
|
284
|
+
async gatherContext(agentName: string): Promise<ModuleContextInjection[]> {
|
|
285
|
+
const reminder = this.hub.reminderFor(agentName);
|
|
286
|
+
if (!reminder) return [];
|
|
287
|
+
return [{ namespace: TOOLSETS_REMINDER_NAMESPACE, position: 'afterUser', content: [{ type: 'text', text: reminder }] }];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
291
|
+
const args = isObj(call.input) ? call.input : {};
|
|
292
|
+
try {
|
|
293
|
+
checkSelectors(args);
|
|
294
|
+
const take = this.takerFor(call);
|
|
295
|
+
switch (call.name) {
|
|
296
|
+
case 'open': return await this.open(args, take);
|
|
297
|
+
case 'act': return this.act(args);
|
|
298
|
+
case 'look': return await this.look(args, take);
|
|
299
|
+
case 'close': return await this.close(args, take);
|
|
300
|
+
default: return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, META_TOOLS) };
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const message = error instanceof MetaError
|
|
304
|
+
? error.message
|
|
305
|
+
: `${META_TOOL_NAMES[call.name as keyof typeof META_TOOL_NAMES] ?? call.name}: ${error instanceof Error ? error.message : String(error)}`;
|
|
306
|
+
return { success: false, isError: true, error: message };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private toolsAt(c: ToolsetController, key: string): HintedTool[] {
|
|
311
|
+
return [...(c.presence.view().get(key)?.tools.values() ?? [])];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A change set rendered without the key the caller already showed whole. */
|
|
315
|
+
private renderOthers(c: ToolsetController, cs: ChangeSet, key: string, shown: boolean): string {
|
|
316
|
+
return c.presence.render({ ...cs, changes: cs.changes.filter((ch) => ch.key !== key) }, { mark: shown, describeAll: !shown });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── the tools ──
|
|
320
|
+
|
|
321
|
+
private async open(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
322
|
+
const shown = take !== null;
|
|
323
|
+
let toolset = str(args.toolset);
|
|
324
|
+
const c = toolset && !str(args.server)
|
|
325
|
+
? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
|
|
326
|
+
: pick(this.hub, args.server);
|
|
327
|
+
const p = c.presence;
|
|
328
|
+
const openable = [...Object.keys(p.declared), ...p.undeclaredGroups];
|
|
329
|
+
if (!toolset) {
|
|
330
|
+
if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.join(', ')}` : 'no toolsets'}`);
|
|
331
|
+
toolset = openable[0]!;
|
|
332
|
+
}
|
|
333
|
+
const instance = str(args.instance);
|
|
334
|
+
if (args.options !== undefined && !isObj(args.options)) fail('options must be an object');
|
|
335
|
+
const options = args.options as Record<string, unknown> | undefined;
|
|
336
|
+
|
|
337
|
+
if (p.isDeclared(toolset)) {
|
|
338
|
+
if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
|
|
339
|
+
let opened: Awaited<ReturnType<ToolsetController['open']>>;
|
|
340
|
+
try {
|
|
341
|
+
opened = await c.open(toolset, instance, options, { shown });
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return requestFailure(c.serverId, `did not open ${toolset}`, `didn't confirm opening ${toolset}`, error);
|
|
344
|
+
}
|
|
345
|
+
// RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
|
|
346
|
+
const blocks: Block[] = [
|
|
347
|
+
{ type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
|
|
348
|
+
...asBlocks(opened.content),
|
|
349
|
+
];
|
|
350
|
+
const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
|
|
351
|
+
const gone = opened.change.changes.find((ch) => ch.key === opened.key)?.gone ?? [];
|
|
352
|
+
if (gone.length) tail.push(`now unavailable: ${gone.join(', ')}`);
|
|
353
|
+
const others = this.renderOthers(c, opened.change, opened.key, shown);
|
|
354
|
+
if (others) tail.push(others);
|
|
355
|
+
const close = p.closeHint(opened.key);
|
|
356
|
+
if (close) tail.push(`${close} closes it.`);
|
|
357
|
+
blocks.push({ type: 'text', text: tail.join('\n') });
|
|
358
|
+
take?.({ toolsets: c, cs: opened.change });
|
|
359
|
+
return blocksResult(blocks);
|
|
360
|
+
}
|
|
361
|
+
if (p.undeclaredGroups.includes(toolset)) {
|
|
362
|
+
// RFC §5.4: a grouping the server offers but doesn't track: the host opens it, the server isn't told.
|
|
363
|
+
if (instance || options) fail(`${c.serverId}/${toolset} takes no instance or options (the server doesn't declare it)`);
|
|
364
|
+
const cs = c.setGroupOpen(toolset, true);
|
|
365
|
+
const key = keyOf(toolset);
|
|
366
|
+
const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
|
|
367
|
+
const others = this.renderOthers(c, cs, key, shown);
|
|
368
|
+
if (others) lines.push(others);
|
|
369
|
+
lines.push(`${p.closeHint(key)} closes it.`);
|
|
370
|
+
if (take) take({ toolsets: c, cs });
|
|
371
|
+
else c.tell(cs);
|
|
372
|
+
return textResult(lines.join('\n'));
|
|
373
|
+
}
|
|
374
|
+
return fail(noToolset(c, toolset));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* act is dispatched by the framework, not here: it resolves the call
|
|
379
|
+
* (resolveAct) and sends it down the same path as a direct call of the
|
|
380
|
+
* server's tool, so state, results, refusals and lifecycle are the same.
|
|
381
|
+
* A caller that reaches the module itself gets the resolution's refusal,
|
|
382
|
+
* or this.
|
|
383
|
+
*/
|
|
384
|
+
private act(args: Record<string, unknown>): ToolResult {
|
|
385
|
+
const target = resolveAct(this.hub, args);
|
|
386
|
+
if (!target.ok) return { success: false, isError: true, error: target.error };
|
|
387
|
+
return { success: false, isError: true, error: `${META_TOOL_NAMES.act} is dispatched by the framework; nothing was sent` };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** look restates the present: titles and visibility are re-read from the servers first (toolsets/list). */
|
|
391
|
+
private async look(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
392
|
+
const server = str(args.server);
|
|
393
|
+
const targets = server ? [this.hub.get(server)].filter((c): c is ToolsetController => !!c && c.presents) : this.hub.presenting();
|
|
394
|
+
const shown = take !== null;
|
|
395
|
+
const reads = await Promise.all(targets.map(async (c) => ({ c, cs: await c.refreshOpen({ shown }).catch(() => null) })));
|
|
396
|
+
const read = reads.filter((r): r is { c: ToolsetController; cs: ChangeSet } => r.cs !== null);
|
|
397
|
+
const unread = reads.filter((r) => r.cs === null).map(({ c }) => `${c.serverId} couldn't be asked just now: this is what the host last knew of it.`);
|
|
398
|
+
const giveBack = () => {
|
|
399
|
+
if (shown) for (const { c, cs } of read) c.tell(cs);
|
|
400
|
+
};
|
|
401
|
+
let result: ToolResult;
|
|
402
|
+
try {
|
|
403
|
+
result = this.lookNow(args, server, str(args.tool), shown);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
giveBack();
|
|
406
|
+
if (error instanceof MetaError && unread.length) fail([error.message, ...unread].join('\n'));
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
if (result.isError || typeof result.data !== 'string') {
|
|
410
|
+
giveBack();
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
const restates = !str(args.tool) && !str(args.toolset) && !str(args.instance);
|
|
414
|
+
if (!restates) giveBack();
|
|
415
|
+
const lines = restates
|
|
416
|
+
? read.flatMap(({ c, cs }) => {
|
|
417
|
+
const said = [...cs.suggest.map((s) => c.presence.renderSuggestion(s)), ...c.presence.untoldNotes(cs), ...cs.notes];
|
|
418
|
+
// The catalog above counted what it showed; the change's own blocks weren't shown, but stay for a notice if its taker can't show the result.
|
|
419
|
+
take?.({ toolsets: c, cs: { ...cs, blocksShown: false } });
|
|
420
|
+
return said;
|
|
421
|
+
})
|
|
422
|
+
: [];
|
|
423
|
+
lines.push(...unread);
|
|
424
|
+
return lines.length ? textResult(`${result.data}\n${lines.join('\n')}`) : result;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined, mark: boolean): ToolResult {
|
|
428
|
+
if (!server) {
|
|
429
|
+
if (tool) return this.lookTool(owner(this.hub, undefined, (c) => !!c.presence.tool(tool), `a tool "${tool}"`), tool, str(args.instance));
|
|
430
|
+
const ts = str(args.toolset);
|
|
431
|
+
const inst = str(args.instance);
|
|
432
|
+
if (ts || inst) {
|
|
433
|
+
const c = owner(this.hub,
|
|
434
|
+
undefined,
|
|
435
|
+
(x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
|
|
436
|
+
ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
|
|
437
|
+
);
|
|
438
|
+
return this.lookNow(args, c.serverId, tool, mark);
|
|
439
|
+
}
|
|
440
|
+
const all = this.hub.presenting();
|
|
441
|
+
if (!all.length) return textResult('No MCPL server presents toolsets or dynamic tools now; MCPL tools are called directly, by name.');
|
|
442
|
+
return textResult(all.map((c) => c.presence.catalog({ mark })).join('\n\n'));
|
|
443
|
+
}
|
|
444
|
+
const c = pick(this.hub, server);
|
|
445
|
+
if (tool) return this.lookTool(c, tool, str(args.instance));
|
|
446
|
+
const p = c.presence;
|
|
447
|
+
let toolset = str(args.toolset);
|
|
448
|
+
const instance = str(args.instance);
|
|
449
|
+
if (!toolset && instance) {
|
|
450
|
+
const open = p.openRecords().filter((o) => o.instance !== undefined);
|
|
451
|
+
toolset = open.find((o) => o.instance === instance)?.toolset
|
|
452
|
+
?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${o.toolset}:${o.instance}`).join(', ')}` : 'no instance is open there'}`);
|
|
453
|
+
}
|
|
454
|
+
if (!toolset) return textResult(p.catalog({ mark }));
|
|
455
|
+
if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
|
|
456
|
+
const rec = p.isDeclared(toolset) ? (instance ? p.openRecords(toolset).find((o) => o.instance === instance) : p.openRecords(toolset)[0]) : undefined;
|
|
457
|
+
const key = keyOf(toolset, rec?.instance);
|
|
458
|
+
const lines = p.toolBlock(key, this.toolsAt(c, key), { describeAll: true, title: rec?.title, seen: mark ? undefined : new Set<string>() });
|
|
459
|
+
const close = p.closeHint(key);
|
|
460
|
+
if (close) lines.push(`${close} closes it.`);
|
|
461
|
+
return textResult(lines.join('\n'));
|
|
462
|
+
}
|
|
463
|
+
const d = p.declared[toolset];
|
|
464
|
+
if (d) {
|
|
465
|
+
const open = p.openRecords(toolset);
|
|
466
|
+
return textResult([
|
|
467
|
+
`${c.serverId}/${toolset}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}`,
|
|
468
|
+
open.length ? `open: ${open.map((o) => o.instance ?? toolset).join(', ')}` : 'not open',
|
|
469
|
+
...(d.instances ? ['several can be open at once; name each with instance'] : []),
|
|
470
|
+
...(d.options ? [`options: ${jsonText(d.options)}`] : []),
|
|
471
|
+
`${p.openHint(toolset)} opens it.`,
|
|
472
|
+
].join('\n'));
|
|
473
|
+
}
|
|
474
|
+
if (p.undeclaredGroups.includes(toolset)) {
|
|
475
|
+
const names = p.allTools.filter((t) => p.toolsetOf(t.name) === toolset).map((t) => t.name);
|
|
476
|
+
return textResult(`${c.serverId}/${toolset}: ${names.join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
|
|
477
|
+
}
|
|
478
|
+
return fail(noToolset(c, toolset!));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private lookTool(c: ToolsetController, tool: string, instance: string | undefined): ToolResult {
|
|
482
|
+
const p = c.presence;
|
|
483
|
+
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
|
|
484
|
+
const ts = p.toolsetOf(tool);
|
|
485
|
+
// The definition act would check, wherever act would take the tool.
|
|
486
|
+
const open = !instance && hasInstances(p, ts) ? p.openRecords(ts) : [];
|
|
487
|
+
const key = !p.shownAs(tool, 'dynamic') ? undefined : actKey(p, tool, instance ?? (open.length === 1 ? open[0]!.instance : undefined));
|
|
488
|
+
const def = key ? p.defOf(key, tool) : p.tool(tool)!;
|
|
489
|
+
return textResult(`${renderToolDefinition(def)}\n${p.toolStatus(tool)}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private async close(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
493
|
+
const shown = take !== null;
|
|
494
|
+
const named = str(args.toolset);
|
|
495
|
+
const c = named && !str(args.server)
|
|
496
|
+
? owner(this.hub, undefined, (x) => x.presence.isOpen(named, str(args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
|
|
497
|
+
: pick(this.hub, args.server);
|
|
498
|
+
const p = c.presence;
|
|
499
|
+
let toolset = named;
|
|
500
|
+
let instance = str(args.instance);
|
|
501
|
+
if (!toolset) {
|
|
502
|
+
const open = [
|
|
503
|
+
...p.openRecords().filter((o) => !instance || o.instance === instance),
|
|
504
|
+
...(instance ? [] : p.openGroups.map((g) => ({ toolset: g, instance: undefined as string | undefined }))),
|
|
505
|
+
];
|
|
506
|
+
if (open.length !== 1) {
|
|
507
|
+
fail(open.length
|
|
508
|
+
? `several are open on ${c.serverId}: name one of ${open.map((o) => `${o.toolset}${o.instance ? `:${o.instance}` : ''}`).join(', ')}`
|
|
509
|
+
: `nothing is open on ${c.serverId}`);
|
|
510
|
+
}
|
|
511
|
+
toolset = open[0]!.toolset;
|
|
512
|
+
instance = open[0]!.instance;
|
|
513
|
+
}
|
|
514
|
+
if (p.isDeclared(toolset!)) {
|
|
515
|
+
// As open refuses one: a toolset without instances is closed without one.
|
|
516
|
+
if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${toolset} has no instances: close it without one`);
|
|
517
|
+
if (p.declared[toolset!]!.instances && !instance) {
|
|
518
|
+
const open = p.openRecords(toolset);
|
|
519
|
+
if (open.length !== 1) fail(open.length ? `several ${toolset} instances are open (${open.map((o) => o.instance).join(', ')}): say which` : `${toolset} is not open`);
|
|
520
|
+
instance = open[0]!.instance;
|
|
521
|
+
}
|
|
522
|
+
let closed: Awaited<ReturnType<ToolsetController['close']>>;
|
|
523
|
+
try {
|
|
524
|
+
closed = await c.close(toolset!, instance, { shown });
|
|
525
|
+
} catch (error) {
|
|
526
|
+
const what = `${toolset}${instance ? `:${instance}` : ''}`;
|
|
527
|
+
return requestFailure(c.serverId, `did not close ${what}`, `didn't confirm closing ${what}`, error);
|
|
528
|
+
}
|
|
529
|
+
const key = keyOf(toolset!, instance);
|
|
530
|
+
const change = p.render(closed.change, { mark: shown, describeAll: !shown }) || `${p.label(key)} closed.`;
|
|
531
|
+
take?.({ toolsets: c, cs: closed.change });
|
|
532
|
+
return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
|
|
533
|
+
}
|
|
534
|
+
if (p.undeclaredGroups.includes(toolset!)) {
|
|
535
|
+
if (instance) fail(`${c.serverId}/${toolset} takes no instance (the server doesn't declare it)`);
|
|
536
|
+
if (!p.isOpen(toolset!)) fail(`${c.serverId}/${toolset} is not open`);
|
|
537
|
+
const cs = c.setGroupOpen(toolset!, false);
|
|
538
|
+
const text = p.render(cs, { mark: shown, describeAll: !shown }) || `${p.label(keyOf(toolset!))} closed.`;
|
|
539
|
+
if (take) take({ toolsets: c, cs });
|
|
540
|
+
else c.tell(cs);
|
|
541
|
+
return textResult(text);
|
|
542
|
+
}
|
|
543
|
+
return fail(noToolset(c, toolset!));
|
|
544
|
+
}
|
|
545
|
+
}
|
package/src/mcpl/transport.ts
CHANGED
|
@@ -830,11 +830,12 @@ const SERVER_CONFIG_BOOLEANS: Record<BooleanSetting, string> = {
|
|
|
830
830
|
inheritEnv: 'true passes the server the host\'s whole environment, and false or leaving it out passes only the allowlisted and LC_* host variables',
|
|
831
831
|
reconnect: 'true retries in the background when the server can\'t be reached or its connection drops, and false or leaving it out doesn\'t',
|
|
832
832
|
allowHostCommands: 'true lets the server run the host\'s admin commands, such as undo, hide, unstick and quiesce, and false or leaving it out refuses them',
|
|
833
|
+
toolHints: 'true advertises MCPL tool hints (RFC-014), so the server\'s dynamic tools are announced in the conversation and called through toolsets--act, and false or leaving it out lists every tool as before',
|
|
833
834
|
};
|
|
834
835
|
|
|
835
836
|
/**
|
|
836
|
-
* Refuse a server config whose `inheritEnv`, `reconnect
|
|
837
|
-
* holds anything but true or false. A config read from JSON can carry a quoted
|
|
837
|
+
* Refuse a server config whose `inheritEnv`, `reconnect`, `allowHostCommands`
|
|
838
|
+
* or `toolHints` holds anything but true or false. A config read from JSON can carry a quoted
|
|
838
839
|
* "false" where the type says boolean, and every reader of these settings takes
|
|
839
840
|
* only `true` as on, so the quotes would hide what the value does. null counts
|
|
840
841
|
* as not set, so a generated config that writes an unset setting as null
|
package/src/mcpl/types.ts
CHANGED
|
@@ -96,6 +96,9 @@ export interface McplAudioContent {
|
|
|
96
96
|
disposition?: 'never' | 'ref';
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/** MCPL's reference block (RFC-005 §3). A `resource` block with no `uri` of
|
|
100
|
+
* its own may be MCP's embedded resource instead, its contents under
|
|
101
|
+
* `resource`, which classifyBlock tells apart and shows as its contents. */
|
|
99
102
|
export interface McplResourceContent {
|
|
100
103
|
type: 'resource';
|
|
101
104
|
/** Resource URI (e.g., "memory://facts/12345") */
|
|
@@ -190,6 +193,9 @@ export interface McplHostCapabilities {
|
|
|
190
193
|
modelInfo?: boolean;
|
|
191
194
|
featureSets?: boolean;
|
|
192
195
|
channels?: boolean | McplChannelCapabilities;
|
|
196
|
+
/** RFC-014 §6.3: this host presents tool hints. Sent only to a server
|
|
197
|
+
* whose config sets `toolHints: true`. */
|
|
198
|
+
toolHints?: boolean;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
/** Channel capability leaves (§14.1). `observe` was the pre-0.5 name for
|
|
@@ -378,7 +384,9 @@ export interface McplServerConfig {
|
|
|
378
384
|
*
|
|
379
385
|
* `disabledCapabilities` takes precedence on conflict. `version` and
|
|
380
386
|
* `featureSets` are not maskable here — feature sets have their own
|
|
381
|
-
* enable/disable knobs above
|
|
387
|
+
* enable/disable knobs above — and neither are RFC-014's `toolHints`,
|
|
388
|
+
* `toolsets` and `surface`, which a server's own `toolHints` config turns
|
|
389
|
+
* off.
|
|
382
390
|
*/
|
|
383
391
|
enabledCapabilities?: string[];
|
|
384
392
|
|
|
@@ -498,6 +506,20 @@ export interface McplServerConfig {
|
|
|
498
506
|
* See src/mcpl/tool-lifecycle.ts.
|
|
499
507
|
*/
|
|
500
508
|
toolLifecycle?: import('./tool-lifecycle.js').ToolLifecycleConfig;
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* MCPL RFC-014 (tool presence, toolsets and surfaces). Default false: the
|
|
512
|
+
* host advertises nothing and treats every tool as static, byte for byte as
|
|
513
|
+
* before. When true, the host advertises `experimental.mcpl.toolHints`, and
|
|
514
|
+
* honours hints only from a server that advertised `toolHints` too. Then
|
|
515
|
+
* the server's static tools stay in the tool list as before, and its
|
|
516
|
+
* dynamic tools (toolset tools, `mcpl/presence: "dynamic"`) leave it: they
|
|
517
|
+
* are announced in the conversation as they become visible and invisible,
|
|
518
|
+
* and called through the `toolsets` module's `toolsets--act`, opened with
|
|
519
|
+
* `toolsets--open`, inspected with `toolsets--look` and closed with
|
|
520
|
+
* `toolsets--close`. Only `true` turns it on, and null counts as not set.
|
|
521
|
+
*/
|
|
522
|
+
toolHints?: boolean;
|
|
501
523
|
}
|
|
502
524
|
|
|
503
525
|
// ============================================================================
|
|
@@ -1299,7 +1321,8 @@ export interface ChannelIncomingMessageResult {
|
|
|
1299
1321
|
* Non-normative; host-defined. See Spec Section 11.5.
|
|
1300
1322
|
*/
|
|
1301
1323
|
export interface InferenceRoutingPolicy {
|
|
1302
|
-
/**
|
|
1324
|
+
/** Model for a request no override, byFeature or wildcards entry names a
|
|
1325
|
+
* model for. Left empty, such a request is refused with -32603. */
|
|
1303
1326
|
default: string;
|
|
1304
1327
|
|
|
1305
1328
|
/** Model override per feature set name */
|