@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
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* Ported from mcpl-cc-bridge's presence.ts, the reference host.
|
|
27
27
|
*/
|
|
28
|
-
import { objectSignature, renderToolLines, toolDigest, type HintedTool } from './tool-signature.js';
|
|
28
|
+
import { objectSignature, renderToolLines, toolDigest, type HintedTool, type Shown } from './tool-signature.js';
|
|
29
29
|
|
|
30
30
|
export interface ToolsetDecl {
|
|
31
31
|
title?: string;
|
|
@@ -69,15 +69,20 @@ export interface Change {
|
|
|
69
69
|
/** The visible tools after the change. */
|
|
70
70
|
now: HintedTool[];
|
|
71
71
|
}
|
|
72
|
-
export interface ChangeSet {
|
|
72
|
+
export interface ChangeSet {
|
|
73
|
+
changes: Change[];
|
|
74
|
+
suggest: Suggestion[];
|
|
75
|
+
notes: string[];
|
|
76
|
+
/** Ended records' notes, by id: rendered while the id is untold, told once the set reaches an agent (markTold). */
|
|
77
|
+
ended?: Array<{ id: string; note: string }>;
|
|
78
|
+
/** false when its taker showed the change without its blocks (a look that restates the present shows the live catalog instead): told() then counts none of its definitions shown, while a notice that tells it still shows them. */
|
|
79
|
+
blocksShown?: false;
|
|
80
|
+
}
|
|
73
81
|
|
|
74
82
|
export const emptyChange = (): ChangeSet => ({ changes: [], suggest: [], notes: [] });
|
|
75
|
-
export const isEmptyChange = (c: ChangeSet): boolean => !c.changes.length && !c.suggest.length && !c.notes.length;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
suggest: sets.flatMap((s) => s.suggest),
|
|
79
|
-
notes: sets.flatMap((s) => s.notes),
|
|
80
|
-
});
|
|
83
|
+
export const isEmptyChange = (c: ChangeSet): boolean => !c.changes.length && !c.suggest.length && !c.notes.length && !c.ended?.length;
|
|
84
|
+
/** Whether a change to one key shows that key's tools in a block: what render prints and markShown counts as shown. */
|
|
85
|
+
const showsBlock = (c: Change): boolean => !c.closed && (c.opened || c.appeared.length > 0 || c.redefined.length > 0 || c.gone.length > 0);
|
|
81
86
|
|
|
82
87
|
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
83
88
|
const arr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []);
|
|
@@ -90,7 +95,7 @@ const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v
|
|
|
90
95
|
* the last + 1 (a message was lost unread): recover.
|
|
91
96
|
* behind: at or below the last, not applied: recover.
|
|
92
97
|
*/
|
|
93
|
-
export interface ApplyOutcome { applied: boolean; skipped: boolean; behind: boolean; suggest: Suggestion[] }
|
|
98
|
+
export interface ApplyOutcome { applied: boolean; skipped: boolean; behind: boolean; suggest: Suggestion[]; unlisted: boolean }
|
|
94
99
|
|
|
95
100
|
/** What a hint `definition` may change (RFC §6.1): everything else stays the listed tool's. */
|
|
96
101
|
interface DefOverride { description?: string; inputSchema?: Record<string, unknown>; annotations?: Record<string, unknown> }
|
|
@@ -117,10 +122,10 @@ export class ToolPresence {
|
|
|
117
122
|
private visible = new Map<string, Set<string>>();
|
|
118
123
|
/** Definition overrides from hints: per key (an instance's own, or a toolset's), per tool. */
|
|
119
124
|
private defs = new Map<string, Map<string, DefOverride>>();
|
|
120
|
-
/** `ended` ids told to the agent
|
|
125
|
+
/** `ended` ids told to the agent, kept while the server still reports them (RFC §5.3). */
|
|
121
126
|
private toldEnded = new Set<string>();
|
|
127
|
+
/** Moves a tools/list made that the model's tool list doesn't show yet: told once a rebuild has published them. */
|
|
122
128
|
private pendingMoves = new Map<string, Presence>();
|
|
123
|
-
private moved = new Map<string, Presence>();
|
|
124
129
|
/** Digests of definitions the model has been shown with their first sentence. */
|
|
125
130
|
readonly shown = new Set<string>();
|
|
126
131
|
|
|
@@ -132,6 +137,8 @@ export class ToolPresence {
|
|
|
132
137
|
private readonly allowed: (tool: string) => boolean = () => true,
|
|
133
138
|
/** How a static tool is named in the API tool list, for `look`. */
|
|
134
139
|
private readonly listedName: (tool: string) => string = (tool) => tool,
|
|
140
|
+
/** Where the model's published tool list last put a tool, if it's there (shownAs). */
|
|
141
|
+
private readonly published: (tool: string) => Presence | undefined = () => undefined,
|
|
135
142
|
) {}
|
|
136
143
|
|
|
137
144
|
// ── connection lifecycle ──
|
|
@@ -166,6 +173,7 @@ export class ToolPresence {
|
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
this.declared = decl;
|
|
176
|
+
for (const name of Object.keys(decl)) this.groups.delete(name);
|
|
169
177
|
this.surface = mcpl && isObj(mcpl.surface) ? { title: str(mcpl.surface.title), summary: str(mcpl.surface.summary) } : null;
|
|
170
178
|
// RFC §5.1: removing a toolset, or turning its instances on or off, ends
|
|
171
179
|
// its open instances; one that becomes undeclared starts closed under the
|
|
@@ -190,21 +198,35 @@ export class ToolPresence {
|
|
|
190
198
|
this.visible.clear();
|
|
191
199
|
this.defs.clear();
|
|
192
200
|
this.pendingMoves.clear();
|
|
193
|
-
this.moved.clear();
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
/** tools/list: every definition, visible or not (RFC §4.1). A tool the host's policy refuses is left out. */
|
|
197
204
|
setTools(list: HintedTool[]): void {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
205
|
+
const seen = new Map([...this.tools.keys()].map((name) => [name, this.presenceOf(name)]));
|
|
206
|
+
const named = list.filter((t) => isObj(t) && typeof t.name === 'string');
|
|
207
|
+
this.tools = new Map();
|
|
208
|
+
for (const t of named) if (this.allowed(t.name) && !this.tools.has(t.name)) this.tools.set(t.name, t);
|
|
201
209
|
for (const set of this.visible.values()) for (const n of [...set]) if (!this.tools.has(n)) set.delete(n);
|
|
210
|
+
// RFC §4.1: the list states each tool's presence, and presence follows
|
|
211
|
+
// it. A changed presence is a move the model's tool list shows at its
|
|
212
|
+
// next rebuild (§4.2; applyMoves); one the list reverses before then
|
|
213
|
+
// never reached that list.
|
|
214
|
+
for (const name of [...this.pendingMoves.keys()]) if (!this.tools.has(name)) this.pendingMoves.delete(name);
|
|
215
|
+
for (const name of this.active ? this.tools.keys() : []) {
|
|
216
|
+
const was = seen.get(name);
|
|
217
|
+
const listed = this.listedPresence(name);
|
|
218
|
+
if (this.pendingMoves.has(name)) {
|
|
219
|
+
if (this.pendingMoves.get(name) !== listed) this.pendingMoves.delete(name);
|
|
220
|
+
} else if (was !== undefined && was !== listed) {
|
|
221
|
+
this.pendingMoves.set(name, listed);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
202
224
|
// The list's `mcpl/visible` is the server's word only where nothing else
|
|
203
225
|
// speaks: toolset-less dynamic tools and undeclared groups. For a declared
|
|
204
226
|
// toolset it's an aggregate ("visible in some open instance"); per-instance
|
|
205
227
|
// state comes from toolsets/list and hint changes (RFC §4.1, §5.3).
|
|
206
228
|
for (const t of this.tools.values()) {
|
|
207
|
-
if (this.
|
|
229
|
+
if (this.listedPresence(t.name) !== 'dynamic') continue;
|
|
208
230
|
const ts = this.toolsetOf(t.name);
|
|
209
231
|
if (ts && this.isDeclared(ts)) continue;
|
|
210
232
|
this.setVisible(keyOf(ts ?? NO_TOOLSET), t.name, t._meta?.['mcpl/visible'] !== false);
|
|
@@ -234,12 +256,23 @@ export class ToolPresence {
|
|
|
234
256
|
return m && typeof m['mcpl/toolset'] === 'string' && m['mcpl/toolset'] ? (m['mcpl/toolset'] as string) : undefined;
|
|
235
257
|
}
|
|
236
258
|
|
|
237
|
-
/**
|
|
259
|
+
/** As the list states it: a toolset's tool is dynamic whatever a hint says (RFC §4.1). */
|
|
238
260
|
presenceOf(name: string): Presence {
|
|
239
261
|
if (!this.active) return 'static';
|
|
262
|
+
return this.listedPresence(name);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** RFC §4.2: "Until it applies the move, the tool keeps working where it
|
|
266
|
+
* is." Whether a tool is at `place` in the tool list last published, or
|
|
267
|
+
* in the server's latest tools/list (its presence). Until a rebuild
|
|
268
|
+
* applies a move the two differ, and a call is taken at either. */
|
|
269
|
+
shownAs(name: string, place: Presence): boolean {
|
|
270
|
+
if (!this.active) return place === 'static';
|
|
271
|
+
return this.published(name) === place || this.listedPresence(name) === place;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private listedPresence(name: string): Presence {
|
|
240
275
|
if (this.toolsetOf(name)) return 'dynamic';
|
|
241
|
-
const moved = this.moved.get(name);
|
|
242
|
-
if (moved) return moved;
|
|
243
276
|
return this.tools.get(name)?._meta?.['mcpl/presence'] === 'dynamic' ? 'dynamic' : 'static';
|
|
244
277
|
}
|
|
245
278
|
|
|
@@ -332,12 +365,12 @@ export class ToolPresence {
|
|
|
332
365
|
|
|
333
366
|
/** Apply a HintChanges (RFC §6.1) from any of its carriers, as it is read (see ApplyOutcome). */
|
|
334
367
|
apply(h: unknown): ApplyOutcome {
|
|
335
|
-
const none: ApplyOutcome = { applied: false, skipped: false, behind: false, suggest: [] };
|
|
368
|
+
const none: ApplyOutcome = { applied: false, skipped: false, behind: false, suggest: [], unlisted: false };
|
|
336
369
|
if (!isObj(h) || typeof h.rev !== 'number' || !Number.isFinite(h.rev)) return none;
|
|
337
370
|
if (h.rev <= this.rev) return { ...none, behind: true };
|
|
338
371
|
const skipped = h.rev !== this.rev + 1;
|
|
339
372
|
this.rev = h.rev;
|
|
340
|
-
return { ...none, applied: true, skipped,
|
|
373
|
+
return { ...none, applied: true, skipped, ...this.applyBody(h) };
|
|
341
374
|
}
|
|
342
375
|
|
|
343
376
|
/**
|
|
@@ -352,7 +385,8 @@ export class ToolPresence {
|
|
|
352
385
|
return lost;
|
|
353
386
|
}
|
|
354
387
|
|
|
355
|
-
private applyBody(h: Record<string, unknown>): Suggestion[] {
|
|
388
|
+
private applyBody(h: Record<string, unknown>): { suggest: Suggestion[]; unlisted: boolean } {
|
|
389
|
+
let unlisted = false;
|
|
356
390
|
for (const e of arr(h.toolsets)) {
|
|
357
391
|
if (!isObj(e) || !str(e.toolset) || !this.isDeclared(e.toolset as string)) continue; // undeclared toolsets are host-local
|
|
358
392
|
const ts = e.toolset as string;
|
|
@@ -366,20 +400,20 @@ export class ToolPresence {
|
|
|
366
400
|
else if (e.state === 'closed') this.dropKey(key);
|
|
367
401
|
}
|
|
368
402
|
for (const e of arr(h.tools)) {
|
|
369
|
-
if (!isObj(e) || !str(e.name)
|
|
403
|
+
if (!isObj(e) || !str(e.name)) continue;
|
|
370
404
|
const name = e.name as string;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (e.presence !== this.presenceOf(name)) this.pendingMoves.set(name, e.presence);
|
|
375
|
-
else this.pendingMoves.delete(name);
|
|
405
|
+
if (!this.tools.has(name)) {
|
|
406
|
+
if (this.allowed(name)) unlisted = true;
|
|
407
|
+
continue;
|
|
376
408
|
}
|
|
377
|
-
|
|
409
|
+
// A hint's presence stages nothing: a hinted move takes effect when
|
|
410
|
+
// the server's tools/list states it (RFC §4.1), as the rebuild's own
|
|
411
|
+
// list does. A definition it gives is the tool's current one, kept
|
|
412
|
+
// whatever its presence now, for when it's shown dynamic (§6.1).
|
|
413
|
+
if (isObj(e.definition)) {
|
|
378
414
|
const key = this.defKeyFor(name, str(e.instance));
|
|
379
|
-
if (key
|
|
380
|
-
|
|
381
|
-
this.defs.get(key)!.set(name, overrideOf(e.definition));
|
|
382
|
-
}
|
|
415
|
+
if (!this.defs.has(key)) this.defs.set(key, new Map());
|
|
416
|
+
this.defs.get(key)!.set(name, overrideOf(e.definition));
|
|
383
417
|
}
|
|
384
418
|
if (typeof e.visible === 'boolean') for (const key of this.keysFor(name, str(e.instance))) this.setVisible(key, name, e.visible);
|
|
385
419
|
}
|
|
@@ -388,14 +422,14 @@ export class ToolPresence {
|
|
|
388
422
|
if (!isObj(s) || !str(s.toolset) || (s.action !== 'open' && s.action !== 'close')) continue;
|
|
389
423
|
suggest.push({ toolset: s.toolset as string, ...(str(s.instance) ? { instance: str(s.instance) } : {}), action: s.action, reason: str(s.reason) ?? '' });
|
|
390
424
|
}
|
|
391
|
-
return suggest;
|
|
425
|
+
return { suggest, unlisted };
|
|
392
426
|
}
|
|
393
427
|
|
|
394
428
|
/**
|
|
395
429
|
* Where a definition lands (RFC §6.1): with `instance`, that instance's own;
|
|
396
430
|
* without, the toolset's (for a toolset without instances, its only one's).
|
|
397
431
|
*/
|
|
398
|
-
private defKeyFor(name: string, instance: string | undefined): string
|
|
432
|
+
private defKeyFor(name: string, instance: string | undefined): string {
|
|
399
433
|
const ts = this.toolsetOf(name);
|
|
400
434
|
if (!ts) return keyOf(NO_TOOLSET);
|
|
401
435
|
return keyOf(ts, this.declared[ts]?.instances ? instance : undefined);
|
|
@@ -445,7 +479,7 @@ export class ToolPresence {
|
|
|
445
479
|
* Returns what was lost, what the server says ended while no connection was
|
|
446
480
|
* told, its suggestions, and whether its rev skipped one.
|
|
447
481
|
*/
|
|
448
|
-
adopt(result: unknown): { lost: OpenRecord[]; ended: Ended[]; suggest: Suggestion[]; skipped: boolean } {
|
|
482
|
+
adopt(result: unknown): { lost: OpenRecord[]; ended: Ended[]; suggest: Suggestion[]; skipped: boolean; unlisted: boolean } {
|
|
449
483
|
const had = [...this.open.values()];
|
|
450
484
|
// A full statement: a dynamic tool not stated visible is invisible.
|
|
451
485
|
// Undeclared groups stay as the host has them open or closed.
|
|
@@ -463,9 +497,10 @@ export class ToolPresence {
|
|
|
463
497
|
}
|
|
464
498
|
let suggest: Suggestion[] = [];
|
|
465
499
|
let skipped = false;
|
|
500
|
+
let unlisted = false;
|
|
466
501
|
const h = isObj(r._meta) ? r._meta['mcpl/hints'] : undefined;
|
|
467
502
|
if (isObj(h)) {
|
|
468
|
-
suggest = this.applyBody(h);
|
|
503
|
+
({ suggest, unlisted } = this.applyBody(h));
|
|
469
504
|
// Its own rev, at its place in order (RFC §6.1).
|
|
470
505
|
if (typeof h.rev === 'number' && Number.isFinite(h.rev)) {
|
|
471
506
|
skipped = h.rev !== this.rev + 1;
|
|
@@ -477,29 +512,51 @@ export class ToolPresence {
|
|
|
477
512
|
if (!isObj(e) || !str(e.toolset)) continue;
|
|
478
513
|
ended.push({ ...(str(e.id) ? { id: str(e.id) } : {}), toolset: e.toolset as string, ...(str(e.instance) ? { instance: str(e.instance) } : {}), reason: str(e.reason) ?? '' });
|
|
479
514
|
}
|
|
480
|
-
|
|
515
|
+
// RFC §5.3: a record is reported on every toolsets/list until it expires, and its id is never
|
|
516
|
+
// reused, so an id the server no longer reports needn't be remembered.
|
|
517
|
+
const reported = new Set(ended.flatMap((e) => (e.id ? [e.id] : [])));
|
|
518
|
+
for (const id of [...this.toldEnded]) if (!reported.has(id)) this.toldEnded.delete(id);
|
|
519
|
+
return { lost: had.filter((o) => !this.open.has(keyOf(o.toolset, o.instance))), ended, suggest, skipped, unlisted };
|
|
481
520
|
}
|
|
482
521
|
|
|
483
522
|
/**
|
|
484
523
|
* What the agent should hear about an adoption: what ended while it was
|
|
485
|
-
* away, each `ended` id
|
|
486
|
-
*
|
|
524
|
+
* away, each `ended` id until it has been told (RFC §5.3; markTold), and
|
|
525
|
+
* what is closed now. An ended record with an id is in `ended`, rendered
|
|
526
|
+
* while its id is untold.
|
|
487
527
|
*/
|
|
488
|
-
adoptionNotes(lost: OpenRecord[], ended: Ended[]): string[] {
|
|
528
|
+
adoptionNotes(lost: OpenRecord[], ended: Ended[]): { notes: string[]; ended: Array<{ id: string; note: string }> } {
|
|
489
529
|
const notes: string[] = [];
|
|
490
|
-
const
|
|
530
|
+
const told: Array<{ id: string; note: string }> = [];
|
|
531
|
+
const endedKeys = new Set<string>();
|
|
491
532
|
for (const e of ended) {
|
|
533
|
+
if (!e.id || !this.toldEnded.has(e.id)) endedKeys.add(keyOf(e.toolset, e.instance));
|
|
534
|
+
const note = `${this.label(keyOf(e.toolset, e.instance))} ended${e.reason ? `: ${e.reason}` : ''}.`;
|
|
492
535
|
if (e.id) {
|
|
493
|
-
if (this.toldEnded.has(e.id))
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
notes.push(`${this.label(keyOf(e.toolset, e.instance))} ended${e.reason ? `: ${e.reason}` : ''}.`);
|
|
536
|
+
if (!this.toldEnded.has(e.id)) told.push({ id: e.id, note });
|
|
537
|
+
} else notes.push(note);
|
|
497
538
|
}
|
|
498
539
|
for (const o of lost) {
|
|
499
540
|
if (endedKeys.has(keyOf(o.toolset, o.instance))) continue;
|
|
500
541
|
notes.push(`${this.label(keyOf(o.toolset, o.instance))} is closed: ${this.server} no longer has it open. ${this.openHint(o.toolset)} starts a new one.`);
|
|
501
542
|
}
|
|
502
|
-
return notes;
|
|
543
|
+
return { notes, ended: told };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The agent has been told these ended records (a notice written, a result it reads); the next statement forgets any it no longer reports. */
|
|
547
|
+
markTold(ended: ChangeSet['ended']): void {
|
|
548
|
+
for (const { id } of ended ?? []) this.toldEnded.add(id);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** The definitions a change set's tool blocks describe have reached the model. */
|
|
552
|
+
markShown(cs: ChangeSet): void {
|
|
553
|
+
for (const c of cs.changes) if (showsBlock(c)) for (const t of c.now) this.shown.add(toolDigest(t));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** A set's ended notes whose ids are still untold, each once. */
|
|
557
|
+
untoldNotes(cs: ChangeSet): string[] {
|
|
558
|
+
const seen = new Set<string>();
|
|
559
|
+
return (cs.ended ?? []).filter(({ id }) => !this.toldEnded.has(id) && !seen.has(id) && !!seen.add(id)).map(({ note }) => note);
|
|
503
560
|
}
|
|
504
561
|
|
|
505
562
|
// ── host-made opens of undeclared groups (RFC §5.4) ──
|
|
@@ -514,15 +571,15 @@ export class ToolPresence {
|
|
|
514
571
|
|
|
515
572
|
// ── boundaries ──
|
|
516
573
|
|
|
517
|
-
/**
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
this.pendingMoves.
|
|
574
|
+
/** The static↔dynamic moves the lists made since the last rebuild, still to be shown. */
|
|
575
|
+
moves(): Array<{ name: string; to: Presence }> {
|
|
576
|
+
return [...this.pendingMoves].map(([name, to]) => ({ name, to }));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** At a rebuild of the model's tool list (RFC §4.2): those of `moves` still pending, now shown. */
|
|
580
|
+
applyMoves(moves = this.moves()): Array<{ name: string; to: Presence }> {
|
|
581
|
+
const out = moves.filter(({ name, to }) => this.pendingMoves.get(name) === to);
|
|
582
|
+
for (const { name } of out) this.pendingMoves.delete(name);
|
|
526
583
|
return out;
|
|
527
584
|
}
|
|
528
585
|
|
|
@@ -550,14 +607,22 @@ export class ToolPresence {
|
|
|
550
607
|
}
|
|
551
608
|
|
|
552
609
|
/** The tool block for one key: its label, how to call, and the visible tools (new or changed ones described). */
|
|
553
|
-
toolBlock(key: string, tools: HintedTool[], opts: { describeAll?: boolean; headed?: boolean; title?: string } = {}): string[] {
|
|
610
|
+
toolBlock(key: string, tools: HintedTool[], opts: { describeAll?: boolean; headed?: boolean; title?: string; seen?: Shown } = {}): string[] {
|
|
554
611
|
const head = opts.headed === false ? '' : `${this.label(key)}${opts.title ? ` ${opts.title}` : ''} `;
|
|
555
612
|
if (!tools.length) return [`${head}no tools available right now.`];
|
|
556
|
-
return [`${head}tools now — ${this.callHint(key)}:`, ...renderToolLines(tools, this.shown, { describeAll: opts.describeAll })];
|
|
613
|
+
return [`${head}tools now — ${this.callHint(key)}:`, ...renderToolLines(tools, opts.seen ?? this.shown, { describeAll: opts.describeAll })];
|
|
557
614
|
}
|
|
558
615
|
|
|
559
|
-
/**
|
|
560
|
-
|
|
616
|
+
/**
|
|
617
|
+
* What the model reads for a change set. `bare`: a key whose header the
|
|
618
|
+
* caller already printed. `mark: false` renders without counting the
|
|
619
|
+
* definitions it describes as shown, for text that may not reach the model
|
|
620
|
+
* (markShown counts them once it has). `describeAll`: every tool with its
|
|
621
|
+
* first sentence, as for a result that may reach no agent.
|
|
622
|
+
*/
|
|
623
|
+
render(cs: ChangeSet, opts: { bare?: string; mark?: boolean; describeAll?: boolean } = {}): string {
|
|
624
|
+
const local = new Set<string>();
|
|
625
|
+
const seen: Shown = opts.mark === false ? { has: (d) => this.shown.has(d) || local.has(d), add: (d) => { local.add(d); } } : this.shown;
|
|
561
626
|
const lines: string[] = [];
|
|
562
627
|
for (const c of cs.changes) {
|
|
563
628
|
const bare = opts.bare === c.key;
|
|
@@ -566,8 +631,7 @@ export class ToolPresence {
|
|
|
566
631
|
continue;
|
|
567
632
|
}
|
|
568
633
|
const title = (c.opened || c.retitled) && !bare ? c.title : undefined;
|
|
569
|
-
|
|
570
|
-
if (setChanged) lines.push(...this.toolBlock(c.key, c.now, { headed: !bare, title }));
|
|
634
|
+
if (showsBlock(c)) lines.push(...this.toolBlock(c.key, c.now, { headed: !bare, title, seen, describeAll: opts.describeAll }));
|
|
571
635
|
else if (title) lines.push(`${this.label(c.key)} ${title}`);
|
|
572
636
|
if (c.gone.length) lines.push(`now unavailable: ${c.gone.join(', ')}`);
|
|
573
637
|
if (c.opened && !bare) {
|
|
@@ -576,7 +640,7 @@ export class ToolPresence {
|
|
|
576
640
|
}
|
|
577
641
|
}
|
|
578
642
|
for (const s of cs.suggest) lines.push(this.renderSuggestion(s));
|
|
579
|
-
lines.push(...cs.notes);
|
|
643
|
+
lines.push(...this.untoldNotes(cs), ...cs.notes);
|
|
580
644
|
return lines.join('\n');
|
|
581
645
|
}
|
|
582
646
|
|
|
@@ -605,14 +669,16 @@ export class ToolPresence {
|
|
|
605
669
|
return lines.join('\n');
|
|
606
670
|
}
|
|
607
671
|
|
|
608
|
-
/** look naming this server: what is open (with tools) and what can be
|
|
609
|
-
|
|
672
|
+
/** look naming this server: what is open (with tools) and what can be
|
|
673
|
+
* opened. `mark: false`, as for render, for a look whose result may
|
|
674
|
+
* reach no agent. */
|
|
675
|
+
catalog(opts: { mark?: boolean } = {}): string {
|
|
610
676
|
const lines: string[] = [];
|
|
611
677
|
lines.push(this.surface ? `${this.server} — ${[this.surface.title, this.surface.summary].filter(Boolean).join(': ')}` : this.server);
|
|
612
678
|
const v = this.view();
|
|
613
679
|
if (v.size) {
|
|
614
680
|
for (const [key, e] of v) {
|
|
615
|
-
lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title }).map((l) => ` ${l}`));
|
|
681
|
+
lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title, seen: opts.mark === false ? new Set<string>() : undefined }).map((l) => ` ${l}`));
|
|
616
682
|
const close = this.closeHint(key);
|
|
617
683
|
if (close) lines.push(` ${close} closes it.`);
|
|
618
684
|
}
|
|
@@ -639,7 +705,7 @@ export class ToolPresence {
|
|
|
639
705
|
|
|
640
706
|
/** Where a tool can be called from now, for `look` naming it. */
|
|
641
707
|
toolStatus(tool: string): string {
|
|
642
|
-
if (this.presenceOf(tool) === 'static') return `static —
|
|
708
|
+
if (this.presenceOf(tool) === 'static') return `static — called by name, as ${this.listedName(tool)}`;
|
|
643
709
|
const where = [...this.view().entries()].filter(([, e]) => e.tools.has(tool)).map(([k]) => k);
|
|
644
710
|
const ts = this.toolsetOf(tool);
|
|
645
711
|
return where.length
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How a dynamic tool reads to a model when it reaches the conversation
|
|
3
3
|
* instead of the tool list (MCPL RFC-014 §8.3): a compact signature, its
|
|
4
|
-
* first sentence the first time a definition is shown,
|
|
5
|
-
*
|
|
6
|
-
* definition (RFC-014 §6.1: "the host checks
|
|
7
|
-
* against it").
|
|
4
|
+
* first sentence the first time a definition is shown, and on request its
|
|
5
|
+
* description, input schema and class. Also the host's own argument check
|
|
6
|
+
* against a tool's current definition (RFC-014 §6.1: "the host checks
|
|
7
|
+
* arguments for that instance against it").
|
|
8
8
|
*
|
|
9
9
|
* Ported from mcpl-cc-bridge's tool-render.ts and schema-check.ts, the
|
|
10
10
|
* reference host of RFC-014. Dependency-free.
|
|
@@ -24,11 +24,25 @@ export interface HintedTool {
|
|
|
24
24
|
|
|
25
25
|
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
26
26
|
|
|
27
|
+
/** How deep a server's value may nest and still be written out, the same on every runtime. */
|
|
28
|
+
const DEEPEST = 64;
|
|
29
|
+
|
|
30
|
+
function nestsWithin(value: unknown, depth = 0): boolean {
|
|
31
|
+
if (value === null || typeof value !== 'object') return true;
|
|
32
|
+
return depth <= DEEPEST && Object.values(value).every((v) => nestsWithin(v, depth + 1));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A schema's literal value (an enum's, a const or a default) as JSON, or … where it's nested too deeply to write. */
|
|
36
|
+
function literal(value: unknown): string {
|
|
37
|
+
return nestsWithin(value) ? JSON.stringify(value) ?? 'null' : '…';
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
function typeSig(schema: JsonSchema | undefined, depth = 0): string {
|
|
28
41
|
if (!schema || typeof schema !== 'object') return 'any';
|
|
42
|
+
if (depth > 8) return '…';
|
|
29
43
|
if (Array.isArray(schema.enum)) {
|
|
30
44
|
// The enum is often the menu (a decision's options): all of it unless it is long.
|
|
31
|
-
const vals = (schema.enum as unknown[]).map(
|
|
45
|
+
const vals = (schema.enum as unknown[]).map(literal);
|
|
32
46
|
if (vals.length <= 8 || vals.join('|').length <= 400) return vals.join('|');
|
|
33
47
|
let shown = 0;
|
|
34
48
|
let len = 0;
|
|
@@ -38,10 +52,12 @@ function typeSig(schema: JsonSchema | undefined, depth = 0): string {
|
|
|
38
52
|
}
|
|
39
53
|
return `${vals.slice(0, Math.max(shown, 4)).join('|')}|…(${vals.length} in all)`;
|
|
40
54
|
}
|
|
41
|
-
if (schema.const !== undefined) return
|
|
55
|
+
if (schema.const !== undefined) return literal(schema.const);
|
|
42
56
|
const alts = (schema.oneOf ?? schema.anyOf) as JsonSchema[] | undefined;
|
|
43
57
|
if (Array.isArray(alts)) return alts.map((s) => typeSig(s, depth + 1)).join(' | ');
|
|
44
|
-
|
|
58
|
+
// Only text names a type: entries that aren't text are left out, and a type with none reads as no type.
|
|
59
|
+
const types = (Array.isArray(schema.type) ? schema.type : [schema.type]).filter((x): x is string => typeof x === 'string');
|
|
60
|
+
const t = types.length ? types.join('|') : undefined;
|
|
45
61
|
switch (t) {
|
|
46
62
|
case 'integer':
|
|
47
63
|
case 'number': {
|
|
@@ -81,7 +97,7 @@ export function signature(tool: HintedTool): string {
|
|
|
81
97
|
const props = (isObj(s.properties) ? s.properties : {}) as Record<string, JsonSchema>;
|
|
82
98
|
const req = new Set(Array.isArray(s.required) ? (s.required as string[]) : []);
|
|
83
99
|
const params = Object.entries(props).map(([k, v]) => {
|
|
84
|
-
const def = isObj(v) && v.default !== undefined ? ` =${
|
|
100
|
+
const def = isObj(v) && v.default !== undefined ? ` =${literal(v.default)}` : '';
|
|
85
101
|
return `${k}${req.has(k) ? '' : '?'}: ${typeSig(v, 1)}${def}`;
|
|
86
102
|
});
|
|
87
103
|
return `${tool.name}(${params.join(', ')})`;
|
|
@@ -94,22 +110,23 @@ export function objectSignature(schema: unknown): string {
|
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
/** First sentence of a description, bounded. */
|
|
97
|
-
export function firstSentence(text:
|
|
98
|
-
const t = (text
|
|
113
|
+
export function firstSentence(text: unknown, max = 140): string {
|
|
114
|
+
const t = (typeof text === 'string' ? text : '').trim().replace(/\s+/g, ' ');
|
|
99
115
|
// A sentence ends at . ! ? followed by a capital, a bracket, or the end, so "e.g. \"D4\"" does not end it.
|
|
100
116
|
const m = t.match(/^(.+?[.!?])(?=\s+[A-Z([]|$)/);
|
|
101
|
-
const s = m ? m[1]! : t;
|
|
102
|
-
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
117
|
+
const s = [...(m ? m[1]! : t)];
|
|
118
|
+
return s.length > max ? s.slice(0, max - 1).join('') + '…' : s.join('');
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
function canonicalJson(value: unknown): string {
|
|
121
|
+
function canonicalJson(value: unknown, depth = 0): string {
|
|
106
122
|
if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
|
|
107
|
-
if (
|
|
123
|
+
if (depth > DEEPEST) return '"…"';
|
|
124
|
+
if (Array.isArray(value)) return `[${value.map((v) => canonicalJson(v, depth + 1)).join(',')}]`;
|
|
108
125
|
const obj = value as Record<string, unknown>;
|
|
109
126
|
return `{${Object.keys(obj)
|
|
110
127
|
.sort()
|
|
111
128
|
.filter((k) => obj[k] !== undefined)
|
|
112
|
-
.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`)
|
|
129
|
+
.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k], depth + 1)}`)
|
|
113
130
|
.join(',')}}`;
|
|
114
131
|
}
|
|
115
132
|
|
|
@@ -121,12 +138,15 @@ export function toolDigest(tool: HintedTool): string {
|
|
|
121
138
|
.slice(0, 22);
|
|
122
139
|
}
|
|
123
140
|
|
|
141
|
+
/** The definitions a model has been shown, by digest: has and add are all rendering needs. */
|
|
142
|
+
export type Shown = { has(digest: string): boolean; add(digest: string): void };
|
|
143
|
+
|
|
124
144
|
/**
|
|
125
145
|
* Tool lines as the model reads them. A definition the model has been shown
|
|
126
146
|
* gets its signature only; a new or changed one also gets its first
|
|
127
147
|
* sentence. `shown` is updated.
|
|
128
148
|
*/
|
|
129
|
-
export function renderToolLines(tools: HintedTool[], shown:
|
|
149
|
+
export function renderToolLines(tools: HintedTool[], shown: Shown, opts: { describeAll?: boolean; indent?: string } = {}): string[] {
|
|
130
150
|
const indent = opts.indent ?? ' ';
|
|
131
151
|
return tools.map((t) => {
|
|
132
152
|
const d = toolDigest(t);
|
|
@@ -137,15 +157,26 @@ export function renderToolLines(tools: HintedTool[], shown: Set<string>, opts: {
|
|
|
137
157
|
});
|
|
138
158
|
}
|
|
139
159
|
|
|
140
|
-
/**
|
|
160
|
+
/** A server's value as JSON, or a note where it's nested too deeply, or too long, to show. */
|
|
161
|
+
export function jsonText(value: unknown, space?: number): string {
|
|
162
|
+
if (!nestsWithin(value)) return '(nested too deeply to show)';
|
|
163
|
+
try {
|
|
164
|
+
return JSON.stringify(value, null, space) ?? '';
|
|
165
|
+
} catch {
|
|
166
|
+
return '(too long to show)';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A tool's signature, description, class and input schema, for `look` naming a tool. */
|
|
141
171
|
export function renderToolDefinition(tool: HintedTool): string {
|
|
142
172
|
const cls = tool._meta?.['mcpl/class'];
|
|
173
|
+
const classes = Array.isArray(cls) ? cls.filter((c): c is string => typeof c === 'string') : [];
|
|
143
174
|
return [
|
|
144
175
|
signature(tool),
|
|
145
|
-
tool.description
|
|
146
|
-
...(
|
|
176
|
+
typeof tool.description === 'string' ? tool.description : '',
|
|
177
|
+
...(classes.length ? [`class: ${classes.join(', ')}`] : []),
|
|
147
178
|
'inputSchema:',
|
|
148
|
-
|
|
179
|
+
jsonText(tool.inputSchema ?? {}, 2),
|
|
149
180
|
]
|
|
150
181
|
.filter((l) => l !== '')
|
|
151
182
|
.join('\n');
|