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