@bevel-software/platform-core-backend 0.25.2 → 0.26.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/agent-guide/access-control.md +234 -0
- package/agent-guide/conventions.md +27 -0
- package/agent-guide/directory-structure.md +145 -0
- package/agent-guide/finding-things.md +7 -0
- package/agent-guide/introduction.md +27 -0
- package/agent-guide/skills.md +47 -0
- package/agent-guide/tool-manuals.md +217 -0
- package/agent-guide/where-a-new-file-goes.md +36 -0
- package/dist/assets.d.ts +7 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +9 -0
- package/dist/assets.js.map +1 -1
- package/dist/core/core-ports.d.ts +11 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +13 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +14 -4
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +9 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +16 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +139 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.js +191 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +4 -0
- package/dist/modules/agent-guide/index.d.ts.map +1 -0
- package/dist/modules/agent-guide/index.js +4 -0
- package/dist/modules/agent-guide/index.js.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +9 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +9 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +1 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +1 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +32 -85
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +29 -2
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +113 -16
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
- package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
- package/dist/modules/mcp/tool-schema-guard.js +171 -0
- package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
- package/dist/modules/plugins/plugins.tools.d.ts +36 -2
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.tools.js +71 -14
- package/dist/modules/plugins/plugins.tools.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +0 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +14 -53
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +3 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +58 -16
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -36
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +14 -14
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +24 -26
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +23 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
- package/dist/modules/tool-registry/guide-first.js +32 -0
- package/dist/modules/tool-registry/guide-first.js.map +1 -0
- package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
- package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool-registry.js +9 -2
- package/dist/modules/tool-registry/tool-registry.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +73 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +582 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +210 -13
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +456 -91
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
- package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
- package/dist/modules/workflow/git/merge-commit.js +89 -0
- package/dist/modules/workflow/git/merge-commit.js.map +1 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +332 -37
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +178 -12
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +6 -2
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +7 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +4 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +3 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +5 -3
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +10 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +211 -18
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +11 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +14 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/hidden-tools.d.ts +44 -0
- package/dist/shared/hidden-tools.d.ts.map +1 -0
- package/dist/shared/hidden-tools.js +13 -0
- package/dist/shared/hidden-tools.js.map +1 -0
- package/kb-template/.bevelignore +0 -5
- package/package.json +4 -3
- package/src/__tests__/kb-layout-config.test.ts +10 -100
- package/src/__tests__/packaged-assets-ship.test.ts +54 -0
- package/src/assets.ts +10 -0
- package/src/core/core-ports.ts +11 -0
- package/src/core/create-core-server.ts +13 -2
- package/src/core/create-core-services.ts +28 -4
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
- package/src/modules/access/access-control.interface.ts +15 -0
- package/src/modules/access/access-control.service.ts +21 -0
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
- package/src/modules/agent-guide/agent-guide.ts +291 -0
- package/src/modules/agent-guide/index.ts +21 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
- package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
- package/src/modules/agent-instructions/compose.ts +9 -6
- package/src/modules/agent-instructions/index.ts +0 -3
- package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
- package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
- package/src/modules/mcp/mcp.service.ts +137 -19
- package/src/modules/mcp/tool-schema-guard.ts +196 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
- package/src/modules/plugins/plugins.tools.ts +75 -15
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
- package/src/modules/settings/deployment-settings.service.ts +13 -54
- package/src/modules/settings/setup.routes.ts +3 -6
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
- package/src/modules/skills/skills.tools.ts +62 -16
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
- package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
- package/src/modules/tool-registry/description-length.ts +24 -26
- package/src/modules/tool-registry/guide-first.ts +34 -0
- package/src/modules/tool-registry/tool-registry.ts +9 -2
- package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
- package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
- package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
- package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
- package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
- package/src/modules/workflow/git/git.service.ts +537 -94
- package/src/modules/workflow/git/merge-commit.ts +88 -0
- package/src/modules/workflow/git/pull-request.service.ts +380 -54
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
- package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
- package/src/modules/workflow/workflow.routes.ts +7 -2
- package/src/modules/workflow/workflow.service.ts +7 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
- package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
- package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
- package/src/modules/workspace/startup/steps/template-source.ts +5 -3
- package/src/modules/workspace/workspace.tools.ts +226 -16
- package/src/shared/domain-errors.ts +15 -0
- package/src/shared/hidden-tools.ts +45 -0
- package/kb-template/AGENTS.md +0 -730
|
@@ -7,6 +7,7 @@ import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
|
|
|
7
7
|
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
8
8
|
import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
|
|
9
9
|
import type { IToolManualService } from './tool-manuals.contract.js';
|
|
10
|
+
import type { HiddenToolSource } from '../../shared/hidden-tools.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* What `list_tool_setup` needs from the secrets vault (structurally satisfied
|
|
@@ -43,6 +44,12 @@ export function registerToolManualsTools(
|
|
|
43
44
|
variableStatus: VariableStatusPort;
|
|
44
45
|
/** Which branch tools are served from, and its clone. */
|
|
45
46
|
kb: Pick<KbContext, 'defaultBranch' | 'defaultWorkspaceId'>;
|
|
47
|
+
/**
|
|
48
|
+
* Where a hidden tool's schema finding comes from (the MCP proxy).
|
|
49
|
+
* Optional: a deployment with no MCP surface has nothing to report, and
|
|
50
|
+
* `hiddenTools` is then empty everywhere.
|
|
51
|
+
*/
|
|
52
|
+
hiddenTools?: HiddenToolSource;
|
|
46
53
|
},
|
|
47
54
|
): void {
|
|
48
55
|
registry.registerExternalTool((ctx) => buildListLocalToolsDef(toolManualService, ctx.userEmail));
|
|
@@ -66,23 +73,30 @@ export function registerToolManualsTools(
|
|
|
66
73
|
const listSetupDef = toolDef({
|
|
67
74
|
name: 'list_tool_setup',
|
|
68
75
|
// What each FIELD means is documented on the field, in `outputs` below:
|
|
69
|
-
// `setup.kind`, `variables` and `
|
|
70
|
-
// which made this description two thousand characters and
|
|
71
|
-
// thing a client cut. The description says what the tool
|
|
72
|
-
//
|
|
76
|
+
// `setup.kind`, `variables`, `invalid` and `hiddenTools` each carried a
|
|
77
|
+
// paragraph here, which made this description two thousand characters and
|
|
78
|
+
// so the first thing a client cut. The description says what the tool
|
|
79
|
+
// answers and the four things an agent cannot read off a field.
|
|
80
|
+
//
|
|
81
|
+
// It sits a few characters under `TOOL_DESCRIPTION_CAP` WITH the guide-first
|
|
82
|
+
// sentence the registry puts in front of it (`tool-registry/guide-first.ts`),
|
|
83
|
+
// which is why every sentence here is the short form: the `hiddenTools`
|
|
84
|
+
// clause and that opener both came out of the same budget, and what a
|
|
85
|
+
// hidden tool's entry CONTAINS is on the field below rather than here.
|
|
73
86
|
description:
|
|
74
87
|
'Configuration status of every `.tool` the current user can access: what each tool needs set up and what is ' +
|
|
75
|
-
'already configured, as `{ tools, invalid, onBranchOnly, note? }`. Scoped to the CALLER
|
|
76
|
-
'READ is absent
|
|
77
|
-
'Secret VALUES are never returned and can never be set through a tool: an admin enters them in the tool
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
88
|
+
'already configured, as `{ tools, invalid, onBranchOnly, note? }`. Scoped to the CALLER: a `.tool` it cannot ' +
|
|
89
|
+
'READ is absent, and every flag is the caller\'s own. ' +
|
|
90
|
+
'Secret VALUES are never returned and can never be set through a tool: an admin enters them in the tool ' +
|
|
91
|
+
'editor; users sign in on /connect. ' +
|
|
92
|
+
'Setting a tool\'s shared secrets is gated by `canWrite` on the `.tool` FILE (its frontmatter ' +
|
|
93
|
+
'`write:`/`owner:` verbs and the access.md chain), NOT by a platform role: who manages the file configures ' +
|
|
94
|
+
'the tool. ' +
|
|
95
|
+
'`hiddenTools` names the tools Hexis hides from agents for an invalid schema. ' +
|
|
82
96
|
'The listing is the RELEASED catalog, built from the default branch only: a server or `.tool` you declared on a ' +
|
|
83
|
-
'draft is not listed,
|
|
84
|
-
'
|
|
85
|
-
'
|
|
97
|
+
'draft is not listed, callable or signed-in-able until it is merged. Pass `branch` (the draft you wrote the ' +
|
|
98
|
+
'declaration on) and `onBranchOnly` names every tool declared there that the default branch does not serve ' +
|
|
99
|
+
'yet — open a change request and ask the user to merge it in the app to activate it.',
|
|
86
100
|
path: '/api/agent/tools/list_tool_setup',
|
|
87
101
|
inputs: {
|
|
88
102
|
type: 'object',
|
|
@@ -132,6 +146,22 @@ export function registerToolManualsTools(
|
|
|
132
146
|
type: 'boolean',
|
|
133
147
|
description: 'You may write this `.tool` FILE, which is what gates setting its shared secrets.',
|
|
134
148
|
},
|
|
149
|
+
hiddenTools: {
|
|
150
|
+
type: 'array',
|
|
151
|
+
description:
|
|
152
|
+
'Tools of this server kept off every agent surface for an invalid input schema. Empty ' +
|
|
153
|
+
'unless the caller may write this tool.',
|
|
154
|
+
items: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
name: { type: 'string', description: 'The name the tool would have been offered under.' },
|
|
158
|
+
path: { type: 'string', description: 'JSON Pointer to the place in the schema that is not valid.' },
|
|
159
|
+
reason: { type: 'string', description: 'Why that place is not valid, in the validator\'s words.' },
|
|
160
|
+
marker: { type: 'string', description: 'The one sentence the tool page shows for this tool.' },
|
|
161
|
+
},
|
|
162
|
+
required: ['name', 'path', 'reason', 'marker'],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
135
165
|
variables: {
|
|
136
166
|
type: 'array',
|
|
137
167
|
description:
|
|
@@ -156,7 +186,7 @@ export function registerToolManualsTools(
|
|
|
156
186
|
},
|
|
157
187
|
},
|
|
158
188
|
},
|
|
159
|
-
required: ['slug', 'name', 'path', 'type', 'canWrite', 'variables'],
|
|
189
|
+
required: ['slug', 'name', 'path', 'type', 'canWrite', 'hiddenTools', 'variables'],
|
|
160
190
|
},
|
|
161
191
|
},
|
|
162
192
|
invalid: {
|
|
@@ -219,27 +249,35 @@ export function registerToolManualsTools(
|
|
|
219
249
|
const status = await deps.variableStatus.statusFor(ctx.user.id, allKeys);
|
|
220
250
|
const statusByKey = new Map(status.map((s) => [s.key, s]));
|
|
221
251
|
const tools = await Promise.all(
|
|
222
|
-
manuals.map(async (m) =>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
252
|
+
manuals.map(async (m) => {
|
|
253
|
+
const canWrite = await deps.accessControl.canWrite(defaultWs(), ctx.user.email, m.path);
|
|
254
|
+
return {
|
|
255
|
+
slug: m.slug,
|
|
256
|
+
name: m.name,
|
|
257
|
+
path: m.path,
|
|
258
|
+
type: m.type,
|
|
259
|
+
setup: m.setup ?? null,
|
|
260
|
+
canWrite,
|
|
261
|
+
// The marker goes only to the people who manage the server — the
|
|
262
|
+
// same verdict that gates setting its shared secrets. A caller who
|
|
263
|
+
// may only READ the tool is told nothing about it: they cannot fix
|
|
264
|
+
// the schema, and the tool is simply not among the ones they can call.
|
|
265
|
+
hiddenTools: canWrite ? (deps.hiddenTools?.hiddenFor(m.name) ?? []) : [],
|
|
266
|
+
variables: (m.variables ?? []).map((v) => {
|
|
267
|
+
const st = statusByKey.get(varKey(m.name, v.name));
|
|
268
|
+
const isOAuth = v.oauth != null;
|
|
269
|
+
return {
|
|
270
|
+
name: v.name,
|
|
271
|
+
scope: v.scope,
|
|
272
|
+
label: v.label ?? null,
|
|
273
|
+
oauth: isOAuth,
|
|
274
|
+
adminConfigured: st?.adminConfigured ?? false,
|
|
275
|
+
userConfigured: st?.userConfigured ?? false,
|
|
276
|
+
authorized: isOAuth ? (st?.userAuthorized ?? false) : null,
|
|
277
|
+
};
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
}),
|
|
243
281
|
);
|
|
244
282
|
const onBranchOnly = pending.map((p) => ({ ...p, branch: branch! }));
|
|
245
283
|
if (onBranchOnly.length === 0) return { tools, invalid, onBranchOnly };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { inputSchemaDefect } from '@bevel-software/platform-mcp-core';
|
|
5
|
+
import { ToolRegistry } from '../tool-registry.js';
|
|
6
|
+
import { registerWorkflowTools } from '../../workflow/agent-tools/workflow.tools.js';
|
|
7
|
+
import { registerChangeRequestReadTools } from '../../workflow/agent-tools/change-request-read.tools.js';
|
|
8
|
+
import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
|
|
9
|
+
import { registerSkillsTools } from '../../skills/skills.tools.js';
|
|
10
|
+
import { registerPluginsTools } from '../../plugins/plugins.tools.js';
|
|
11
|
+
import { registerToolManualsTools } from '../../tool-manuals/tool-manuals.tools.js';
|
|
12
|
+
import { registerAgentGuideTool } from '../../agent-guide/agent-guide.tools.js';
|
|
13
|
+
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
14
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Every tool Hexis declares itself, checked against the same JSON Schema rules
|
|
18
|
+
* a connected server's tool is checked against.
|
|
19
|
+
*
|
|
20
|
+
* Hexis hides a connected tool whose input schema is invalid, because an AI
|
|
21
|
+
* client would otherwise drop it in silence. The platform's own tools go to
|
|
22
|
+
* those same clients through those same surfaces, so a mistake in one of these
|
|
23
|
+
* schemas costs the tool just as quietly — and it would be Hexis's mistake.
|
|
24
|
+
* This test is the thing that makes shipping one impossible.
|
|
25
|
+
*
|
|
26
|
+
* It registers the DEFS as `create-core-server` does, with stubs for the
|
|
27
|
+
* services (no def-building path touches one: the lazy providers are resolved
|
|
28
|
+
* with no caller context, which is their static form).
|
|
29
|
+
*
|
|
30
|
+
* Add a tool module to the server and this test FAILS until it is added here:
|
|
31
|
+
* `covers every tool module the server registers` reads the server's own source
|
|
32
|
+
* and compares the `register…Tools` calls in it with `MODULES` below. A test
|
|
33
|
+
* that silently skipped a whole module's schemas would be worse than no test,
|
|
34
|
+
* because it would read as if it had checked them.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A stub service: every method answers with an empty list. The def-building
|
|
39
|
+
* paths that do reach a service only ask it what exists (which skills, which
|
|
40
|
+
* local-only tools) to name them in a description, and "none" is a valid
|
|
41
|
+
* answer that keeps the SCHEMAS — what this test is about — untouched.
|
|
42
|
+
* `then` is excluded so the stub is never mistaken for a promise.
|
|
43
|
+
*/
|
|
44
|
+
const nothing = new Proxy(
|
|
45
|
+
{},
|
|
46
|
+
{
|
|
47
|
+
get: (_target, key) => (key === 'then' ? undefined : async () => []),
|
|
48
|
+
},
|
|
49
|
+
) as never;
|
|
50
|
+
|
|
51
|
+
const pass: express.RequestHandler = (_req, _res, next) => next();
|
|
52
|
+
const handler = (() => pass) as never;
|
|
53
|
+
|
|
54
|
+
/** Where the server registers its tool modules — read, not imported, see below. */
|
|
55
|
+
const CORE_SERVER_SOURCE = new URL('../../../core/create-core-server.ts', import.meta.url);
|
|
56
|
+
|
|
57
|
+
/** Every tool-registering module, by the name the server calls it under. */
|
|
58
|
+
const MODULES: ReadonlyArray<{ name: string; register: (registry: ToolRegistry) => void }> = [
|
|
59
|
+
{
|
|
60
|
+
name: 'registerWorkflowTools',
|
|
61
|
+
register: (registry) => registerWorkflowTools(registry, express.Router(), pass, handler, testKbContext()),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'registerChangeRequestReadTools',
|
|
65
|
+
register: (registry) =>
|
|
66
|
+
registerChangeRequestReadTools(registry, express.Router(), pass, handler, nothing, testKbContext()),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'registerWorkspaceTools',
|
|
70
|
+
register: (registry) =>
|
|
71
|
+
registerWorkspaceTools(
|
|
72
|
+
registry,
|
|
73
|
+
express.Router(),
|
|
74
|
+
pass,
|
|
75
|
+
handler,
|
|
76
|
+
nothing,
|
|
77
|
+
nothing,
|
|
78
|
+
nothing,
|
|
79
|
+
testKbContext(),
|
|
80
|
+
{ recoveryBotEmail: 'recovery@bevel.software', hooks: nothing, notes: new ToolDescriptionNotes() },
|
|
81
|
+
nothing,
|
|
82
|
+
nothing,
|
|
83
|
+
undefined,
|
|
84
|
+
undefined,
|
|
85
|
+
// The upload store: the two upload tools are mounted only when one is
|
|
86
|
+
// supplied, and the server supplies one — so this harness does too,
|
|
87
|
+
// or their schemas would be the two it silently never checked.
|
|
88
|
+
nothing,
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'registerSkillsTools',
|
|
93
|
+
register: (registry) => registerSkillsTools(registry, express.Router(), pass, handler, nothing),
|
|
94
|
+
},
|
|
95
|
+
{ name: 'registerPluginsTools', register: (registry) => registerPluginsTools(registry, testKbContext()) },
|
|
96
|
+
{
|
|
97
|
+
name: 'registerToolManualsTools',
|
|
98
|
+
register: (registry) =>
|
|
99
|
+
registerToolManualsTools(registry, express.Router(), pass, handler, nothing, {
|
|
100
|
+
accessControl: nothing,
|
|
101
|
+
variableStatus: nothing,
|
|
102
|
+
kb: testKbContext(),
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// The one module registering a single tool: a provider on both surfaces,
|
|
107
|
+
// built from the sections the reader answers — none here, which still
|
|
108
|
+
// builds the def.
|
|
109
|
+
name: 'registerAgentGuideTool',
|
|
110
|
+
register: (registry) => registerAgentGuideTool(registry, express.Router(), pass, handler, async () => []),
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
async function toolsOf(modules: ReadonlyArray<(typeof MODULES)[number]>) {
|
|
115
|
+
const registry = new ToolRegistry();
|
|
116
|
+
for (const module of modules) module.register(registry);
|
|
117
|
+
// Both surfaces: the external one is what reaches a connected AI client, the
|
|
118
|
+
// internal one is what the in-app agent calls. Either can carry a bad schema.
|
|
119
|
+
return [...(await registry.listExternal()), ...(await registry.listInternal())];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const allOwnTools = () => toolsOf(MODULES);
|
|
123
|
+
|
|
124
|
+
describe("Hexis's own tool schemas", () => {
|
|
125
|
+
/**
|
|
126
|
+
* The harness's own guard, and the reason it reads a source file: nothing in
|
|
127
|
+
* the registry can tell this test about a module nobody registered. A count
|
|
128
|
+
* cannot either — a new module leaves it larger either way. The server's call
|
|
129
|
+
* list can, and it is the only place that knows the whole set.
|
|
130
|
+
*/
|
|
131
|
+
it('covers every tool module the server registers', () => {
|
|
132
|
+
const source = readFileSync(CORE_SERVER_SOURCE, 'utf8');
|
|
133
|
+
// `register…Tools` and `register…Tool` alike: a module registering one
|
|
134
|
+
// tool is a module whose schema this harness must check too.
|
|
135
|
+
const registered = [...source.matchAll(/\b(register\w+Tools?)\s*\(/g)].map((m) => m[1]);
|
|
136
|
+
expect([...new Set(registered)].sort()).toEqual(MODULES.map((m) => m.name).sort());
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it.each(MODULES.map((m) => [m.name, m] as const))('%s contributes tools to check', async (_name, module) => {
|
|
140
|
+
// A module that registers nothing — a renamed registry method, a provider
|
|
141
|
+
// that threw away its defs — would otherwise pass every assertion below
|
|
142
|
+
// while exercising nothing.
|
|
143
|
+
expect(await toolsOf([module])).not.toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('declares valid JSON Schema for every tool, on both surfaces', async () => {
|
|
147
|
+
const defects = (await allOwnTools())
|
|
148
|
+
.map((tool) => ({ tool: tool.name, defect: inputSchemaDefect(tool.inputs) }))
|
|
149
|
+
.filter((entry) => entry.defect !== null);
|
|
150
|
+
// Named, not counted: a failure has to say WHICH tool and WHERE.
|
|
151
|
+
expect(defects).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('declares an object schema for every tool, as MCP requires of an input schema', async () => {
|
|
155
|
+
const notObjects = (await allOwnTools())
|
|
156
|
+
.filter((tool) => (tool.inputs as { type?: unknown }).type !== 'object')
|
|
157
|
+
.map((tool) => tool.name);
|
|
158
|
+
expect(notObjects).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -23,13 +23,13 @@ import { UuidSessionSink } from '../../workspace/session-sink.js';
|
|
|
23
23
|
import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
|
|
24
24
|
import { CLIENT_SHORT_CUT, TOOL_DESCRIPTION_CAP, clientVisibleLength, firstSentenceEnd } from '../description-length.js';
|
|
25
25
|
import {
|
|
26
|
-
POINTER_GUIDE_NAME_BUDGET,
|
|
27
|
-
SHARED_RULES_POINTER_MAX,
|
|
28
26
|
TOOL_PREFIX_CAP,
|
|
29
27
|
sharedFileRules,
|
|
30
|
-
sharedRulesPointer,
|
|
31
28
|
} from '../../agent-instructions/index.js';
|
|
32
29
|
import { isPlatformFile, platformFilesByDepth } from '@bevel-software/platform-shared';
|
|
30
|
+
import { GET_AGENT_GUIDE_TOOL, GUIDE_FIRST_SENTENCE, guideFirstDescription } from '../guide-first.js';
|
|
31
|
+
import { registerAgentGuideTool } from '../../agent-guide/agent-guide.tools.js';
|
|
32
|
+
import { agentGuideSections } from '../../agent-guide/agent-guide.js';
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* The cap exists because clients cut a long tool description, and they cut it
|
|
@@ -62,16 +62,18 @@ const emptySkills = { listSkills: async () => [] } as unknown as Parameters<type
|
|
|
62
62
|
/**
|
|
63
63
|
* The meta-tools as the hosted endpoint builds them (`mcp.service.ts`):
|
|
64
64
|
* examples written against the namespace it registers the knowledge-base tools
|
|
65
|
-
* under and against the catalog it serves, and the chain
|
|
66
|
-
*
|
|
67
|
-
* really sent, which is its longest form.
|
|
65
|
+
* under and against the catalog it serves, and the chain without the rules it
|
|
66
|
+
* states only in the shared places. So the chain description measured here
|
|
67
|
+
* carries the worked call a client is really sent, which is its longest form.
|
|
68
68
|
*/
|
|
69
|
-
function servedMetaTools(external: readonly UtcpTool[]
|
|
69
|
+
function servedMetaTools(external: readonly UtcpTool[]) {
|
|
70
70
|
return codeModeMetaTools(
|
|
71
71
|
EXTERNAL_KB_MANUAL_NAME,
|
|
72
72
|
external.map((t) => ({ utcpName: `${EXTERNAL_KB_MANUAL_NAME}.${t.name}`, inputSchema: t.inputs })),
|
|
73
|
-
{ sharedRulesPointer:
|
|
74
|
-
|
|
73
|
+
{ sharedRulesPointer: '' },
|
|
74
|
+
// As the hosted endpoint serves them: opening with the guide-first
|
|
75
|
+
// sentence, through the one helper the endpoint itself uses.
|
|
76
|
+
).map((t) => ({ ...t, description: guideFirstDescription(t.description) }));
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** Every tool Hexis itself registers, on both surfaces, deduplicated by name. */
|
|
@@ -103,18 +105,21 @@ async function hexisTools(): Promise<UtcpTool[]> {
|
|
|
103
105
|
unused(),
|
|
104
106
|
);
|
|
105
107
|
registerWorkflowTools(registry, router, toolAuth, toolHandler, kb);
|
|
106
|
-
registerPluginsTools(registry);
|
|
108
|
+
registerPluginsTools(registry, kb);
|
|
107
109
|
registerSkillsTools(registry, router, toolAuth, toolHandler, emptySkills);
|
|
108
110
|
registerToolManualsTools(registry, router, toolAuth, toolHandler, emptyManuals, {
|
|
109
111
|
accessControl: unused(),
|
|
110
112
|
variableStatus: unused(),
|
|
111
113
|
kb,
|
|
112
114
|
});
|
|
115
|
+
// The guide's own tool, with the platform's sections: the one description
|
|
116
|
+
// that lists the sections is measured with the list it really carries.
|
|
117
|
+
registerAgentGuideTool(registry, router, toolAuth, toolHandler, () => agentGuideSections(kb.layout));
|
|
113
118
|
|
|
114
119
|
const byName = new Map<string, UtcpTool>();
|
|
115
|
-
// The meta-tools as a client is served them
|
|
116
|
-
//
|
|
117
|
-
// the catalog that goes out rather than the
|
|
120
|
+
// The meta-tools as a client is served them (`mcp.service.ts` opens each
|
|
121
|
+
// with the guide-first sentence at the mount), so the catalog measured here
|
|
122
|
+
// is the catalog that goes out rather than the bare constant.
|
|
118
123
|
const external = (await registry.listExternal()) as UtcpTool[];
|
|
119
124
|
const metaTools = servedMetaTools(external);
|
|
120
125
|
for (const tool of [...(await registry.listInternal()), ...external, ...metaTools]) {
|
|
@@ -141,9 +146,11 @@ describe('no Hexis tool description is long enough to be cut', () => {
|
|
|
141
146
|
// The cap does not answer the ~500-character cut; the ORDER of the text
|
|
142
147
|
// does, and this is where that claim is checked rather than asserted in a
|
|
143
148
|
// comment. A client that stops at 500 must still have the sentence saying
|
|
144
|
-
// what the tool does — what it loses is the
|
|
145
|
-
//
|
|
146
|
-
//
|
|
149
|
+
// what the tool does — what it loses is the tail. The shared rules are
|
|
150
|
+
// stated in the guide (which the opener sends every client to, including
|
|
151
|
+
// the description-only ones this cut is about) and, for the clients that
|
|
152
|
+
// honour it, in the handshake instructions as well. Lowering
|
|
153
|
+
// the cap to 500 would not buy this; only order does.
|
|
147
154
|
const late = (await hexisTools())
|
|
148
155
|
.map((t) => ({ tool: t.name, endsAt: firstSentenceEnd(t) }))
|
|
149
156
|
.filter((m) => m.endsAt > CLIENT_SHORT_CUT)
|
|
@@ -156,30 +163,26 @@ describe('no Hexis tool description is long enough to be cut', () => {
|
|
|
156
163
|
).toEqual([]);
|
|
157
164
|
});
|
|
158
165
|
|
|
159
|
-
it('
|
|
160
|
-
// The
|
|
161
|
-
//
|
|
162
|
-
// of `AGENTS.md` guarantees nothing about the catalog a renamed deployment
|
|
163
|
-
// serves. Two things make it hold: the pointer is bounded by construction
|
|
164
|
-
// (past `POINTER_GUIDE_NAME_BUDGET` the guide is named by its role), and
|
|
165
|
-
// `clientVisibleLength` charges the worst case rather than this layout's.
|
|
166
|
+
it('opens every tool but the guide\'s own with the guide-first sentence, and fits with it', async () => {
|
|
167
|
+
// The sentence is in the description the registry lists, so the catalog
|
|
168
|
+
// measured here is the catalog a client gets, sentence included.
|
|
166
169
|
const tools = await hexisTools();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
expect(
|
|
170
|
+
// ONE predicate for both halves, so no description falls between them: a
|
|
171
|
+
// tool whose own description is empty is served the sentence alone, and
|
|
172
|
+
// one with text after it has whitespace between — not `here.Read`.
|
|
173
|
+
const opensWithGuide = (t: UtcpTool): boolean => {
|
|
174
|
+
const description = t.description ?? '';
|
|
175
|
+
if (description === GUIDE_FIRST_SENTENCE) return true;
|
|
176
|
+
return description.startsWith(GUIDE_FIRST_SENTENCE) && /^\s/.test(description.slice(GUIDE_FIRST_SENTENCE.length));
|
|
177
|
+
};
|
|
178
|
+
const opened = tools.filter(opensWithGuide);
|
|
179
|
+
expect(opened.length).toBeGreaterThan(0);
|
|
180
|
+
// The guide's own tool is the one exception: it is what the sentence
|
|
181
|
+
// points at, and it is in the catalog measured here so the cap holds on
|
|
182
|
+
// it too (see the first test).
|
|
183
|
+
expect(tools.filter((t) => !opensWithGuide(t)).map((t) => t.name)).toEqual([GET_AGENT_GUIDE_TOOL]);
|
|
184
|
+
for (const tool of opened) {
|
|
185
|
+
expect(clientVisibleLength(tool), tool.name).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
|
|
183
186
|
}
|
|
184
187
|
});
|
|
185
188
|
|
|
@@ -190,12 +193,10 @@ describe('no Hexis tool description is long enough to be cut', () => {
|
|
|
190
193
|
// the catalog the agent reads was over it by 300.
|
|
191
194
|
const read = (await hexisTools()).find((t) => t.name === 'read_file');
|
|
192
195
|
expect(read).toBeDefined();
|
|
193
|
-
// Its own text
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
expect(clientVisibleLength(read!)).toBe(ownAtWorstPointer + TOOL_PREFIX_CAP + 2);
|
|
198
|
-
expect(ownAtWorstPointer + TOOL_PREFIX_CAP + 2).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
|
|
196
|
+
// Its own text (the guide-first sentence included), plus the prefix at
|
|
197
|
+
// ITS cap and the blank line between, must fit — measured as the client
|
|
198
|
+
// sees it.
|
|
199
|
+
expect(clientVisibleLength(read!)).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
|
|
199
200
|
});
|
|
200
201
|
|
|
201
202
|
it('fails, naming the tool, when a paragraph takes a description over the cap', () => {
|
|
@@ -228,7 +229,7 @@ describe('no Hexis tool description is long enough to be cut', () => {
|
|
|
228
229
|
});
|
|
229
230
|
});
|
|
230
231
|
|
|
231
|
-
describe('every file tool
|
|
232
|
+
describe('every file tool opens with the guide-first sentence and carries no shared paragraph', () => {
|
|
232
233
|
/** The tools that used to carry the shared paragraphs — every `mount`ed one. */
|
|
233
234
|
const FILE_TOOLS = [
|
|
234
235
|
'read_file',
|
|
@@ -248,13 +249,12 @@ describe('every file tool ends with the pointer and carries no shared paragraph'
|
|
|
248
249
|
'apply_file_upload',
|
|
249
250
|
];
|
|
250
251
|
|
|
251
|
-
it('
|
|
252
|
+
it('opens each description with the sentence sending the agent to the guide, and ends it as a sentence', async () => {
|
|
252
253
|
const tools = await hexisTools();
|
|
253
|
-
const pointer = sharedRulesPointer(testKbContext().layout);
|
|
254
254
|
for (const name of FILE_TOOLS) {
|
|
255
255
|
const def = tools.find((t) => t.name === name);
|
|
256
256
|
expect(def, name).toBeDefined();
|
|
257
|
-
expect(def!.description!.
|
|
257
|
+
expect(def!.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `), `${name} must open with: ${GUIDE_FIRST_SENTENCE}`).toBe(true);
|
|
258
258
|
// Ends with a full sentence, so nothing reads as cut off mid-thought.
|
|
259
259
|
expect(def!.description!.trimEnd().endsWith('.'), name).toBe(true);
|
|
260
260
|
}
|
|
@@ -283,25 +283,27 @@ describe('every file tool ends with the pointer and carries no shared paragraph'
|
|
|
283
283
|
});
|
|
284
284
|
|
|
285
285
|
describe('the shared rules describe the tools they name', () => {
|
|
286
|
-
it('
|
|
286
|
+
it('opens the chain description with the guide-first sentence too, under the cap', async () => {
|
|
287
287
|
// What a chain does with a failure, a large result or an image is true of
|
|
288
288
|
// every call, so it is stated in the shared rules — and the clients that
|
|
289
289
|
// drop the handshake `instructions` see only descriptions, so the chain
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
290
|
+
// opens with the same sentence every tool of the platform's own opens
|
|
291
|
+
// with, and ends with no second pointer: the one at the front is the way
|
|
292
|
+
// to the rules. Composed at the mount, because `mcp-core` builds the
|
|
293
|
+
// constant without knowing where this deployment states them.
|
|
294
294
|
const served = (await hexisTools()).find((t) => t.name === CALL_TOOL_CHAIN_NAME)!;
|
|
295
|
-
expect(served.description!.
|
|
295
|
+
expect(served.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `)).toBe(true);
|
|
296
|
+
expect(served.description!.split(GUIDE_FIRST_SENTENCE)).toHaveLength(2);
|
|
297
|
+
expect(served.description!.trimEnd().endsWith('.')).toBe(true);
|
|
296
298
|
expect(clientVisibleLength(served)).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
|
|
297
|
-
// The other two
|
|
299
|
+
// The other two open the same way: the guide comes before the registry too.
|
|
298
300
|
for (const tool of servedMetaTools([]).filter((t) => t.name !== CALL_TOOL_CHAIN_NAME)) {
|
|
299
|
-
expect(tool.description).
|
|
301
|
+
expect(tool.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `), tool.name).toBe(true);
|
|
300
302
|
}
|
|
301
303
|
});
|
|
302
304
|
|
|
303
305
|
it('states what a chain does in the shared rules, and not a second time on the chain', async () => {
|
|
304
|
-
// The
|
|
306
|
+
// The opener is only honest if the rules it points at are THERE. These
|
|
305
307
|
// two were paragraphs of the chain's description; they moved, whole, and a
|
|
306
308
|
// description that kept them as well would be the long one a client cuts.
|
|
307
309
|
const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'tool-chain')!;
|
|
@@ -317,7 +319,7 @@ describe('the shared rules describe the tools they name', () => {
|
|
|
317
319
|
|
|
318
320
|
it('keeps the rules on the chain itself where no shared rules are served', () => {
|
|
319
321
|
// The standalone bridge proxies a deployment whose guide it cannot name, so
|
|
320
|
-
// it passes no pointer — and an agent there must still be told what a chain
|
|
322
|
+
// it passes no pointer at all — and an agent there must still be told what a chain
|
|
321
323
|
// that timed out, or answered too much, or read an image, does.
|
|
322
324
|
const [chain] = codeModeMetaTools('hexis', []).filter((t) => t.name === CALL_TOOL_CHAIN_NAME);
|
|
323
325
|
expect(chain.description).toContain(CHAIN_FAILURES_RULE);
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
* where the text specific to the tool sits, after whatever shared preamble it
|
|
6
6
|
* carried. Agents reported `file_stat`, `read_file`, `write_file` and
|
|
7
7
|
* `write_files` arriving ending in "[truncated]". The rules those descriptions
|
|
8
|
-
* shared now live in one place (see `agent-instructions/shared-file-rules.ts
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* shared now live in one place (see `agent-instructions/shared-file-rules.ts`,
|
|
9
|
+
* served in the guide `get_agent_guide` returns) and each description OPENS
|
|
10
|
+
* with the one sentence sending the agent there (`guide-first.ts`), which is
|
|
11
|
+
* what makes the cap below reachable rather than aspirational.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { TOOL_PREFIX_CAP } from '@bevel-software/platform-shared';
|
|
14
15
|
import { PREFIXED_TOOLS } from '../agent-instructions/compose.js';
|
|
15
|
-
import {
|
|
16
|
+
import { GUIDE_FIRST_SENTENCE } from './guide-first.js';
|
|
16
17
|
import type { UtcpTool } from './tool.contract.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -25,8 +26,10 @@ import type { UtcpTool } from './tool.contract.js';
|
|
|
25
26
|
* could answer: no useful description of `move_file` fits in 500. What answers
|
|
26
27
|
* that one is the ORDER of the text, which is why the deployment's purpose line
|
|
27
28
|
* is prepended rather than appended (see `prefixToolDescription`) and why every
|
|
28
|
-
* description now
|
|
29
|
-
*
|
|
29
|
+
* description now opens with the guide-first sentence and then what the tool
|
|
30
|
+
* does, with the tool's own detail last: a cut at 500 keeps the sentence and
|
|
31
|
+
* the tool's first sentence (see `firstSentenceEnd`) and takes the detail,
|
|
32
|
+
* which the guide states in full anyway. The other cut is the
|
|
30
33
|
* four-figure one agents reported on `file_stat`, `read_file`, `write_file` and
|
|
31
34
|
* `write_files`, and 1,200 sits below it with room to spare.
|
|
32
35
|
*
|
|
@@ -60,9 +63,10 @@ export const CLIENT_SHORT_CUT = 500;
|
|
|
60
63
|
|
|
61
64
|
/**
|
|
62
65
|
* Where the tool's OWN opening sentence ends in the text a client is handed:
|
|
63
|
-
* the purpose prefix counted at its cap, as in {@link clientVisibleLength},
|
|
64
|
-
* the
|
|
65
|
-
*
|
|
66
|
+
* the purpose prefix counted at its cap, as in {@link clientVisibleLength},
|
|
67
|
+
* and the guide-first sentence every listed tool opens with counted as the
|
|
68
|
+
* text it is — a client that cuts at 500 must still reach the sentence saying
|
|
69
|
+
* what the tool does, past that one.
|
|
66
70
|
*
|
|
67
71
|
* A description with no sentence-ending punctuation counts whole — the honest
|
|
68
72
|
* answer for text that never finishes a sentence.
|
|
@@ -71,10 +75,15 @@ export function firstSentenceEnd(tool: Pick<UtcpTool, 'name' | 'description'>):
|
|
|
71
75
|
const prefix = PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP + 2 : 0;
|
|
72
76
|
const description = tool.description ?? '';
|
|
73
77
|
if (description === '') return prefix;
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
// The opener and whatever whitespace follows it: `guideFirstDescription`
|
|
79
|
+
// joins with one space, and leaves a description that already opens with
|
|
80
|
+
// the sentence as it came — a newline after it is still the opener's.
|
|
81
|
+
const opener = description.startsWith(GUIDE_FIRST_SENTENCE)
|
|
82
|
+
? GUIDE_FIRST_SENTENCE.length + (description.slice(GUIDE_FIRST_SENTENCE.length).match(/^\s*/)?.[0].length ?? 0)
|
|
83
|
+
: 0;
|
|
84
|
+
const own = description.slice(opener);
|
|
76
85
|
const firstSentence = own.match(/^[\s\S]*?[.!?](?=\s|$)/)?.[0] ?? own;
|
|
77
|
-
return prefix + firstSentence.length;
|
|
86
|
+
return prefix + opener + firstSentence.length;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
/**
|
|
@@ -85,12 +94,8 @@ export function firstSentenceEnd(tool: Pick<UtcpTool, 'name' | 'description'>):
|
|
|
85
94
|
* the cap is what an admin may grow their text to without being told, so a
|
|
86
95
|
* description that only fits beside a short prefix does not really fit.
|
|
87
96
|
*
|
|
88
|
-
* The
|
|
89
|
-
*
|
|
90
|
-
* — the guide's file name — so a description measured beside the nine
|
|
91
|
-
* characters of `AGENTS.md` would pass here and arrive cut on a deployment
|
|
92
|
-
* that renamed its guide. Whatever pointer a description actually carries is
|
|
93
|
-
* discounted and charged at {@link SHARED_RULES_POINTER_MAX} instead.
|
|
97
|
+
* The guide-first sentence every listed tool opens with is already in the
|
|
98
|
+
* description the registry lists, so it is measured as the text it is.
|
|
94
99
|
*/
|
|
95
100
|
export function clientVisibleLength(tool: Pick<UtcpTool, 'name' | 'description'>): number {
|
|
96
101
|
const own = tool.description?.length ?? 0;
|
|
@@ -99,13 +104,6 @@ export function clientVisibleLength(tool: Pick<UtcpTool, 'name' | 'description'>
|
|
|
99
104
|
// blank line after it. Measuring that as zero would under-report the only
|
|
100
105
|
// text the client got.
|
|
101
106
|
if (own === 0) return PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP : 0;
|
|
102
|
-
// The pointer at its worst case rather than at this layout's: swap the one
|
|
103
|
-
// it carries for the longest it could be. A description that does not end
|
|
104
|
-
// with it (`start_session`, the proxied tools) is charged nothing.
|
|
105
|
-
const pointer = sharedRulesPointer();
|
|
106
|
-
const atWorstPointer = tool.description!.endsWith(pointer)
|
|
107
|
-
? own - pointer.length + SHARED_RULES_POINTER_MAX
|
|
108
|
-
: own;
|
|
109
107
|
// `+ 2` for the blank line `prefixToolDescription` puts between the two.
|
|
110
|
-
return PREFIXED_TOOLS.has(tool.name) ?
|
|
108
|
+
return PREFIXED_TOOLS.has(tool.name) ? own + TOOL_PREFIX_CAP + 2 : own;
|
|
111
109
|
}
|