@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
|
@@ -26,8 +26,9 @@ import type { EventResponse, Module, ModuleContext, ModuleContextInjection, Proc
|
|
|
26
26
|
import type { ProcessEvent, ToolCall, ToolDefinition, ToolResult } from '../types/events.js';
|
|
27
27
|
import { unknownToolMessage } from '../modules/unknown-tool.js';
|
|
28
28
|
import { META_TOOL_NAMES, TOOLSETS_MODULE, type ToolsetController, type ToolsetHub } from './toolset-host.js';
|
|
29
|
-
import { keyOf, type ChangeSet } from './tool-presence.js';
|
|
30
|
-
import { checkArgs, renderToolDefinition, type HintedTool } from './tool-signature.js';
|
|
29
|
+
import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
|
|
30
|
+
import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
|
|
31
|
+
import { McplRequestError } from './server-connection.js';
|
|
31
32
|
|
|
32
33
|
/** The namespace a reminder's stored injection carries. */
|
|
33
34
|
export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
|
|
@@ -55,7 +56,7 @@ const META_TOOLS: ToolDefinition[] = [
|
|
|
55
56
|
description:
|
|
56
57
|
'Call a tool that was announced in the conversation (a tool of an open toolset, or another dynamic tool); they are not in your tool list. ' +
|
|
57
58
|
'args are the arguments its signature shows; instance picks the open copy when several are open; server is needed only when two servers have the tool. ' +
|
|
58
|
-
'Returns the server\'s result, then any change in which tools are available.',
|
|
59
|
+
'Returns the server\'s result, then any change it made in which tools are available, unless that change already went out as a notice. toolsets--look shows what is available now.',
|
|
59
60
|
inputSchema: {
|
|
60
61
|
type: 'object',
|
|
61
62
|
properties: {
|
|
@@ -84,7 +85,7 @@ const META_TOOLS: ToolDefinition[] = [
|
|
|
84
85
|
},
|
|
85
86
|
{
|
|
86
87
|
name: 'close',
|
|
87
|
-
description: 'Close an open toolset or instance: its tools and its events
|
|
88
|
+
description: 'Close an open toolset or instance: its tools stop, and for a toolset the server declares, the server is told and stops its events too. Closing a game in progress may count as leaving it.',
|
|
88
89
|
inputSchema: {
|
|
89
90
|
type: 'object',
|
|
90
91
|
properties: {
|
|
@@ -97,6 +98,33 @@ const META_TOOLS: ToolDefinition[] = [
|
|
|
97
98
|
];
|
|
98
99
|
|
|
99
100
|
const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
101
|
+
|
|
102
|
+
const SELECTORS = ['server', 'toolset', 'instance', 'tool'] as const;
|
|
103
|
+
|
|
104
|
+
function checkSelectors(args: Record<string, unknown>): void {
|
|
105
|
+
for (const key of SELECTORS) {
|
|
106
|
+
const v = args[key];
|
|
107
|
+
if (v === undefined || v === null || str(v) !== undefined) continue;
|
|
108
|
+
const got = typeof v === 'string' ? 'a blank string' : Array.isArray(v) ? 'a list' : typeof v === 'object' ? 'an object' : `a ${typeof v}`;
|
|
109
|
+
fail(`${key} must be a name, or left out; got ${got}, so nothing was done`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A failure whose outcome is unknown, with how to find it out. */
|
|
114
|
+
export function withLookHint(serverId: string, text: string): string {
|
|
115
|
+
const said = text.trimEnd();
|
|
116
|
+
return `${said}${/[.!?]$/.test(said) ? '' : '.'} ${META_TOOL_NAMES.look} {server:"${serverId}"} asks it what's open, or says it couldn't.`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function requestFailure(serverId: string, did: string, unconfirmed: string, error: unknown): never {
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
if (error instanceof McplRequestError && error.outcome === 'not-sent') return fail(`${serverId} ${did}: ${message}`);
|
|
122
|
+
return fail(withLookHint(serverId, `${serverId} ${unconfirmed}: ${message}`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A result's tool change, for whoever shows it with the result (ToolsetsModule's takerFor). */
|
|
126
|
+
export type TakenChange = { toolsets: ToolsetController; cs: ChangeSet };
|
|
127
|
+
|
|
100
128
|
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
101
129
|
|
|
102
130
|
type Block = { type: string; text?: string; [k: string]: unknown };
|
|
@@ -142,11 +170,11 @@ function pick(hub: ToolsetHub, server: unknown): ToolsetController {
|
|
|
142
170
|
const ids = hub.presenting().map((c) => c.serverId);
|
|
143
171
|
if (!id) {
|
|
144
172
|
if (ids.length === 1) return hub.get(ids[0]!)!;
|
|
145
|
-
return fail(`name a server: ${ids.join(', ')
|
|
173
|
+
return fail(ids.length ? `name a server: ${ids.join(', ')}` : NONE_PRESENTS);
|
|
146
174
|
}
|
|
147
175
|
const c = hub.get(id);
|
|
148
176
|
if (!c) return fail(`no server "${id}" presents toolsets; the ones that do: ${ids.join(', ') || 'none'}`);
|
|
149
|
-
if (!c.presence.active) return fail(`${id} sends no tool hints now: its tools are
|
|
177
|
+
if (!c.presence.active) return fail(`${id} sends no tool hints now: its tools are called directly, by name, not through ${META_TOOL_NAMES.act}`);
|
|
150
178
|
return c;
|
|
151
179
|
}
|
|
152
180
|
|
|
@@ -158,9 +186,15 @@ function owner(hub: ToolsetHub, server: unknown, has: (c: ToolsetController) =>
|
|
|
158
186
|
if (str(server)) return pick(hub, server);
|
|
159
187
|
const owners = hub.presenting().filter(has);
|
|
160
188
|
if (owners.length === 1) return owners[0]!;
|
|
161
|
-
return fail(owners.
|
|
162
|
-
|
|
163
|
-
|
|
189
|
+
if (owners.length) return fail(`${what} is on several servers (${owners.map((c) => c.serverId).join(', ')}): add server`);
|
|
190
|
+
return fail(hub.presenting().length ? `no server that presents toolsets has ${what}: ${META_TOOL_NAMES.look} shows what they have` : NONE_PRESENTS);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const NONE_PRESENTS = 'no MCPL server presents toolsets now: MCPL tools are called directly, by name';
|
|
194
|
+
|
|
195
|
+
function noToolset(c: ToolsetController, toolset: string): string {
|
|
196
|
+
const names = [...Object.keys(c.presence.declared), ...c.presence.undeclaredGroups];
|
|
197
|
+
return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.join(', ')}` : 'it has none'}`;
|
|
164
198
|
}
|
|
165
199
|
|
|
166
200
|
/** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
|
|
@@ -171,24 +205,33 @@ export type ActTarget = { ok: true; serverId: string; tool: string; instance?: s
|
|
|
171
205
|
* or the reason it can't be sent. The framework then dispatches it exactly as
|
|
172
206
|
* a direct call of that tool, with `_meta["mcpl/instance"]` added (RFC §5.5).
|
|
173
207
|
* Refused here, before anything is sent: a tool no presenting server has, a
|
|
174
|
-
*
|
|
208
|
+
* tool neither the published list nor the server's latest tools/list puts as dynamic (RFC §4.2), an instance that isn't named where
|
|
175
209
|
* several are open, a group the host has closed (RFC §5.4), and arguments
|
|
176
210
|
* that don't fit the definition in force for that instance (RFC §6.1).
|
|
177
211
|
*/
|
|
212
|
+
const hasInstances = (p: ToolPresence, ts: string | undefined): boolean => ts !== undefined && p.isDeclared(ts) && !!p.declared[ts]!.instances;
|
|
213
|
+
|
|
214
|
+
/** The key whose definition act checks a call of `tool` against (RFC §6.1): an instance's own, else the toolset's, else the listed one. */
|
|
215
|
+
function actKey(p: ToolPresence, tool: string, instance: string | undefined): string {
|
|
216
|
+
const ts = p.toolsetOf(tool);
|
|
217
|
+
return ts ? keyOf(ts, hasInstances(p, ts) ? instance : undefined) : keyOf('');
|
|
218
|
+
}
|
|
219
|
+
|
|
178
220
|
export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
|
|
179
221
|
try {
|
|
180
222
|
const args = isObj(input) ? input : {};
|
|
223
|
+
checkSelectors(args);
|
|
181
224
|
const tool = str(args.tool) ?? fail('name the tool');
|
|
182
225
|
const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
|
|
183
226
|
const p = c.presence;
|
|
184
227
|
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
|
|
185
|
-
if (p.
|
|
228
|
+
if (!p.shownAs(tool, 'dynamic')) fail(`${tool} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
|
|
186
229
|
if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
|
|
187
230
|
const callArgs = (args.args ?? {}) as Record<string, unknown>;
|
|
188
231
|
let instance = str(args.instance);
|
|
189
232
|
const ts = p.toolsetOf(tool);
|
|
190
233
|
const declared = ts !== undefined && p.isDeclared(ts);
|
|
191
|
-
const withInstances =
|
|
234
|
+
const withInstances = hasInstances(p, ts);
|
|
192
235
|
if (withInstances && !instance) {
|
|
193
236
|
// RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
|
|
194
237
|
const open = p.openRecords(ts);
|
|
@@ -202,7 +245,7 @@ export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
|
|
|
202
245
|
// RFC §6.1: arguments are checked against the definition in force for
|
|
203
246
|
// that instance: its own from hints (the legal moves of one game), else
|
|
204
247
|
// the toolset's, else the listed one.
|
|
205
|
-
const key =
|
|
248
|
+
const key = actKey(p, tool, instance);
|
|
206
249
|
const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
|
|
207
250
|
if (problem) {
|
|
208
251
|
fail(`Invalid arguments for ${tool} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
|
|
@@ -218,7 +261,13 @@ export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
|
|
|
218
261
|
export class ToolsetsModule implements Module {
|
|
219
262
|
readonly name = TOOLSETS_MODULE;
|
|
220
263
|
|
|
221
|
-
|
|
264
|
+
/** `takerFor`: who shows a call's result to an agent, taking the tool
|
|
265
|
+
* change rendered into it; null when the result may reach none, and its
|
|
266
|
+
* change is left to a notice (RFC-014 §6.1: told once). */
|
|
267
|
+
constructor(
|
|
268
|
+
private readonly hub: ToolsetHub,
|
|
269
|
+
private readonly takerFor: (call: ToolCall) => ((taken: TakenChange) => void) | null,
|
|
270
|
+
) {}
|
|
222
271
|
|
|
223
272
|
async start(_ctx: ModuleContext): Promise<void> {}
|
|
224
273
|
async stop(): Promise<void> {}
|
|
@@ -241,11 +290,13 @@ export class ToolsetsModule implements Module {
|
|
|
241
290
|
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
242
291
|
const args = isObj(call.input) ? call.input : {};
|
|
243
292
|
try {
|
|
293
|
+
checkSelectors(args);
|
|
294
|
+
const take = this.takerFor(call);
|
|
244
295
|
switch (call.name) {
|
|
245
|
-
case 'open': return await this.open(args);
|
|
296
|
+
case 'open': return await this.open(args, take);
|
|
246
297
|
case 'act': return this.act(args);
|
|
247
|
-
case 'look': return await this.look(args);
|
|
248
|
-
case 'close': return await this.close(args);
|
|
298
|
+
case 'look': return await this.look(args, take);
|
|
299
|
+
case 'close': return await this.close(args, take);
|
|
249
300
|
default: return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, META_TOOLS) };
|
|
250
301
|
}
|
|
251
302
|
} catch (error) {
|
|
@@ -261,13 +312,14 @@ export class ToolsetsModule implements Module {
|
|
|
261
312
|
}
|
|
262
313
|
|
|
263
314
|
/** A change set rendered without the key the caller already showed whole. */
|
|
264
|
-
private renderOthers(c: ToolsetController, cs: ChangeSet, key: string): string {
|
|
265
|
-
return c.presence.render({ ...cs, changes: cs.changes.filter((ch) => ch.key !== key) });
|
|
315
|
+
private renderOthers(c: ToolsetController, cs: ChangeSet, key: string, shown: boolean): string {
|
|
316
|
+
return c.presence.render({ ...cs, changes: cs.changes.filter((ch) => ch.key !== key) }, { mark: shown, describeAll: !shown });
|
|
266
317
|
}
|
|
267
318
|
|
|
268
319
|
// ── the tools ──
|
|
269
320
|
|
|
270
|
-
private async open(args: Record<string, unknown
|
|
321
|
+
private async open(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
322
|
+
const shown = take !== null;
|
|
271
323
|
let toolset = str(args.toolset);
|
|
272
324
|
const c = toolset && !str(args.server)
|
|
273
325
|
? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
|
|
@@ -286,23 +338,24 @@ export class ToolsetsModule implements Module {
|
|
|
286
338
|
if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
|
|
287
339
|
let opened: Awaited<ReturnType<ToolsetController['open']>>;
|
|
288
340
|
try {
|
|
289
|
-
opened = await c.open(toolset, instance, options);
|
|
341
|
+
opened = await c.open(toolset, instance, options, { shown });
|
|
290
342
|
} catch (error) {
|
|
291
|
-
return
|
|
343
|
+
return requestFailure(c.serverId, `did not open ${toolset}`, `didn't confirm opening ${toolset}`, error);
|
|
292
344
|
}
|
|
293
345
|
// RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
|
|
294
346
|
const blocks: Block[] = [
|
|
295
347
|
{ type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
|
|
296
348
|
...asBlocks(opened.content),
|
|
297
349
|
];
|
|
298
|
-
const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false })];
|
|
350
|
+
const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
|
|
299
351
|
const gone = opened.change.changes.find((ch) => ch.key === opened.key)?.gone ?? [];
|
|
300
352
|
if (gone.length) tail.push(`now unavailable: ${gone.join(', ')}`);
|
|
301
|
-
const others = this.renderOthers(c, opened.change, opened.key);
|
|
353
|
+
const others = this.renderOthers(c, opened.change, opened.key, shown);
|
|
302
354
|
if (others) tail.push(others);
|
|
303
355
|
const close = p.closeHint(opened.key);
|
|
304
356
|
if (close) tail.push(`${close} closes it.`);
|
|
305
357
|
blocks.push({ type: 'text', text: tail.join('\n') });
|
|
358
|
+
take?.({ toolsets: c, cs: opened.change });
|
|
306
359
|
return blocksResult(blocks);
|
|
307
360
|
}
|
|
308
361
|
if (p.undeclaredGroups.includes(toolset)) {
|
|
@@ -310,13 +363,15 @@ export class ToolsetsModule implements Module {
|
|
|
310
363
|
if (instance || options) fail(`${c.serverId}/${toolset} takes no instance or options (the server doesn't declare it)`);
|
|
311
364
|
const cs = c.setGroupOpen(toolset, true);
|
|
312
365
|
const key = keyOf(toolset);
|
|
313
|
-
const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false })];
|
|
314
|
-
const others = this.renderOthers(c, cs, key);
|
|
366
|
+
const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
|
|
367
|
+
const others = this.renderOthers(c, cs, key, shown);
|
|
315
368
|
if (others) lines.push(others);
|
|
316
369
|
lines.push(`${p.closeHint(key)} closes it.`);
|
|
370
|
+
if (take) take({ toolsets: c, cs });
|
|
371
|
+
else c.tell(cs);
|
|
317
372
|
return textResult(lines.join('\n'));
|
|
318
373
|
}
|
|
319
|
-
return fail(
|
|
374
|
+
return fail(noToolset(c, toolset));
|
|
320
375
|
}
|
|
321
376
|
|
|
322
377
|
/**
|
|
@@ -333,15 +388,43 @@ export class ToolsetsModule implements Module {
|
|
|
333
388
|
}
|
|
334
389
|
|
|
335
390
|
/** look restates the present: titles and visibility are re-read from the servers first (toolsets/list). */
|
|
336
|
-
private async look(args: Record<string, unknown
|
|
391
|
+
private async look(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
337
392
|
const server = str(args.server);
|
|
338
393
|
const targets = server ? [this.hub.get(server)].filter((c): c is ToolsetController => !!c && c.presents) : this.hub.presenting();
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
394
|
+
const shown = take !== null;
|
|
395
|
+
const reads = await Promise.all(targets.map(async (c) => ({ c, cs: await c.refreshOpen({ shown }).catch(() => null) })));
|
|
396
|
+
const read = reads.filter((r): r is { c: ToolsetController; cs: ChangeSet } => r.cs !== null);
|
|
397
|
+
const unread = reads.filter((r) => r.cs === null).map(({ c }) => `${c.serverId} couldn't be asked just now: this is what the host last knew of it.`);
|
|
398
|
+
const giveBack = () => {
|
|
399
|
+
if (shown) for (const { c, cs } of read) c.tell(cs);
|
|
400
|
+
};
|
|
401
|
+
let result: ToolResult;
|
|
402
|
+
try {
|
|
403
|
+
result = this.lookNow(args, server, str(args.tool), shown);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
giveBack();
|
|
406
|
+
if (error instanceof MetaError && unread.length) fail([error.message, ...unread].join('\n'));
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
if (result.isError || typeof result.data !== 'string') {
|
|
410
|
+
giveBack();
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
const restates = !str(args.tool) && !str(args.toolset) && !str(args.instance);
|
|
414
|
+
if (!restates) giveBack();
|
|
415
|
+
const lines = restates
|
|
416
|
+
? read.flatMap(({ c, cs }) => {
|
|
417
|
+
const said = [...cs.suggest.map((s) => c.presence.renderSuggestion(s)), ...c.presence.untoldNotes(cs), ...cs.notes];
|
|
418
|
+
// The catalog above counted what it showed; the change's own blocks weren't shown, but stay for a notice if its taker can't show the result.
|
|
419
|
+
take?.({ toolsets: c, cs: { ...cs, blocksShown: false } });
|
|
420
|
+
return said;
|
|
421
|
+
})
|
|
422
|
+
: [];
|
|
423
|
+
lines.push(...unread);
|
|
424
|
+
return lines.length ? textResult(`${result.data}\n${lines.join('\n')}`) : result;
|
|
342
425
|
}
|
|
343
426
|
|
|
344
|
-
private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined): ToolResult {
|
|
427
|
+
private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined, mark: boolean): ToolResult {
|
|
345
428
|
if (!server) {
|
|
346
429
|
if (tool) return this.lookTool(owner(this.hub, undefined, (c) => !!c.presence.tool(tool), `a tool "${tool}"`), tool, str(args.instance));
|
|
347
430
|
const ts = str(args.toolset);
|
|
@@ -352,23 +435,27 @@ export class ToolsetsModule implements Module {
|
|
|
352
435
|
(x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
|
|
353
436
|
ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
|
|
354
437
|
);
|
|
355
|
-
return this.lookNow(args, c.serverId, tool);
|
|
438
|
+
return this.lookNow(args, c.serverId, tool, mark);
|
|
356
439
|
}
|
|
357
440
|
const all = this.hub.presenting();
|
|
358
|
-
if (!all.length) return textResult('No MCPL server presents toolsets or dynamic tools now;
|
|
359
|
-
return textResult(all.map((c) => c.presence.catalog()).join('\n\n'));
|
|
441
|
+
if (!all.length) return textResult('No MCPL server presents toolsets or dynamic tools now; MCPL tools are called directly, by name.');
|
|
442
|
+
return textResult(all.map((c) => c.presence.catalog({ mark })).join('\n\n'));
|
|
360
443
|
}
|
|
361
444
|
const c = pick(this.hub, server);
|
|
362
445
|
if (tool) return this.lookTool(c, tool, str(args.instance));
|
|
363
446
|
const p = c.presence;
|
|
364
447
|
let toolset = str(args.toolset);
|
|
365
448
|
const instance = str(args.instance);
|
|
366
|
-
if (!toolset && instance)
|
|
367
|
-
|
|
449
|
+
if (!toolset && instance) {
|
|
450
|
+
const open = p.openRecords().filter((o) => o.instance !== undefined);
|
|
451
|
+
toolset = open.find((o) => o.instance === instance)?.toolset
|
|
452
|
+
?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${o.toolset}:${o.instance}`).join(', ')}` : 'no instance is open there'}`);
|
|
453
|
+
}
|
|
454
|
+
if (!toolset) return textResult(p.catalog({ mark }));
|
|
368
455
|
if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
|
|
369
456
|
const rec = p.isDeclared(toolset) ? (instance ? p.openRecords(toolset).find((o) => o.instance === instance) : p.openRecords(toolset)[0]) : undefined;
|
|
370
457
|
const key = keyOf(toolset, rec?.instance);
|
|
371
|
-
const lines = p.toolBlock(key, this.toolsAt(c, key), { describeAll: true, title: rec?.title });
|
|
458
|
+
const lines = p.toolBlock(key, this.toolsAt(c, key), { describeAll: true, title: rec?.title, seen: mark ? undefined : new Set<string>() });
|
|
372
459
|
const close = p.closeHint(key);
|
|
373
460
|
if (close) lines.push(`${close} closes it.`);
|
|
374
461
|
return textResult(lines.join('\n'));
|
|
@@ -380,7 +467,7 @@ export class ToolsetsModule implements Module {
|
|
|
380
467
|
`${c.serverId}/${toolset}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}`,
|
|
381
468
|
open.length ? `open: ${open.map((o) => o.instance ?? toolset).join(', ')}` : 'not open',
|
|
382
469
|
...(d.instances ? ['several can be open at once; name each with instance'] : []),
|
|
383
|
-
...(d.options ? [`options: ${
|
|
470
|
+
...(d.options ? [`options: ${jsonText(d.options)}`] : []),
|
|
384
471
|
`${p.openHint(toolset)} opens it.`,
|
|
385
472
|
].join('\n'));
|
|
386
473
|
}
|
|
@@ -388,20 +475,22 @@ export class ToolsetsModule implements Module {
|
|
|
388
475
|
const names = p.allTools.filter((t) => p.toolsetOf(t.name) === toolset).map((t) => t.name);
|
|
389
476
|
return textResult(`${c.serverId}/${toolset}: ${names.join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
|
|
390
477
|
}
|
|
391
|
-
return fail(
|
|
478
|
+
return fail(noToolset(c, toolset!));
|
|
392
479
|
}
|
|
393
480
|
|
|
394
481
|
private lookTool(c: ToolsetController, tool: string, instance: string | undefined): ToolResult {
|
|
395
482
|
const p = c.presence;
|
|
396
|
-
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}"`);
|
|
483
|
+
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
|
|
397
484
|
const ts = p.toolsetOf(tool);
|
|
398
|
-
|
|
399
|
-
const
|
|
485
|
+
// The definition act would check, wherever act would take the tool.
|
|
486
|
+
const open = !instance && hasInstances(p, ts) ? p.openRecords(ts) : [];
|
|
487
|
+
const key = !p.shownAs(tool, 'dynamic') ? undefined : actKey(p, tool, instance ?? (open.length === 1 ? open[0]!.instance : undefined));
|
|
400
488
|
const def = key ? p.defOf(key, tool) : p.tool(tool)!;
|
|
401
489
|
return textResult(`${renderToolDefinition(def)}\n${p.toolStatus(tool)}`);
|
|
402
490
|
}
|
|
403
491
|
|
|
404
|
-
private async close(args: Record<string, unknown
|
|
492
|
+
private async close(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
493
|
+
const shown = take !== null;
|
|
405
494
|
const named = str(args.toolset);
|
|
406
495
|
const c = named && !str(args.server)
|
|
407
496
|
? owner(this.hub, undefined, (x) => x.presence.isOpen(named, str(args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
|
|
@@ -432,20 +521,25 @@ export class ToolsetsModule implements Module {
|
|
|
432
521
|
}
|
|
433
522
|
let closed: Awaited<ReturnType<ToolsetController['close']>>;
|
|
434
523
|
try {
|
|
435
|
-
closed = await c.close(toolset!, instance);
|
|
524
|
+
closed = await c.close(toolset!, instance, { shown });
|
|
436
525
|
} catch (error) {
|
|
437
|
-
|
|
526
|
+
const what = `${toolset}${instance ? `:${instance}` : ''}`;
|
|
527
|
+
return requestFailure(c.serverId, `did not close ${what}`, `didn't confirm closing ${what}`, error);
|
|
438
528
|
}
|
|
439
529
|
const key = keyOf(toolset!, instance);
|
|
440
|
-
const change = p.render(closed.change) || `${p.label(key)} closed.`;
|
|
530
|
+
const change = p.render(closed.change, { mark: shown, describeAll: !shown }) || `${p.label(key)} closed.`;
|
|
531
|
+
take?.({ toolsets: c, cs: closed.change });
|
|
441
532
|
return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
|
|
442
533
|
}
|
|
443
534
|
if (p.undeclaredGroups.includes(toolset!)) {
|
|
444
535
|
if (instance) fail(`${c.serverId}/${toolset} takes no instance (the server doesn't declare it)`);
|
|
445
536
|
if (!p.isOpen(toolset!)) fail(`${c.serverId}/${toolset} is not open`);
|
|
446
537
|
const cs = c.setGroupOpen(toolset!, false);
|
|
447
|
-
|
|
538
|
+
const text = p.render(cs, { mark: shown, describeAll: !shown }) || `${p.label(keyOf(toolset!))} closed.`;
|
|
539
|
+
if (take) take({ toolsets: c, cs });
|
|
540
|
+
else c.tell(cs);
|
|
541
|
+
return textResult(text);
|
|
448
542
|
}
|
|
449
|
-
return fail(
|
|
543
|
+
return fail(noToolset(c, toolset!));
|
|
450
544
|
}
|
|
451
545
|
}
|
package/src/mcpl/types.ts
CHANGED
|
@@ -384,7 +384,9 @@ export interface McplServerConfig {
|
|
|
384
384
|
*
|
|
385
385
|
* `disabledCapabilities` takes precedence on conflict. `version` and
|
|
386
386
|
* `featureSets` are not maskable here — feature sets have their own
|
|
387
|
-
* enable/disable knobs above
|
|
387
|
+
* enable/disable knobs above — and neither are RFC-014's `toolHints`,
|
|
388
|
+
* `toolsets` and `surface`, which a server's own `toolHints` config turns
|
|
389
|
+
* off.
|
|
388
390
|
*/
|
|
389
391
|
enabledCapabilities?: string[];
|
|
390
392
|
|
package/src/module-registry.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { heldWriteId } from './held-writes.js';
|
|
|
28
28
|
import type { Agent } from './agent.js';
|
|
29
29
|
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
|
|
30
30
|
import { logValue } from './mcpl/log-value.js';
|
|
31
|
-
import { toolArgsProblems } from './tool-args.js';
|
|
31
|
+
import { asCalled, markedInputRefusal, toolArgsProblems } from './tool-args.js';
|
|
32
32
|
|
|
33
33
|
/** How the registry queues a module's event: its push names the module. A
|
|
34
34
|
* `ProcessQueue` serves too, ignoring the name. */
|
|
@@ -317,11 +317,17 @@ export class ModuleRegistry {
|
|
|
317
317
|
* A call whose input isn't an object, or lacks a key the tool's schema
|
|
318
318
|
* requires, is answered before the module runs, with an error result that
|
|
319
319
|
* opens "Nothing was done:" and names what to send (toolArgsProblems).
|
|
320
|
+
* So is a call membrane marked `unparseableInput`, unless its text is JSON
|
|
321
|
+
* `null`, which reaches the module as a call with no arguments
|
|
322
|
+
* (markedInputRefusal).
|
|
320
323
|
* A caller that has already resolved the tool's definition, as the `utils`
|
|
321
324
|
* tool has a utility's, passes it as `definition`, and it's checked against
|
|
322
325
|
* that one.
|
|
323
326
|
*/
|
|
324
|
-
async handleToolCall(
|
|
327
|
+
async handleToolCall(given: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
|
|
328
|
+
const marked = markedInputRefusal(given);
|
|
329
|
+
if (marked !== undefined) return { success: false, error: marked, isError: true };
|
|
330
|
+
const call = asCalled(given);
|
|
325
331
|
// Parse module--tool name
|
|
326
332
|
const sepIndex = call.name.indexOf('--');
|
|
327
333
|
if (sepIndex === -1) {
|
|
@@ -535,7 +535,7 @@ export class HistoryModule implements Module {
|
|
|
535
535
|
channelId?: string;
|
|
536
536
|
n: number;
|
|
537
537
|
side: 'newest' | 'oldest';
|
|
538
|
-
}): { messages: StoredMessage[]; more: boolean } {
|
|
538
|
+
}): { messages: StoredMessage[]; more: boolean; total?: number } {
|
|
539
539
|
const cm = this.cm as ContextManager;
|
|
540
540
|
const { fromMs, toMs, channelId, n, side } = opts;
|
|
541
541
|
const newest = side === 'newest';
|
|
@@ -553,8 +553,8 @@ export class HistoryModule implements Module {
|
|
|
553
553
|
const fetch = (f: number | undefined, t: number | undefined, limit: number, offset = 0) =>
|
|
554
554
|
limit <= 0 ? [] : cm.queryMessagesByTimeAndChannel({ fromMs: f, toMs: t, channelId, limit, offset }).messages;
|
|
555
555
|
const total = count(fromMs, toMs);
|
|
556
|
-
if (n === 0) return { messages: [], more: total > 0 };
|
|
557
|
-
if (total <= n) return { messages: dirSort(fetch(fromMs, toMs, total)), more: false };
|
|
556
|
+
if (n === 0) return { messages: [], more: total > 0, total };
|
|
557
|
+
if (total <= n) return { messages: dirSort(fetch(fromMs, toMs, total)), more: false, total };
|
|
558
558
|
|
|
559
559
|
// The window is parameterised by its FAR bound `e`: [e, toMs] for the
|
|
560
560
|
// newest side, [fromMs, e] for the oldest; its count is monotone in e.
|
|
@@ -580,7 +580,7 @@ export class HistoryModule implements Module {
|
|
|
580
580
|
}
|
|
581
581
|
const fetchCap = Math.max(4 * n, n + 1000);
|
|
582
582
|
if (inCount <= fetchCap) {
|
|
583
|
-
return { messages: dirSort(fetch(...win(inside), inCount)).slice(0, n), more: true };
|
|
583
|
+
return { messages: dirSort(fetch(...win(inside), inCount)).slice(0, n), more: true, total };
|
|
584
584
|
}
|
|
585
585
|
// Overshoot: bisect the far bound. The empty side starts one past the
|
|
586
586
|
// range's near end (newest: past toMs / any date; oldest: before lo).
|
|
@@ -595,7 +595,7 @@ export class HistoryModule implements Module {
|
|
|
595
595
|
inside = mid;
|
|
596
596
|
inCount = c;
|
|
597
597
|
if (c <= fetchCap) {
|
|
598
|
-
return { messages: dirSort(fetch(...win(mid), c)).slice(0, n), more: true };
|
|
598
|
+
return { messages: dirSort(fetch(...win(mid), c)).slice(0, n), more: true, total };
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
}
|
|
@@ -607,7 +607,7 @@ export class HistoryModule implements Module {
|
|
|
607
607
|
const need = n - near.length;
|
|
608
608
|
const ties = count(inside, inside);
|
|
609
609
|
const edgeMs = fetch(inside, inside, need, newest ? Math.max(0, ties - need) : 0);
|
|
610
|
-
return { messages: dirSort([...near, ...edgeMs]).slice(0, n), more: true };
|
|
610
|
+
return { messages: dirSort([...near, ...edgeMs]).slice(0, n), more: true, total };
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
async start(ctx: ModuleContext): Promise<void> {
|
|
@@ -1150,7 +1150,8 @@ export class HistoryModule implements Module {
|
|
|
1150
1150
|
hint:
|
|
1151
1151
|
(pageFull
|
|
1152
1152
|
? 'More matching messages exist. '
|
|
1153
|
-
: `Stopped after scanning ${scanned} messages of the window without reaching its end
|
|
1153
|
+
: `Stopped after scanning ${scanned} messages of the window without reaching its end` +
|
|
1154
|
+
(page.length === 0 ? ', so returning none is not a negative result for the window. ' : '. ')) +
|
|
1154
1155
|
`Continue by repeating this call with the same from/to/channelId/author plus ` +
|
|
1155
1156
|
`the fields of \`resume\` (windowOffset:${resume.windowOffset}, offset:${resume.offset}, afterId).`,
|
|
1156
1157
|
};
|
|
@@ -1373,6 +1374,7 @@ export class HistoryModule implements Module {
|
|
|
1373
1374
|
!!skipRange && m.timestamp.getTime() === edgeMs && m.sequence >= skipRange[0] && m.sequence <= skipRange[1];
|
|
1374
1375
|
let windowMessages: StoredMessage[];
|
|
1375
1376
|
let truncated: boolean;
|
|
1377
|
+
let unscanned: number | undefined;
|
|
1376
1378
|
{
|
|
1377
1379
|
// Over-fetch by the number of messages at the edge millisecond (the
|
|
1378
1380
|
// only ones that can be skipped), then drop the skipped ones.
|
|
@@ -1381,6 +1383,7 @@ export class HistoryModule implements Module {
|
|
|
1381
1383
|
const pool = skipRange ? w.messages.filter((m) => !isSkipped(m)) : w.messages;
|
|
1382
1384
|
truncated = w.more || pool.length > maxScan;
|
|
1383
1385
|
windowMessages = pool.slice(0, maxScan);
|
|
1386
|
+
if (w.total !== undefined) unscanned = w.total - (w.messages.length - pool.length) - windowMessages.length;
|
|
1384
1387
|
}
|
|
1385
1388
|
const candidatePoolSize = windowMessages.length;
|
|
1386
1389
|
const candidates = authorFilter ? windowMessages.filter(authorFilter) : windowMessages;
|
|
@@ -1394,7 +1397,7 @@ export class HistoryModule implements Module {
|
|
|
1394
1397
|
// the pool was never looked at) or the pool itself was truncated. The
|
|
1395
1398
|
// timestamp is the continuation point — from: (oldest) / to: (newest).
|
|
1396
1399
|
// Boundary is inclusive, so a message at exactly that instant may repeat.
|
|
1397
|
-
const resumeInfo = (scanned: number): Record<string, unknown> => {
|
|
1400
|
+
const resumeInfo = (scanned: number, matched: number): Record<string, unknown> => {
|
|
1398
1401
|
const stoppedEarly = scanned < candidates.length;
|
|
1399
1402
|
if (!stoppedEarly && !truncated) return {};
|
|
1400
1403
|
// Stopped at limit: the last candidate actually looked at. Scanned the
|
|
@@ -1432,9 +1435,16 @@ export class HistoryModule implements Module {
|
|
|
1432
1435
|
hint:
|
|
1433
1436
|
(stoppedEarly
|
|
1434
1437
|
? 'Stopped at limit; more candidates remain. '
|
|
1435
|
-
:
|
|
1438
|
+
: matched === 0
|
|
1439
|
+
? `No match among the ${order} ${candidatePoolSize} ${candidatePoolSize === 1 ? 'message' : 'messages'} ` +
|
|
1440
|
+
'scanned (maxScan); ' +
|
|
1441
|
+
(unscanned === undefined
|
|
1442
|
+
? 'the rest of this range was not searched'
|
|
1443
|
+
: `${unscanned} more in this range ${unscanned === 1 ? 'was' : 'were'} not searched`) +
|
|
1444
|
+
', so this is not a negative result for the range. '
|
|
1445
|
+
: `Only the ${order} ${candidatePoolSize} messages of this range were scanned (maxScan). `) +
|
|
1436
1446
|
`Continue by repeating the call with the fields of \`resume\` (${bound}:"${at}" and skipSequences), ` +
|
|
1437
|
-
|
|
1447
|
+
`or raise limit/maxScan, or narrow with ${[channelId === undefined ? 'channelId' : null, stoppedEarly && input.author === undefined ? 'author' : null, 'dates'].filter((w) => w !== null).join('/')}.`,
|
|
1438
1448
|
};
|
|
1439
1449
|
};
|
|
1440
1450
|
|
|
@@ -1449,7 +1459,7 @@ export class HistoryModule implements Module {
|
|
|
1449
1459
|
// on a deadline. See search-regex-worker.ts's header for the full
|
|
1450
1460
|
// rationale.
|
|
1451
1461
|
const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit);
|
|
1452
|
-
return { success: true, data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned), matches } };
|
|
1462
|
+
return { success: true, data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches } };
|
|
1453
1463
|
}
|
|
1454
1464
|
|
|
1455
1465
|
// Plain substring search (String.prototype.indexOf) is inherently
|
|
@@ -1481,7 +1491,7 @@ export class HistoryModule implements Module {
|
|
|
1481
1491
|
|
|
1482
1492
|
return {
|
|
1483
1493
|
success: true,
|
|
1484
|
-
data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned), matches },
|
|
1494
|
+
data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches },
|
|
1485
1495
|
};
|
|
1486
1496
|
}
|
|
1487
1497
|
|