@animalabs/agent-framework 0.27.0 → 0.29.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 +145 -41
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +73 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +11 -5
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +5 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +9 -5
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- 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 +468 -125
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3651 -1111
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +133 -13
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +654 -59
- 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/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 +173 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +202 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +778 -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/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- 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 +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +133 -54
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +29 -11
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +43 -70
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +70 -30
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.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-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +150 -80
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +60 -30
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -55
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +220 -111
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +27 -35
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +313 -91
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +31 -13
- 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 +10 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +15 -9
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- 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 +30 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +26 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +138 -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 +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +59 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- 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 +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +67 -50
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +63 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +980 -547
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +125 -82
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +465 -241
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +156 -50
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- 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/types.d.ts +96 -43
- 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/notice-tags.d.ts +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -0
- package/dist/src/notice-tags.js.map +1 -0
- 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 +52 -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.js +1 -1
- package/dist/src/silent-heartbeat.d.ts +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- 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 +39 -4
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- 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 +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- 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 +63 -18
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +58 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -13
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +76 -12
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +11 -5
- package/src/auto-continue.ts +12 -5
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +3772 -1138
- package/src/gate/event-gate.ts +705 -60
- package/src/gate/index.ts +9 -1
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +176 -3
- package/src/heartbeat/heartbeat.ts +843 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +5 -1
- package/src/line-reader.ts +58 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +139 -53
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +45 -70
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +76 -34
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +149 -82
- package/src/mcpl/tool-signature.ts +64 -32
- package/src/mcpl/toolset-host.ts +212 -110
- package/src/mcpl/toolsets-module.ts +277 -91
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +32 -13
- package/src/module-registry.ts +14 -9
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +30 -18
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +151 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +59 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +69 -50
- package/src/modules/workspace/index.ts +966 -556
- package/src/modules/workspace/observe.ts +497 -242
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +184 -49
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/notice-tags.ts +84 -0
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +64 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +38 -4
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/coordinator.ts +25 -27
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +63 -18
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +59 -0
- package/src/types/module.ts +28 -13
- package/src/types/trace.ts +36 -1
package/src/mcpl/toolset-host.ts
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* what changed; and the hub that holds every server's controller for the
|
|
6
6
|
* meta-tools (toolsets-module.ts) and for reminders.
|
|
7
7
|
*
|
|
8
|
-
* Hint changes apply as their message is read (RFC §6.1)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Hint changes apply as their message is read (RFC §6.1); a static↔dynamic
|
|
9
|
+
* move takes effect when a tools/list states it (§4.1), and the model's tool
|
|
10
|
+
* list shows it at its next rebuild; until then the tool is called at either
|
|
11
|
+
* place (§4.2). The connection's read tap hands
|
|
12
|
+
* the controller every result of a hinted request (awaited or not, while no
|
|
13
|
+
* more than 1,000 hinted requests await an answer), each tools/list whole, every push/event and every tools/hints, of the
|
|
14
|
+
* current transport, synchronously and in arrival order, before anything
|
|
15
|
+
* routes the message or anything read after it is handled.
|
|
12
16
|
* What a message's changes did to the model's view is kept by the object
|
|
13
17
|
* that carried them (`readChanges`), so whoever later handles that object
|
|
14
18
|
* (the tool result, the push, the meta-tool) renders it next to the content.
|
|
@@ -20,12 +24,15 @@
|
|
|
20
24
|
* which the toolsets module gathers at its next start as a stored
|
|
21
25
|
* `afterUser` injection.
|
|
22
26
|
*/
|
|
23
|
-
import
|
|
27
|
+
import { nameToTypeBack, quotedValue } from '../supplied-text.js';
|
|
28
|
+
import { McplRequestError, type McplServerConnection, type HintRead } from './server-connection.js';
|
|
24
29
|
import type { McplServerConfig } from './types.js';
|
|
25
30
|
import { isToolAllowed } from './tool-policy.js';
|
|
31
|
+
import { redactServerText } from './protocol-family.js';
|
|
32
|
+
import { logDetail, logValueRedacted } from './log-value.js';
|
|
26
33
|
import {
|
|
27
|
-
ToolPresence, emptyChange, isEmptyChange,
|
|
28
|
-
type ChangeSet, type MetaToolNames,
|
|
34
|
+
ToolPresence, emptyChange, isEmptyChange, keyOf,
|
|
35
|
+
type ChangeSet, type MetaToolNames, type Presence,
|
|
29
36
|
} from './tool-presence.js';
|
|
30
37
|
import type { HintedTool } from './tool-signature.js';
|
|
31
38
|
|
|
@@ -38,7 +45,7 @@ export const META_TOOL_NAMES: MetaToolNames = {
|
|
|
38
45
|
close: `${TOOLSETS_MODULE}--close`,
|
|
39
46
|
};
|
|
40
47
|
|
|
41
|
-
/** How long an open
|
|
48
|
+
/** How long an open or close is waited for. A late result still applies when it's read. */
|
|
42
49
|
export const TOOLSET_REQUEST_TIMEOUT_MS = 60_000;
|
|
43
50
|
/** A toolsets/list after a (re)connect or in a recovery. */
|
|
44
51
|
const LIST_TIMEOUT_MS = 15_000;
|
|
@@ -54,16 +61,28 @@ export interface ToolsetHostDeps {
|
|
|
54
61
|
/** A standalone notice into the conversation: a tools/hints change, a late
|
|
55
62
|
* result's, a reconnect's adoption, a recovery's. `invite`: it suggests an
|
|
56
63
|
* open. The host applies its usual rules for events. */
|
|
57
|
-
notify(serverId: string, text: string, opts: { invite: boolean }):
|
|
64
|
+
notify(serverId: string, text: string, opts: { invite: boolean }): boolean;
|
|
65
|
+
/** Whether the server's changes may be put into the conversation unasked
|
|
66
|
+
* (its `pushEvents` grant): notices and reminders. */
|
|
67
|
+
canNotify(serverId: string): boolean;
|
|
58
68
|
/** The operator's log. */
|
|
59
69
|
log(line: string): void;
|
|
60
70
|
/** How a static tool is named in the tool list (`prefix--tool`). */
|
|
61
71
|
listedName(serverId: string, tool: string): string;
|
|
72
|
+
/** Where the model's published tool list last put a server's tool:
|
|
73
|
+
* offered in it (static), listed but not offered (dynamic), or undefined
|
|
74
|
+
* where it isn't published. RFC §4.2: until its move applies, a tool
|
|
75
|
+
* keeps working where it is. */
|
|
76
|
+
published?(serverId: string, tool: string): Presence | undefined;
|
|
77
|
+
/** The host's own toolsets an agent has open (the built-in one, server
|
|
78
|
+
* tabs): each one's marker and reminder, for RFC §8.3 reminders beside the
|
|
79
|
+
* servers' toolsets. */
|
|
80
|
+
hostOpen?(agentName: string): Array<{ marker: string; reminder: () => string; dueWhenAbsent?: boolean }>;
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
/** One server's RFC-014 state and traffic. Lives as long as the server's
|
|
65
|
-
* configuration, across reconnects and restarts (its `ended`
|
|
66
|
-
* the model was shown carry over). */
|
|
84
|
+
* configuration, across reconnects and the server's restarts (its `ended`
|
|
85
|
+
* ids and what the model was shown carry over), in memory only. */
|
|
67
86
|
export class ToolsetController {
|
|
68
87
|
readonly presence: ToolPresence;
|
|
69
88
|
private connection: McplServerConnection | null = null;
|
|
@@ -71,16 +90,10 @@ export class ToolsetController {
|
|
|
71
90
|
private epoch = -1;
|
|
72
91
|
/** What applying a message's hints changed, by the object that carried them. */
|
|
73
92
|
private readonly readChanges = new WeakMap<object, ChangeSet>();
|
|
74
|
-
/** Results whose change no one has
|
|
75
|
-
private readonly unclaimed = new
|
|
93
|
+
/** Results whose change no one has taken yet, in the order they were read, with their method (tellUnlessClaimed). */
|
|
94
|
+
private readonly unclaimed = new Map<object, string>();
|
|
76
95
|
/** eventIds of pushes read on this transport (bounded): a repeat's hints are ignored (RFC §6.1). */
|
|
77
96
|
private seenEvents = new Set<string>();
|
|
78
|
-
/** A paged tools/list being read. */
|
|
79
|
-
private listPages: HintedTool[] | null = null;
|
|
80
|
-
/** Lists this controller asked for itself, whose change it delivers. */
|
|
81
|
-
private ownLists = 0;
|
|
82
|
-
/** What lists read while one of its own was out changed, for it to tell. */
|
|
83
|
-
private ownListChange: ChangeSet | null = null;
|
|
84
97
|
private recoveryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
85
98
|
private recovering = false;
|
|
86
99
|
private recoverAgain = false;
|
|
@@ -95,6 +108,7 @@ export class ToolsetController {
|
|
|
95
108
|
META_TOOL_NAMES,
|
|
96
109
|
(tool) => isToolAllowed(tool, this.config),
|
|
97
110
|
(tool) => deps.listedName(serverId, tool),
|
|
111
|
+
(tool) => deps.published?.(serverId, tool),
|
|
98
112
|
);
|
|
99
113
|
}
|
|
100
114
|
|
|
@@ -102,6 +116,14 @@ export class ToolsetController {
|
|
|
102
116
|
this.deps.log(`${this.serverId}: ${line}`);
|
|
103
117
|
}
|
|
104
118
|
|
|
119
|
+
private field(value: unknown): string {
|
|
120
|
+
return logValueRedacted(value, (text) => redactServerText(text, this.config));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private failure(error: unknown): string {
|
|
124
|
+
return logDetail(redactServerText(error instanceof Error ? error.message : String(error), this.config));
|
|
125
|
+
}
|
|
126
|
+
|
|
105
127
|
/** A restart can bring a new configuration under the same id. */
|
|
106
128
|
setConfig(config: McplServerConfig): void {
|
|
107
129
|
this.config = config;
|
|
@@ -118,6 +140,10 @@ export class ToolsetController {
|
|
|
118
140
|
connection.setReadTap((read, epoch) => this.onRead(connection, read, epoch));
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
release(): void {
|
|
144
|
+
if (this.connection) this.detach(this.connection);
|
|
145
|
+
}
|
|
146
|
+
|
|
121
147
|
/** The server's connection is gone for good (disconnected, removed). */
|
|
122
148
|
detach(connection: McplServerConnection): void {
|
|
123
149
|
if (this.connection !== connection) return;
|
|
@@ -139,7 +165,7 @@ export class ToolsetController {
|
|
|
139
165
|
/** The live connection, for a request. */
|
|
140
166
|
private live(): McplServerConnection {
|
|
141
167
|
const c = this.connection;
|
|
142
|
-
if (!c || !c.isConnected) throw new
|
|
168
|
+
if (!c || !c.isConnected) throw new McplRequestError(`${this.serverId} is not connected right now; try again later`, 'not-sent');
|
|
143
169
|
this.sync(c.transportEpoch, c);
|
|
144
170
|
return c;
|
|
145
171
|
}
|
|
@@ -153,7 +179,6 @@ export class ToolsetController {
|
|
|
153
179
|
if (epoch === this.epoch) return;
|
|
154
180
|
this.epoch = epoch;
|
|
155
181
|
this.seenEvents = new Set();
|
|
156
|
-
this.listPages = null;
|
|
157
182
|
this.presence.beginConnection(connection.toolsetDeclarations, true);
|
|
158
183
|
}
|
|
159
184
|
|
|
@@ -163,7 +188,7 @@ export class ToolsetController {
|
|
|
163
188
|
if (connection !== this.connection) return;
|
|
164
189
|
this.sync(epoch, connection);
|
|
165
190
|
if (read.kind === 'result') {
|
|
166
|
-
this.readResult(read.method, read.params, read.result as Record<string, unknown>, read.awaited);
|
|
191
|
+
this.readResult(read.method, read.params, read.result as Record<string, unknown>, read.awaited, read.straddled === true);
|
|
167
192
|
return;
|
|
168
193
|
}
|
|
169
194
|
if (!this.presence.active || !isObj(read.params)) return;
|
|
@@ -178,7 +203,7 @@ export class ToolsetController {
|
|
|
178
203
|
// RFC §7: a push carrying hints is never coalesced; when its changes
|
|
179
204
|
// would apply is undefined, so they're ignored and the state re-read.
|
|
180
205
|
if (p.coalesce !== undefined && p.coalesce !== null) {
|
|
181
|
-
this.log(`push ${
|
|
206
|
+
this.log(`push ${this.field(p.eventId ?? '')} carries hints and coalesce (RFC-014 §7 forbids it): its hints are ignored; recovering`);
|
|
182
207
|
this.scheduleRecovery();
|
|
183
208
|
return;
|
|
184
209
|
}
|
|
@@ -186,32 +211,29 @@ export class ToolsetController {
|
|
|
186
211
|
const eventId = typeof p.eventId === 'string' || typeof p.eventId === 'number' ? String(p.eventId) : '';
|
|
187
212
|
if (eventId) {
|
|
188
213
|
if (this.seenEvents.has(eventId)) {
|
|
189
|
-
this.log(`push ${eventId} was read before: its hints are ignored`);
|
|
214
|
+
this.log(`push ${this.field(eventId)} was read before: its hints are ignored`);
|
|
190
215
|
return;
|
|
191
216
|
}
|
|
192
217
|
this.seenEvents.add(eventId);
|
|
193
218
|
if (this.seenEvents.size > 1000) this.seenEvents.delete(this.seenEvents.values().next().value!);
|
|
194
219
|
}
|
|
195
220
|
// Applied now, before (and whatever) the host decides about the push.
|
|
196
|
-
|
|
221
|
+
const cs = this.applyHints(p.hints, 'push');
|
|
222
|
+
this.readChanges.set(p, cs);
|
|
223
|
+
this.tellUnlessClaimed(p, cs, 'push/event');
|
|
197
224
|
}
|
|
198
225
|
|
|
199
|
-
private readResult(method: string, params: Record<string, unknown>, res: Record<string, unknown>, awaited: boolean): void {
|
|
226
|
+
private readResult(method: string, params: Record<string, unknown>, res: Record<string, unknown>, awaited: boolean, straddled: boolean): void {
|
|
200
227
|
let cs: ChangeSet;
|
|
201
228
|
switch (method) {
|
|
202
229
|
case 'tools/list': {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
cs = this.readToolList(tools, res);
|
|
211
|
-
// The framework's own refresh (a list change, a reconnect) told here;
|
|
212
|
-
// a list this controller asked for is told by whoever asked.
|
|
213
|
-
if (this.ownLists === 0) this.deliver(cs);
|
|
214
|
-
else this.ownListChange = joinChanges(this.ownListChange ?? emptyChange(), cs);
|
|
230
|
+
if (!Array.isArray(res.tools)) return;
|
|
231
|
+
cs = this.readToolList(res.tools as HintedTool[], res);
|
|
232
|
+
if (straddled && !this.recovering) {
|
|
233
|
+
this.log('tools/list pages carried different revs, so its earlier pages are older than changes read between them: recovering');
|
|
234
|
+
this.scheduleRecovery();
|
|
235
|
+
}
|
|
236
|
+
this.deliver(cs);
|
|
215
237
|
return;
|
|
216
238
|
}
|
|
217
239
|
case 'toolsets/list':
|
|
@@ -259,9 +281,9 @@ export class ToolsetController {
|
|
|
259
281
|
private applyHints(h: unknown, source: string): ChangeSet {
|
|
260
282
|
if (h === undefined || !this.presence.active) return emptyChange();
|
|
261
283
|
const before = this.presence.view();
|
|
262
|
-
const last = this.presence.rev;
|
|
284
|
+
const last = this.field(this.presence.rev);
|
|
263
285
|
const r = this.presence.apply(h);
|
|
264
|
-
const rev =
|
|
286
|
+
const rev = this.field((h as { rev?: unknown } | null)?.rev);
|
|
265
287
|
if (r.behind) {
|
|
266
288
|
this.log(`hint change rev ${rev} (${source}) is not after rev ${last}: ignored; recovering from tools/list and toolsets/list`);
|
|
267
289
|
this.scheduleRecovery();
|
|
@@ -271,6 +293,10 @@ export class ToolsetController {
|
|
|
271
293
|
} else if (!r.applied) {
|
|
272
294
|
this.log(`a malformed hint change (${source}) was ignored`);
|
|
273
295
|
}
|
|
296
|
+
if (r.unlisted) {
|
|
297
|
+
this.log(`hint change rev ${rev} (${source}) names a tool tools/list hasn't listed: recovering from tools/list and toolsets/list`);
|
|
298
|
+
this.scheduleRecovery();
|
|
299
|
+
}
|
|
274
300
|
const cs = this.presence.diff(before, this.presence.view());
|
|
275
301
|
cs.suggest.push(...r.suggest);
|
|
276
302
|
return cs;
|
|
@@ -281,7 +307,7 @@ export class ToolsetController {
|
|
|
281
307
|
const before = this.presence.view();
|
|
282
308
|
this.presence.setTools(tools);
|
|
283
309
|
if (this.presence.active && this.presence.noteListRev(isObj(last._meta) ? last._meta['mcpl/rev'] : undefined)) {
|
|
284
|
-
this.log(`tools/list was written after rev ${
|
|
310
|
+
this.log(`tools/list was written after rev ${this.field(isObj(last._meta) ? last._meta['mcpl/rev'] : '')}, past the last rev read: a message was lost; recovering`);
|
|
285
311
|
this.scheduleRecovery();
|
|
286
312
|
}
|
|
287
313
|
return this.presence.diff(before, this.presence.view());
|
|
@@ -290,39 +316,78 @@ export class ToolsetController {
|
|
|
290
316
|
/** toolsets/list, as read: a full statement of dynamic-tool state under its own rev (RFC §5.3, §6.1). */
|
|
291
317
|
private readToolsetList(res: Record<string, unknown>): ChangeSet {
|
|
292
318
|
if (!this.presence.active) return emptyChange();
|
|
319
|
+
if (!Array.isArray(res.open)) {
|
|
320
|
+
this.log('a toolsets/list result with no open list was ignored');
|
|
321
|
+
return emptyChange();
|
|
322
|
+
}
|
|
293
323
|
const before = this.presence.view();
|
|
294
|
-
const { lost, ended, suggest, skipped } = this.presence.adopt(res);
|
|
324
|
+
const { lost, ended, suggest, skipped, unlisted } = this.presence.adopt(res);
|
|
295
325
|
if (skipped && !this.recovering) {
|
|
296
326
|
// Per-instance state is restated in full; what else was lost (presence) comes from tools/list.
|
|
297
327
|
this.log('toolsets/list skipped a rev: re-reading tools/list');
|
|
298
328
|
this.scheduleRecovery();
|
|
299
329
|
}
|
|
330
|
+
if (unlisted && !this.recovering) {
|
|
331
|
+
this.log('toolsets/list names a tool tools/list hasn\'t listed: re-reading tools/list');
|
|
332
|
+
this.scheduleRecovery();
|
|
333
|
+
}
|
|
300
334
|
const cs = this.presence.diff(before, this.presence.view());
|
|
301
335
|
cs.suggest.push(...suggest);
|
|
302
336
|
// A lost toolset gets its note, not also a "closed" line.
|
|
303
337
|
cs.changes = cs.changes.filter((c) => !lost.some((o) => keyOf(o.toolset, o.instance) === c.key));
|
|
304
|
-
|
|
338
|
+
const told = this.presence.adoptionNotes(lost, ended);
|
|
339
|
+
cs.notes.push(...told.notes);
|
|
340
|
+
if (told.ended.length) cs.ended = [...(cs.ended ?? []), ...told.ended];
|
|
305
341
|
return cs;
|
|
306
342
|
}
|
|
307
343
|
|
|
308
|
-
/** A change with nothing else to ride on, told as a notice. */
|
|
344
|
+
/** A change with nothing else to ride on, told as a notice, after every result's change still unclaimed. */
|
|
309
345
|
private deliver(cs: ChangeSet): void {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
346
|
+
this.tellUnclaimed();
|
|
347
|
+
this.notice(cs);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Unclaimed carriers' changes, told as notices in the order they were read,
|
|
352
|
+
* through `upTo` when given. When no notice can be written, a push keeps its
|
|
353
|
+
* change for the framework's push path, which takes it when the push gets
|
|
354
|
+
* there; a result's is dropped, whether or not its caller is still to
|
|
355
|
+
* resume.
|
|
356
|
+
*/
|
|
357
|
+
private tellUnclaimed(upTo?: object): void {
|
|
358
|
+
for (const [res, method] of [...this.unclaimed]) {
|
|
359
|
+
this.unclaimed.delete(res);
|
|
360
|
+
const cs = this.readChanges.get(res) ?? emptyChange();
|
|
361
|
+
const sent = this.notice(cs);
|
|
362
|
+
if (sent !== 'refused' || method !== 'push/event') this.readChanges.delete(res);
|
|
363
|
+
if (sent === 'told') this.log(`${method}'s tool change wasn't shown with it: told as a notice`);
|
|
364
|
+
if (res === upTo) return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Whether a notice was written, there was nothing left to tell (`empty`), or one couldn't be written (`refused`). */
|
|
369
|
+
private notice(cs: ChangeSet): 'told' | 'empty' | 'refused' {
|
|
370
|
+
if (isEmptyChange(cs)) return 'empty';
|
|
371
|
+
const text = this.presence.render(cs, { mark: false });
|
|
372
|
+
if (!text) return 'empty';
|
|
373
|
+
if (!this.deps.notify(this.serverId, text, { invite: cs.suggest.some((s) => s.action === 'open') })) return 'refused';
|
|
374
|
+
this.presence.markTold(cs.ended);
|
|
375
|
+
this.presence.markShown(cs);
|
|
376
|
+
return 'told';
|
|
314
377
|
}
|
|
315
378
|
|
|
316
379
|
/**
|
|
317
380
|
* A dynamic tool called by its namespaced name, as if it were listed (a
|
|
318
381
|
* model imitating an earlier call, a module): refused, so every call into
|
|
319
382
|
* a toolset goes through act, which knows its instance and gates closed
|
|
320
|
-
* groups (RFC §5.4, §5.5). Null for any other tool
|
|
383
|
+
* groups (RFC §5.4, §5.5). Null for any other tool, including one the tool
|
|
384
|
+
* list last published or the server's latest tools/list puts as static
|
|
385
|
+
* (shownAs: RFC §4.2, until its move applies).
|
|
321
386
|
*/
|
|
322
387
|
dynamicCallRefusal(tool: string, calledAs: string): string | null {
|
|
323
|
-
if (!this.presence.active || !this.presence.tool(tool) || this.presence.
|
|
324
|
-
return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${tool} is a dynamic tool of ${this.serverId}, ` +
|
|
325
|
-
`called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool
|
|
388
|
+
if (!this.presence.active || !this.presence.tool(tool) || this.presence.shownAs(tool, 'static')) return null;
|
|
389
|
+
return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${nameToTypeBack(tool)} is a dynamic tool of ${this.serverId}, ` +
|
|
390
|
+
`called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(tool)}, args}. ${this.presence.toolStatus(tool)}.`;
|
|
326
391
|
}
|
|
327
392
|
|
|
328
393
|
/**
|
|
@@ -332,35 +397,56 @@ export class ToolsetController {
|
|
|
332
397
|
* run (the next macrotask), such as the result of a call the framework makes
|
|
333
398
|
* itself (a reaction, a send, an accounted side effect), is told as a notice
|
|
334
399
|
* instead, as a standalone tools/hints is (RFC §6.1: every change reaches the
|
|
335
|
-
* agent once).
|
|
400
|
+
* agent once), or dropped as tellUnclaimed says when none can be written.
|
|
336
401
|
*/
|
|
337
402
|
private tellUnlessClaimed(res: object, cs: ChangeSet, method: string): void {
|
|
338
403
|
if (isEmptyChange(cs)) return;
|
|
339
|
-
this.unclaimed.
|
|
404
|
+
this.unclaimed.set(res, method);
|
|
340
405
|
setImmediate(() => {
|
|
341
|
-
if (
|
|
342
|
-
this.log(`${method} result's tool change wasn't shown with it: told as a notice`);
|
|
343
|
-
this.deliver(cs);
|
|
406
|
+
if (this.unclaimed.has(res)) this.tellUnclaimed(res);
|
|
344
407
|
});
|
|
345
408
|
}
|
|
346
409
|
|
|
347
|
-
/** What applying `carrier`'s hints changed (a tool result, a push's params),
|
|
348
|
-
* rendered, for its handler to show with it; '' for nothing. Taking it
|
|
349
|
-
* means it's told there, not as a notice. */
|
|
350
|
-
changeText(carrier: unknown): string {
|
|
351
|
-
if (!isObj(carrier)) return '';
|
|
352
|
-
this.unclaimed.delete(carrier);
|
|
353
|
-
const cs = this.readChanges.get(carrier);
|
|
354
|
-
return cs ? this.presence.render(cs) : '';
|
|
355
|
-
}
|
|
356
410
|
|
|
357
|
-
/**
|
|
358
|
-
|
|
411
|
+
/**
|
|
412
|
+
* The change a carrier's hints made (a tool result, a push's params), taken
|
|
413
|
+
* by a caller that shows it to an agent with the carrier: it isn't told as a
|
|
414
|
+
* notice, and the caller says when it has reached an agent (told). Call it
|
|
415
|
+
* synchronously as the carrier is read, before the next macrotask, when an
|
|
416
|
+
* unclaimed carrier's change goes out as a notice instead (tellUnclaimed
|
|
417
|
+
* says what happens when none can be written). A change already gone that
|
|
418
|
+
* way is empty here. Unclaimed carriers read before this one
|
|
419
|
+
* are told first, as notices, so no later change is shown before them.
|
|
420
|
+
*/
|
|
421
|
+
takeChange(carrier: unknown): ChangeSet {
|
|
359
422
|
if (!isObj(carrier)) return emptyChange();
|
|
423
|
+
if (this.unclaimed.has(carrier)) {
|
|
424
|
+
let before: object | undefined;
|
|
425
|
+
for (const res of this.unclaimed.keys()) {
|
|
426
|
+
if (res === carrier) break;
|
|
427
|
+
before = res;
|
|
428
|
+
}
|
|
429
|
+
if (before) this.tellUnclaimed(before);
|
|
430
|
+
}
|
|
360
431
|
this.unclaimed.delete(carrier);
|
|
361
432
|
return this.readChanges.get(carrier) ?? emptyChange();
|
|
362
433
|
}
|
|
363
434
|
|
|
435
|
+
/** A change its taker showed to an agent: its ended records count as told, and the definitions it describes as shown, unless the taker showed it without its blocks (`blocksShown: false`). */
|
|
436
|
+
told(cs: ChangeSet): void {
|
|
437
|
+
this.presence.markTold(cs.ended);
|
|
438
|
+
if (cs.blocksShown !== false) this.presence.markShown(cs);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** A change its taker couldn't show, told as a notice instead, or dropped when none can be written. */
|
|
442
|
+
tell(cs: ChangeSet): void {
|
|
443
|
+
this.notice(cs);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private changeOf(carrier: object): ChangeSet {
|
|
447
|
+
return this.readChanges.get(carrier) ?? emptyChange();
|
|
448
|
+
}
|
|
449
|
+
|
|
364
450
|
// ── recovery (RFC §6.1) ──
|
|
365
451
|
|
|
366
452
|
private scheduleRecovery(): void {
|
|
@@ -380,19 +466,23 @@ export class ToolsetController {
|
|
|
380
466
|
/**
|
|
381
467
|
* Recovering: re-read tools/list (presence, declared definitions), then
|
|
382
468
|
* toolsets/list (dynamic-tool state in full), so every tool the statement
|
|
383
|
-
* names is already known
|
|
384
|
-
*
|
|
469
|
+
* names is already known, unless tools/list failed: a tool the last list
|
|
470
|
+
* lacks then stays unknown until a later change or list starts a recovery.
|
|
471
|
+
* Each applies where it's read. What
|
|
472
|
+
* changed is told. (stdio and WebSocket deliver in order, so no early rev is
|
|
473
|
+
* held.)
|
|
385
474
|
*/
|
|
386
475
|
private async recover(): Promise<void> {
|
|
387
476
|
const connection = this.connection;
|
|
388
477
|
if (!connection || !connection.isConnected) return;
|
|
389
478
|
this.recovering = true;
|
|
390
479
|
try {
|
|
391
|
-
|
|
480
|
+
await this.ownToolList(connection);
|
|
481
|
+
// RFC §6.1: toolsets/list after tools/list, so every tool it names is known, unless tools/list failed.
|
|
392
482
|
if (connection !== this.connection) return;
|
|
393
483
|
const lists = await this.ownToolsetList(connection, LIST_TIMEOUT_MS, 'recovery');
|
|
394
484
|
if (connection !== this.connection) return;
|
|
395
|
-
this.deliver(
|
|
485
|
+
this.deliver(lists);
|
|
396
486
|
} finally {
|
|
397
487
|
this.recovering = false;
|
|
398
488
|
if (this.recoverAgain) {
|
|
@@ -402,29 +492,21 @@ export class ToolsetController {
|
|
|
402
492
|
}
|
|
403
493
|
}
|
|
404
494
|
|
|
405
|
-
|
|
406
|
-
|
|
495
|
+
/** The tap applies the list and tells its change as it's read. */
|
|
496
|
+
private async ownToolList(connection: McplServerConnection): Promise<void> {
|
|
407
497
|
try {
|
|
408
|
-
// The tap applied each page as it was read, and kept what the whole
|
|
409
|
-
// list changed (ownListChange).
|
|
410
498
|
await connection.sendToolsList();
|
|
411
499
|
} catch (error) {
|
|
412
|
-
this.log(`tools/list failed: ${
|
|
413
|
-
} finally {
|
|
414
|
-
this.ownLists--;
|
|
500
|
+
this.log(`tools/list failed: ${this.failure(error)}`);
|
|
415
501
|
}
|
|
416
|
-
const collected = this.ownListChange ?? emptyChange();
|
|
417
|
-
if (this.ownLists === 0) this.ownListChange = null;
|
|
418
|
-
else this.ownListChange = emptyChange();
|
|
419
|
-
return collected;
|
|
420
502
|
}
|
|
421
503
|
|
|
422
504
|
private async ownToolsetList(connection: McplServerConnection, timeoutMs: number, why: string): Promise<ChangeSet> {
|
|
423
505
|
try {
|
|
424
506
|
const r = await connection.sendToolsetsList(timeoutMs);
|
|
425
|
-
return this.
|
|
507
|
+
return this.takeChange(r);
|
|
426
508
|
} catch (error) {
|
|
427
|
-
this.log(`toolsets/list (${why}) failed: ${
|
|
509
|
+
this.log(`toolsets/list (${why}) failed: ${this.failure(error)}`);
|
|
428
510
|
return emptyChange();
|
|
429
511
|
}
|
|
430
512
|
}
|
|
@@ -441,35 +523,41 @@ export class ToolsetController {
|
|
|
441
523
|
this.sync(connection.transportEpoch, connection);
|
|
442
524
|
if (!this.presence.active) return;
|
|
443
525
|
const epoch = connection.transportEpoch;
|
|
444
|
-
|
|
526
|
+
await this.ownToolList(connection);
|
|
445
527
|
if (connection !== this.connection || connection.transportEpoch !== epoch) return;
|
|
446
528
|
const lists = await this.ownToolsetList(connection, LIST_TIMEOUT_MS, 'after connecting');
|
|
447
529
|
if (connection !== this.connection || connection.transportEpoch !== epoch) return;
|
|
448
|
-
this.deliver(
|
|
530
|
+
this.deliver(lists);
|
|
449
531
|
}
|
|
450
532
|
|
|
451
533
|
/**
|
|
452
534
|
* Re-read what the server holds open (titles, visibility) without telling
|
|
453
|
-
* it
|
|
454
|
-
*
|
|
535
|
+
* it, for look. Returns what the re-read changed, taken from the notice it
|
|
536
|
+
* would otherwise get when `shown` (its result reaches an agent): look then
|
|
537
|
+
* either shows it (told) or hands it back (tell). Otherwise the change is
|
|
538
|
+
* left to its notice, and returned to be shown to the caller alone. Null
|
|
539
|
+
* when nothing was read.
|
|
455
540
|
*/
|
|
456
|
-
async refreshOpen(): Promise<
|
|
457
|
-
if (!this.presence.active) return
|
|
541
|
+
async refreshOpen(opts: { shown: boolean }): Promise<ChangeSet | null> {
|
|
542
|
+
if (!this.presence.active) return null;
|
|
458
543
|
const connection = this.connection;
|
|
459
|
-
if (!connection || !connection.isConnected) return
|
|
544
|
+
if (!connection || !connection.isConnected) return null;
|
|
460
545
|
try {
|
|
461
546
|
const r = await connection.sendToolsetsList(REFRESH_TIMEOUT_MS);
|
|
462
|
-
|
|
547
|
+
if (!isObj(r)) return null;
|
|
548
|
+
return opts.shown ? this.takeChange(r) : this.changeOf(r);
|
|
463
549
|
} catch (error) {
|
|
464
|
-
this.log(`toolsets/list (look) failed: ${
|
|
465
|
-
return
|
|
550
|
+
this.log(`toolsets/list (look) failed: ${this.failure(error)}`);
|
|
551
|
+
return null;
|
|
466
552
|
}
|
|
467
553
|
}
|
|
468
554
|
|
|
469
555
|
// ── the host's acts ──
|
|
470
556
|
|
|
471
|
-
/** toolsets/open (RFC §5.2). The server's refusal rejects.
|
|
472
|
-
|
|
557
|
+
/** toolsets/open (RFC §5.2). The server's refusal rejects. Its change is
|
|
558
|
+
* taken when `shown` (its result reaches an agent), else left to its
|
|
559
|
+
* notice and returned for the caller alone; close's is the same. */
|
|
560
|
+
async open(toolset: string, instance: string | undefined, options: Record<string, unknown> | undefined, opts: { shown: boolean }): Promise<{
|
|
473
561
|
key: string; title?: string; content: unknown[]; change: ChangeSet; adopted: boolean;
|
|
474
562
|
}> {
|
|
475
563
|
const connection = this.live();
|
|
@@ -477,24 +565,24 @@ export class ToolsetController {
|
|
|
477
565
|
const res = isObj(r) ? r : {};
|
|
478
566
|
const inst = typeof res.instance === 'string' && res.instance ? res.instance : instance;
|
|
479
567
|
// RFC §5.2: a proposed name is used as given or the open refused.
|
|
480
|
-
if (instance && inst !== instance) this.log(`proposed instance
|
|
568
|
+
if (instance && inst !== instance) this.log(`proposed instance ${this.field(instance)} came back as ${this.field(inst)} (RFC-014 §5.2 forbids renaming): using the server's`);
|
|
481
569
|
const key = keyOf(toolset, this.presence.declared[toolset]?.instances ? inst : undefined);
|
|
482
570
|
const title = typeof res.title === 'string' && res.title ? res.title : this.presence.openRecords().find((o) => keyOf(o.toolset, o.instance) === key)?.title;
|
|
483
571
|
return {
|
|
484
572
|
key,
|
|
485
573
|
...(title ? { title } : {}),
|
|
486
574
|
content: Array.isArray(res.content) ? res.content : [],
|
|
487
|
-
change: this.changeOf(res),
|
|
575
|
+
change: opts.shown ? this.takeChange(res) : this.changeOf(res),
|
|
488
576
|
adopted: res.adopted === true,
|
|
489
577
|
};
|
|
490
578
|
}
|
|
491
579
|
|
|
492
580
|
/** toolsets/close (RFC §5.2). */
|
|
493
|
-
async close(toolset: string, instance: string | undefined): Promise<{ content: unknown[]; change: ChangeSet }> {
|
|
581
|
+
async close(toolset: string, instance: string | undefined, opts: { shown: boolean }): Promise<{ content: unknown[]; change: ChangeSet }> {
|
|
494
582
|
const connection = this.live();
|
|
495
583
|
const r = await connection.sendToolsetsClose({ toolset, ...(instance ? { instance } : {}) }, TOOLSET_REQUEST_TIMEOUT_MS);
|
|
496
584
|
const res = isObj(r) ? r : {};
|
|
497
|
-
return { content: Array.isArray(res.content) ? res.content : [], change: this.changeOf(res) };
|
|
585
|
+
return { content: Array.isArray(res.content) ? res.content : [], change: opts.shown ? this.takeChange(res) : this.changeOf(res) };
|
|
498
586
|
}
|
|
499
587
|
|
|
500
588
|
/** Open or close an undeclared group (RFC §5.4): the host's own act; the server isn't told. */
|
|
@@ -518,7 +606,7 @@ export class ToolsetController {
|
|
|
518
606
|
this.presence.deactivate();
|
|
519
607
|
this.log('the server withdrew toolHints: every tool is static from now on');
|
|
520
608
|
const cs = this.presence.diff(before, this.presence.view());
|
|
521
|
-
cs.notes.push(`${this.serverId} no longer presents toolsets: its tools are
|
|
609
|
+
cs.notes.push(`${this.serverId} no longer presents toolsets: its tools are called directly now, by name, not through ${META_TOOL_NAMES.act}.`);
|
|
522
610
|
this.deliver(cs);
|
|
523
611
|
return;
|
|
524
612
|
}
|
|
@@ -532,11 +620,15 @@ export class ToolsetController {
|
|
|
532
620
|
this.deliver(this.presence.diff(before, this.presence.view()));
|
|
533
621
|
}
|
|
534
622
|
|
|
535
|
-
/**
|
|
536
|
-
applyMoves(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
623
|
+
/** At a rebuild of the model's tool list (RFC §4.2), tell where the tools it moved went: `moves`, those still pending, or every one pending. A move a list has undone since the rebuild listed the server isn't told; the next rebuild publishes the undoing. */
|
|
624
|
+
applyMoves(moves?: Array<{ name: string; to: Presence }>): void {
|
|
625
|
+
const moved = this.presence.applyMoves(moves);
|
|
626
|
+
if (!moved.length) return;
|
|
627
|
+
const cs = emptyChange();
|
|
628
|
+
cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
|
|
629
|
+
? `${nameToTypeBack(name)} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(name)}, args}.`
|
|
630
|
+
: `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
|
|
631
|
+
this.deliver(cs);
|
|
540
632
|
}
|
|
541
633
|
}
|
|
542
634
|
|
|
@@ -565,8 +657,11 @@ export class ToolsetHub {
|
|
|
565
657
|
return this.controllers.get(serverId);
|
|
566
658
|
}
|
|
567
659
|
|
|
568
|
-
/** A server's configuration went (removed):
|
|
660
|
+
/** A server's configuration went (removed): its controller lets go of the
|
|
661
|
+
* connection it holds, and is forgotten, so a server added later under
|
|
662
|
+
* the same id starts from nothing. */
|
|
569
663
|
forget(serverId: string): void {
|
|
664
|
+
this.controllers.get(serverId)?.release();
|
|
570
665
|
this.controllers.delete(serverId);
|
|
571
666
|
}
|
|
572
667
|
|
|
@@ -591,8 +686,9 @@ export class ToolsetHub {
|
|
|
591
686
|
* reminder due.
|
|
592
687
|
*/
|
|
593
688
|
observeCompile(agentName: string, messages: readonly unknown[]): void {
|
|
689
|
+
const host = this.deps.hostOpen?.(agentName) ?? [];
|
|
594
690
|
const open = this.openMarkers();
|
|
595
|
-
if (open.length === 0) {
|
|
691
|
+
if (open.length === 0 && host.length === 0) {
|
|
596
692
|
this.seen.delete(agentName);
|
|
597
693
|
this.due.delete(agentName);
|
|
598
694
|
return;
|
|
@@ -606,6 +702,9 @@ export class ToolsetHub {
|
|
|
606
702
|
}
|
|
607
703
|
for (const marker of [...seen]) if (!open.includes(marker)) seen.delete(marker);
|
|
608
704
|
this.seen.set(agentName, seen);
|
|
705
|
+
// A host toolset open across a restart was never seen here: it's due
|
|
706
|
+
// whenever a request doesn't carry its marker.
|
|
707
|
+
for (const h of host) if (h.dueWhenAbsent && !text.includes(h.marker)) missing = true;
|
|
609
708
|
// Due until a request carries every marker the agent has seen again: a
|
|
610
709
|
// gather that doesn't send (a preview) leaves it due for the next start.
|
|
611
710
|
if (missing) this.due.add(agentName);
|
|
@@ -615,7 +714,10 @@ export class ToolsetHub {
|
|
|
615
714
|
/** The reminder due for `agentName`, or null when none is. */
|
|
616
715
|
reminderFor(agentName: string): string | null {
|
|
617
716
|
if (!this.due.has(agentName)) return null;
|
|
618
|
-
const parts =
|
|
717
|
+
const parts = [
|
|
718
|
+
...(this.deps.hostOpen?.(agentName) ?? []).map((h) => h.reminder()),
|
|
719
|
+
...this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r): r is string => !!r),
|
|
720
|
+
];
|
|
619
721
|
if (parts.length === 0) return null;
|
|
620
722
|
return `Open toolsets (a reminder: their earlier announcements have left your context):\n${parts.join('\n')}`;
|
|
621
723
|
}
|