@feniix/bridgekit 0.5.0 → 0.7.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/CHANGELOG.md +109 -0
- package/README.md +41 -2
- package/dist/src/adapters/mcp-signal.d.ts +8 -0
- package/dist/src/adapters/mcp-signal.js +8 -0
- package/dist/src/adapters/pi.d.ts +45 -3
- package/dist/src/adapters/pi.js +63 -13
- package/dist/src/core/define-tool.d.ts +30 -3
- package/dist/src/core/execute-tool.d.ts +2 -8
- package/dist/src/core/execute-tool.js +1 -0
- package/dist/src/core/result-guards.d.ts +63 -0
- package/dist/src/core/result-guards.js +43 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +2 -1
- package/dist/src/pi.d.ts +1 -1
- package/llms.txt +26 -3
- package/package.json +3 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@feniix/bridgekit` are documented here. The format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.7.0] - Unreleased
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`registerPiTools` now returns `isError` results by default instead of
|
|
12
|
+
throwing.** The third parameter is a new `RegisterPiToolsOptions` with
|
|
13
|
+
`errorHandling?: "throw" | "return"`; the default is `"return"`, mirroring
|
|
14
|
+
the MCP adapter. Portable validation failures and portable `isError: true`
|
|
15
|
+
results now surface as `{ content, details, isError: true }` on the pi side.
|
|
16
|
+
Unexpected exceptions thrown by tool handlers are also caught in return
|
|
17
|
+
mode and surfaced as `{ content: [{type:"text", text: message}], details: {}, isError: true }`,
|
|
18
|
+
matching MCP. This makes the documented "pi throws, MCP returns" asymmetry
|
|
19
|
+
symmetric. Resolves [#2](https://github.com/feniix/bridgekit/issues/2).
|
|
20
|
+
- pi success-path results now include `isError: false` explicitly so consumers
|
|
21
|
+
can write `result.isError === false` symmetrically across pi and MCP.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `isValidationFailure(result)` and `isDomainFailure(result)` type guards in
|
|
26
|
+
the root entrypoint. Use them after `result.isError` to narrow between
|
|
27
|
+
BridgeKit-emitted validation failures and tool-emitted domain failures.
|
|
28
|
+
Resolves [#30](https://github.com/feniix/bridgekit/issues/30).
|
|
29
|
+
- `PortableValidationFailure` and `PortableDomainFailure` types alongside the
|
|
30
|
+
guards.
|
|
31
|
+
- `RegisterPiToolsOptions` exported from `@feniix/bridgekit/pi`.
|
|
32
|
+
- A `DeprecationWarning` (code `BRIDGEKIT_PI_THROW_DEPRECATED`) is emitted
|
|
33
|
+
once per process when `registerPiTools(..., { errorHandling: "throw" })`
|
|
34
|
+
is invoked, so consumers see the migration signal at runtime instead of
|
|
35
|
+
only in JSDoc.
|
|
36
|
+
|
|
37
|
+
### Deprecated
|
|
38
|
+
|
|
39
|
+
- `registerPiTools(..., { errorHandling: "throw" })` is retained for one
|
|
40
|
+
minor-version cycle so existing pi extensions can migrate without source
|
|
41
|
+
changes. It will be removed in 1.0. Only the `"throw"` *value* is
|
|
42
|
+
deprecated; the `errorHandling` option itself stays. Migrate by switching
|
|
43
|
+
to the default `"return"` mode and branching on `result.isError`.
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
|
|
47
|
+
- The duplicate `PortableValidationError` re-export from `src/core/execute-tool.ts`.
|
|
48
|
+
The canonical export still lives in `src/core/define-tool.ts` and is surfaced
|
|
49
|
+
through `@feniix/bridgekit` unchanged.
|
|
50
|
+
|
|
51
|
+
### Migration
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// Before (0.6 and earlier)
|
|
55
|
+
try {
|
|
56
|
+
const result = await piTool.execute(toolCallId, params, signal);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (isPortableToolExecutionError(err)) {
|
|
59
|
+
if (err.details.kind === "validation") { /* TypeBox errors */ }
|
|
60
|
+
else { /* domain failure */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// After (0.7+)
|
|
65
|
+
const result = await piTool.execute(toolCallId, params, signal);
|
|
66
|
+
if (result.isError) {
|
|
67
|
+
if (isValidationFailure(result)) { /* result.structuredContent.validationErrors */ }
|
|
68
|
+
else if (isDomainFailure(result)) { /* handler-level error */ }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Notes on the guards
|
|
73
|
+
|
|
74
|
+
`isValidationFailure` narrows `structuredContent` to
|
|
75
|
+
`{ kind: "validation"; tool: string; validationErrors: PortableValidationError[] }`
|
|
76
|
+
because `executePortableTool` produces that exact shape on TypeBox rejection.
|
|
77
|
+
The guard matches on shape, not provenance: a handler that returns the same
|
|
78
|
+
shape on its own will also satisfy it. Keep handler-emitted error shapes
|
|
79
|
+
distinct from the validation discriminator if that ambiguity matters.
|
|
80
|
+
|
|
81
|
+
`isDomainFailure` narrows only to `result & { isError: true }`. It does **not**
|
|
82
|
+
synthesize a `kind: "domain"` discriminator on `structuredContent` because the
|
|
83
|
+
tool's handler chose `structuredContent` freely and BridgeKit does not rewrite
|
|
84
|
+
it on the wire.
|
|
85
|
+
|
|
86
|
+
#### Caveat: `kind` is not the same between modes
|
|
87
|
+
|
|
88
|
+
The two `errorHandling` modes expose the failure discriminator on different
|
|
89
|
+
fields and with different semantics. Branch by mode, not by reading `.kind`
|
|
90
|
+
across both shapes:
|
|
91
|
+
|
|
92
|
+
| Mode | Where to read | Validation failure | Handler-emitted failure |
|
|
93
|
+
|------|---------------|--------------------|-------------------------|
|
|
94
|
+
| `"return"` (default, 0.7+) | `result.structuredContent` | `{ kind: "validation", tool, validationErrors }` (BridgeKit-emitted) | Whatever the handler returned — no synthesized `kind` |
|
|
95
|
+
| `"throw"` (deprecated) | `(err as PortableToolExecutionError).details` | `{ kind: "validation", tool, validationErrors }` | `{ kind: "domain", ...rest }` (BridgeKit synthesizes) |
|
|
96
|
+
|
|
97
|
+
In return mode, prefer the type guards (`isValidationFailure`,
|
|
98
|
+
`isDomainFailure`) over reading `kind` directly. In throw mode, the
|
|
99
|
+
`PortableToolExecutionError.details.kind` discriminator is stable but the
|
|
100
|
+
whole mode is deprecated.
|
|
101
|
+
|
|
102
|
+
### Scope: where to call the guards
|
|
103
|
+
|
|
104
|
+
The guards take a `PortableToolResult` — the value `executePortableTool`
|
|
105
|
+
returns, or what your portable tool's own `execute` returns at the seam
|
|
106
|
+
between BridgeKit and a host. They are **not** designed to operate on the pi
|
|
107
|
+
adapter's wire object (`{ content, details, isError }`), which exposes
|
|
108
|
+
`details` instead of `structuredContent`. Calling the guards on a pi wire
|
|
109
|
+
object will always return `false`.
|
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ Read these files in order:
|
|
|
21
21
|
import {
|
|
22
22
|
definePortableTool,
|
|
23
23
|
executePortableTool,
|
|
24
|
+
isDomainFailure,
|
|
25
|
+
isValidationFailure,
|
|
24
26
|
type PortableTool,
|
|
25
27
|
type PortableToolBuiltInHost,
|
|
26
28
|
type PortableToolContext,
|
|
@@ -88,7 +90,42 @@ export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
|
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
By default (`errorHandling: "return"`, as of 0.7) the pi adapter mirrors the MCP adapter: portable validation failures and portable `isError: true` results surface as `{ content, details, isError: true }` so consumers can branch on `result.isError` and narrow with `isValidationFailure` / `isDomainFailure`. Unexpected handler exceptions are caught and surfaced as `{ content: [{type:"text", text: message}], details: {}, isError: true }`, matching MCP. Success-path results include `isError: false` explicitly so consumers can use the same strict-equality checks across both adapters. `details` is populated from `structuredContent` first, then from `details`, then `{}`. Progress updates from `ctx.progress?.(...)` map to pi tool updates.
|
|
94
|
+
|
|
95
|
+
The pre-0.7 behavior — throw `PortableToolExecutionError` on `isError` — is still available for one deprecation cycle:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
registerPiTools(pi, createTools(), { errorHandling: "throw" });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Selecting `errorHandling: "throw"` emits a `DeprecationWarning` (code `BRIDGEKIT_PI_THROW_DEPRECATED`) once per process. Only the `"throw"` value is deprecated; the `errorHandling` option itself remains. Migrate by switching to the default and branching on the returned result:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// Before (0.6 and earlier)
|
|
105
|
+
try {
|
|
106
|
+
const result = await piTool.execute(...);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (isPortableToolExecutionError(err)) {
|
|
109
|
+
if (err.details.kind === "validation") { /* TypeBox errors */ }
|
|
110
|
+
else { /* domain failure */ }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// After (0.7+)
|
|
115
|
+
const result = await piTool.execute(...);
|
|
116
|
+
if (result.isError) {
|
|
117
|
+
if (isValidationFailure(result)) { /* result.structuredContent.validationErrors */ }
|
|
118
|
+
else if (isDomainFailure(result)) { /* handler-level error */ }
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The two modes expose the failure discriminator on different fields. In the
|
|
123
|
+
new default `"return"` mode, prefer the result guards over inspecting a
|
|
124
|
+
`kind` field directly — handler-emitted failures do not carry a synthesized
|
|
125
|
+
`kind`. In the deprecated `"throw"` mode, the discriminator lives on
|
|
126
|
+
`(err as PortableToolExecutionError).details.kind` (`"validation"` or
|
|
127
|
+
`"domain"`). The guards operate on a `PortableToolResult`; they are not
|
|
128
|
+
designed to be called on the pi adapter's wire object.
|
|
92
129
|
|
|
93
130
|
## MCP adapter
|
|
94
131
|
|
|
@@ -127,7 +164,9 @@ if (process.argv[1] && realpathIfPossible(resolve(process.argv[1])) === realpath
|
|
|
127
164
|
|
|
128
165
|
The MCP adapter uses low-level `tools/list` and `tools/call` handlers so TypeBox schemas are exposed as JSON Schema directly. It intentionally does not expose a high-level `registerMcpTools` helper.
|
|
129
166
|
|
|
130
|
-
|
|
167
|
+
Portable validation failures and portable `isError: true` results return `CallToolResult` with `isError: true`. `structuredContent` is preserved; `details` is used only as a fallback when `structuredContent` is absent. Exporting a server-options factory keeps MCP entrypoints import-passive and easy to test without starting stdio.
|
|
168
|
+
|
|
169
|
+
The two adapters now read in parallel: invalid args and portable `isError` results return `{ isError: true }` from both hosts by default, and the same result-guard helpers (`isValidationFailure`, `isDomainFailure`) narrow them on either side.
|
|
131
170
|
|
|
132
171
|
## Custom host typing
|
|
133
172
|
|
|
@@ -1 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensively pulls an `AbortSignal` out of MCP request `extra`. The MCP SDK
|
|
3
|
+
* does not formally type a `signal` on `extra`, so the shape is sniffed
|
|
4
|
+
* structurally.
|
|
5
|
+
*
|
|
6
|
+
* Validated against `@modelcontextprotocol/sdk` v1.x. Revalidate on any SDK
|
|
7
|
+
* major bump in case the cancellation channel moves or gains a typed surface.
|
|
8
|
+
*/
|
|
1
9
|
export declare function signalFromExtra(extra: unknown): AbortSignal | undefined;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensively pulls an `AbortSignal` out of MCP request `extra`. The MCP SDK
|
|
3
|
+
* does not formally type a `signal` on `extra`, so the shape is sniffed
|
|
4
|
+
* structurally.
|
|
5
|
+
*
|
|
6
|
+
* Validated against `@modelcontextprotocol/sdk` v1.x. Revalidate on any SDK
|
|
7
|
+
* major bump in case the cancellation channel moves or gains a typed surface.
|
|
8
|
+
*/
|
|
1
9
|
export function signalFromExtra(extra) {
|
|
2
10
|
if (!extra || typeof extra !== "object" || !("signal" in extra)) {
|
|
3
11
|
return undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
|
-
import type { PortableTool, PortableToolResult } from "../core/define-tool.js";
|
|
2
|
+
import type { PortableTool, PortableToolErrorDetails, PortableToolResult } from "../core/define-tool.js";
|
|
3
3
|
type PiContent = {
|
|
4
4
|
type: "text";
|
|
5
5
|
text: string;
|
|
@@ -11,6 +11,7 @@ type PiToolUpdate = {
|
|
|
11
11
|
type PiToolResult = {
|
|
12
12
|
content: PiContent[];
|
|
13
13
|
details: Record<string, unknown>;
|
|
14
|
+
isError?: boolean;
|
|
14
15
|
};
|
|
15
16
|
type PiToolDefinition = {
|
|
16
17
|
name: string;
|
|
@@ -22,10 +23,51 @@ type PiToolDefinition = {
|
|
|
22
23
|
export type PiToolRegistration = {
|
|
23
24
|
registerTool(tool: PiToolDefinition): unknown;
|
|
24
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* How the pi adapter surfaces portable `isError: true` results (both
|
|
28
|
+
* BridgeKit-emitted validation failures and tool-emitted domain failures).
|
|
29
|
+
*
|
|
30
|
+
* - `"return"` (default as of 0.7) — return `{ content, details, isError: true }`
|
|
31
|
+
* so the result mirrors the MCP adapter's `CallToolResult`. Consumers branch
|
|
32
|
+
* on `result.isError` and may narrow with `isValidationFailure` /
|
|
33
|
+
* `isDomainFailure`.
|
|
34
|
+
* - `"throw"` (deprecated, scheduled for removal in 1.0) — throw
|
|
35
|
+
* `PortableToolExecutionError`. Available for one minor-version cycle so
|
|
36
|
+
* existing pi extensions can migrate without source changes. Selecting
|
|
37
|
+
* this value emits a `DeprecationWarning` (code
|
|
38
|
+
* `BRIDGEKIT_PI_THROW_DEPRECATED`) once per process.
|
|
39
|
+
*/
|
|
40
|
+
export interface RegisterPiToolsOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Controls how `isError: true` portable results are surfaced. Defaults to
|
|
43
|
+
* `"return"`. Passing `"throw"` is deprecated and will be removed in 1.0;
|
|
44
|
+
* only the `"throw"` value is deprecated, not the option itself.
|
|
45
|
+
*
|
|
46
|
+
* @default "return"
|
|
47
|
+
*/
|
|
48
|
+
errorHandling?: "throw" | "return";
|
|
49
|
+
}
|
|
25
50
|
export declare class PortableToolExecutionError extends Error {
|
|
26
|
-
readonly details:
|
|
51
|
+
readonly details: PortableToolErrorDetails;
|
|
27
52
|
constructor(result: PortableToolResult);
|
|
28
53
|
}
|
|
29
54
|
export declare function isPortableToolExecutionError(error: unknown): error is PortableToolExecutionError;
|
|
30
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Register a set of portable tools with a pi tool registry.
|
|
57
|
+
*
|
|
58
|
+
* Defaults to `errorHandling: "return"` so pi behaves like the MCP adapter:
|
|
59
|
+
* portable `isError: true` results and TypeBox validation failures surface as
|
|
60
|
+
* `{ content, details, isError: true }` rather than thrown exceptions. Pass
|
|
61
|
+
* `{ errorHandling: "throw" }` to opt into the pre-0.7 throwing behavior for
|
|
62
|
+
* one deprecation cycle.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // 0.7+ default: errors as data.
|
|
66
|
+
* registerPiTools(pi, tools);
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Deprecated, but available through 0.x.
|
|
70
|
+
* registerPiTools(pi, tools, { errorHandling: "throw" });
|
|
71
|
+
*/
|
|
72
|
+
export declare function registerPiTools(pi: PiToolRegistration, tools: readonly PortableTool<TSchema>[], options?: RegisterPiToolsOptions): void;
|
|
31
73
|
export {};
|
package/dist/src/adapters/pi.js
CHANGED
|
@@ -1,19 +1,54 @@
|
|
|
1
1
|
import { executePortableTool } from "../core/execute-tool.js";
|
|
2
|
+
import { isValidationFailure } from "../core/result-guards.js";
|
|
2
3
|
function toPiDetails(result) {
|
|
3
4
|
return result.structuredContent ?? result.details ?? {};
|
|
4
5
|
}
|
|
6
|
+
function toPortableToolErrorDetails(result) {
|
|
7
|
+
if (isValidationFailure(result)) {
|
|
8
|
+
return result.structuredContent;
|
|
9
|
+
}
|
|
10
|
+
const { kind: _ignored, ...rest } = toPiDetails(result);
|
|
11
|
+
return { kind: "domain", ...rest };
|
|
12
|
+
}
|
|
5
13
|
export class PortableToolExecutionError extends Error {
|
|
6
14
|
details;
|
|
7
15
|
constructor(result) {
|
|
8
16
|
super(result.text);
|
|
9
17
|
this.name = "PortableToolExecutionError";
|
|
10
|
-
this.details =
|
|
18
|
+
this.details = toPortableToolErrorDetails(result);
|
|
11
19
|
}
|
|
12
20
|
}
|
|
13
21
|
export function isPortableToolExecutionError(error) {
|
|
14
22
|
return error instanceof PortableToolExecutionError;
|
|
15
23
|
}
|
|
16
|
-
|
|
24
|
+
let throwModeDeprecationWarned = false;
|
|
25
|
+
/**
|
|
26
|
+
* Register a set of portable tools with a pi tool registry.
|
|
27
|
+
*
|
|
28
|
+
* Defaults to `errorHandling: "return"` so pi behaves like the MCP adapter:
|
|
29
|
+
* portable `isError: true` results and TypeBox validation failures surface as
|
|
30
|
+
* `{ content, details, isError: true }` rather than thrown exceptions. Pass
|
|
31
|
+
* `{ errorHandling: "throw" }` to opt into the pre-0.7 throwing behavior for
|
|
32
|
+
* one deprecation cycle.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // 0.7+ default: errors as data.
|
|
36
|
+
* registerPiTools(pi, tools);
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Deprecated, but available through 0.x.
|
|
40
|
+
* registerPiTools(pi, tools, { errorHandling: "throw" });
|
|
41
|
+
*/
|
|
42
|
+
export function registerPiTools(pi, tools, options = {}) {
|
|
43
|
+
const errorHandling = options.errorHandling ?? "return";
|
|
44
|
+
if (errorHandling === "throw" && !throwModeDeprecationWarned) {
|
|
45
|
+
throwModeDeprecationWarned = true;
|
|
46
|
+
process.emitWarning('`registerPiTools(..., { errorHandling: "throw" })` is deprecated and will be removed in 1.0. ' +
|
|
47
|
+
'Migrate to the default `"return"` mode and branch on `result.isError`.', {
|
|
48
|
+
type: "DeprecationWarning",
|
|
49
|
+
code: "BRIDGEKIT_PI_THROW_DEPRECATED",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
17
52
|
for (const tool of tools) {
|
|
18
53
|
pi.registerTool({
|
|
19
54
|
name: tool.name,
|
|
@@ -21,22 +56,37 @@ export function registerPiTools(pi, tools) {
|
|
|
21
56
|
description: tool.description,
|
|
22
57
|
parameters: tool.parameters,
|
|
23
58
|
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = await executePortableTool(tool, params, {
|
|
62
|
+
host: "pi",
|
|
63
|
+
signal,
|
|
64
|
+
progress(update) {
|
|
65
|
+
onUpdate?.({
|
|
66
|
+
content: [{ type: "text", text: update.text }],
|
|
67
|
+
details: toPiDetails(update),
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (errorHandling === "throw") {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: message }],
|
|
79
|
+
details: {},
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (result.isError && errorHandling === "throw") {
|
|
35
84
|
throw new PortableToolExecutionError(result);
|
|
36
85
|
}
|
|
37
86
|
return {
|
|
38
87
|
content: [{ type: "text", text: result.text }],
|
|
39
88
|
details: toPiDetails(result),
|
|
89
|
+
isError: result.isError === true,
|
|
40
90
|
};
|
|
41
91
|
},
|
|
42
92
|
});
|
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
import type { Static, TSchema } from "typebox";
|
|
2
|
-
export interface PortableToolResult {
|
|
2
|
+
export interface PortableToolResult<TStructured extends Record<string, unknown> = Record<string, unknown>> {
|
|
3
3
|
/** Plain text sent back to the model in every host. */
|
|
4
4
|
text: string;
|
|
5
5
|
/** Structured data for hosts that support it. Preferred by both pi and MCP adapters. */
|
|
6
|
-
structuredContent?:
|
|
7
|
-
/**
|
|
6
|
+
structuredContent?: TStructured;
|
|
7
|
+
/**
|
|
8
|
+
* Legacy/adapter debug details used only when `structuredContent` is absent.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Slated for removal in 1.0. Prefer `structuredContent` for
|
|
11
|
+
* machine-readable data. Both adapters still fall back to this field, but
|
|
12
|
+
* new tool code should not set it.
|
|
13
|
+
*/
|
|
8
14
|
details?: Record<string, unknown>;
|
|
9
15
|
/** Tool-level error flag. Throw for unexpected adapter/runtime failures. */
|
|
10
16
|
isError?: boolean;
|
|
11
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Discriminated union describing the shape `executePortableTool` puts into a
|
|
20
|
+
* failure result's `structuredContent`, and that the pi adapter exposes as
|
|
21
|
+
* `PortableToolExecutionError.details`.
|
|
22
|
+
*
|
|
23
|
+
* - `kind: "validation"` — TypeBox rejected the args. Always carries the
|
|
24
|
+
* offending tool name and the validation errors.
|
|
25
|
+
* - `kind: "domain"` — the tool's own handler returned `isError: true`. The
|
|
26
|
+
* shape of the rest of the object is whatever the handler chose to expose.
|
|
27
|
+
*/
|
|
28
|
+
export type PortableToolErrorDetails = {
|
|
29
|
+
kind: "validation";
|
|
30
|
+
tool: string;
|
|
31
|
+
validationErrors: PortableValidationError[];
|
|
32
|
+
} | ({
|
|
33
|
+
kind: "domain";
|
|
34
|
+
} & Record<string, unknown>);
|
|
35
|
+
export interface PortableValidationError {
|
|
36
|
+
path: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
12
39
|
export type PortableToolBuiltInHost = "pi" | "mcp" | "test";
|
|
13
40
|
export type PortableToolHost<TExtension extends string = never> = PortableToolBuiltInHost | TExtension;
|
|
14
41
|
export interface PortableToolContext<THost extends string = PortableToolBuiltInHost> {
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
|
-
import type { PortableTool, PortableToolBuiltInHost, PortableToolContext, PortableToolResult } from "./define-tool.js";
|
|
3
|
-
type NoInferPortable<T> = [T][T extends unknown ? 0 : never];
|
|
4
|
-
export interface PortableValidationError {
|
|
5
|
-
path: string;
|
|
6
|
-
message: string;
|
|
7
|
-
}
|
|
2
|
+
import type { PortableTool, PortableToolBuiltInHost, PortableToolContext, PortableToolResult, PortableValidationError } from "./define-tool.js";
|
|
8
3
|
export declare function validatePortableToolArgs<THost extends string = PortableToolBuiltInHost>(tool: PortableTool<TSchema, THost>, args: unknown): {
|
|
9
4
|
ok: true;
|
|
10
5
|
} | {
|
|
11
6
|
ok: false;
|
|
12
7
|
errors: PortableValidationError[];
|
|
13
8
|
};
|
|
14
|
-
export declare function executePortableTool<THost extends string = PortableToolBuiltInHost>(tool: PortableTool<TSchema, THost>, args: unknown, ctx: PortableToolContext<
|
|
15
|
-
export {};
|
|
9
|
+
export declare function executePortableTool<THost extends string = PortableToolBuiltInHost>(tool: PortableTool<TSchema, THost>, args: unknown, ctx: PortableToolContext<NoInfer<THost>>): Promise<PortableToolResult>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { PortableToolResult, PortableValidationError } from "./define-tool.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validation-failure narrowing of `PortableToolResult`.
|
|
4
|
+
*
|
|
5
|
+
* `executePortableTool` produces this shape when TypeBox rejects the args:
|
|
6
|
+
* `structuredContent` carries `kind: "validation"`, the offending tool name,
|
|
7
|
+
* and the list of validation errors. The flag is also set to `isError: true`.
|
|
8
|
+
*
|
|
9
|
+
* This is the typed form a consumer can match against in both pi (default
|
|
10
|
+
* `errorHandling: "return"`) and MCP adapters.
|
|
11
|
+
*/
|
|
12
|
+
export type PortableValidationFailure = PortableToolResult & {
|
|
13
|
+
isError: true;
|
|
14
|
+
structuredContent: {
|
|
15
|
+
kind: "validation";
|
|
16
|
+
tool: string;
|
|
17
|
+
validationErrors: PortableValidationError[];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Domain-failure narrowing of `PortableToolResult`.
|
|
22
|
+
*
|
|
23
|
+
* Any `result.isError === true` that is not a validation failure is treated
|
|
24
|
+
* as a domain failure. `structuredContent` stays whatever the handler chose;
|
|
25
|
+
* the guard does not synthesize a `kind: "domain"` discriminator on the wire.
|
|
26
|
+
* (The pi adapter's `PortableToolExecutionError.details` does synthesize that
|
|
27
|
+
* discriminator for the deprecated `errorHandling: "throw"` path.)
|
|
28
|
+
*/
|
|
29
|
+
export type PortableDomainFailure = PortableToolResult & {
|
|
30
|
+
isError: true;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Type guard for results produced by `executePortableTool` when TypeBox
|
|
34
|
+
* validation rejected the args.
|
|
35
|
+
*
|
|
36
|
+
* The guard matches on shape, not provenance: a tool handler that manually
|
|
37
|
+
* returns `{ isError: true, structuredContent: { kind: "validation", tool, validationErrors } }`
|
|
38
|
+
* will also satisfy this predicate. BridgeKit cannot authenticate the source of
|
|
39
|
+
* a result on the wire, so handler-emitted validation-shaped failures are
|
|
40
|
+
* indistinguishable from BridgeKit-emitted ones. If that ambiguity matters,
|
|
41
|
+
* keep handler-emitted error shapes distinct from the validation discriminator.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const result = await executePortableTool(tool, args, ctx);
|
|
45
|
+
* if (result.isError) {
|
|
46
|
+
* if (isValidationFailure(result)) {
|
|
47
|
+
* log.warn({ errors: result.structuredContent.validationErrors }, "bad args");
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
export declare function isValidationFailure(result: PortableToolResult): result is PortableValidationFailure;
|
|
52
|
+
/**
|
|
53
|
+
* Type guard for handler-emitted `isError: true` results that are not
|
|
54
|
+
* validation failures. Use after `result.isError` to branch between
|
|
55
|
+
* BridgeKit-emitted validation failures and tool-emitted domain failures.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const result = await executePortableTool(tool, args, ctx);
|
|
59
|
+
* if (isDomainFailure(result)) {
|
|
60
|
+
* metrics.increment("tool.domain_error", { tool: tool.name });
|
|
61
|
+
* }
|
|
62
|
+
*/
|
|
63
|
+
export declare function isDomainFailure(result: PortableToolResult): result is PortableDomainFailure;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard for results produced by `executePortableTool` when TypeBox
|
|
3
|
+
* validation rejected the args.
|
|
4
|
+
*
|
|
5
|
+
* The guard matches on shape, not provenance: a tool handler that manually
|
|
6
|
+
* returns `{ isError: true, structuredContent: { kind: "validation", tool, validationErrors } }`
|
|
7
|
+
* will also satisfy this predicate. BridgeKit cannot authenticate the source of
|
|
8
|
+
* a result on the wire, so handler-emitted validation-shaped failures are
|
|
9
|
+
* indistinguishable from BridgeKit-emitted ones. If that ambiguity matters,
|
|
10
|
+
* keep handler-emitted error shapes distinct from the validation discriminator.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const result = await executePortableTool(tool, args, ctx);
|
|
14
|
+
* if (result.isError) {
|
|
15
|
+
* if (isValidationFailure(result)) {
|
|
16
|
+
* log.warn({ errors: result.structuredContent.validationErrors }, "bad args");
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export function isValidationFailure(result) {
|
|
21
|
+
if (result.isError !== true)
|
|
22
|
+
return false;
|
|
23
|
+
const structured = result.structuredContent;
|
|
24
|
+
if (!structured || typeof structured !== "object")
|
|
25
|
+
return false;
|
|
26
|
+
return (structured.kind === "validation" &&
|
|
27
|
+
typeof structured.tool === "string" &&
|
|
28
|
+
Array.isArray(structured.validationErrors));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Type guard for handler-emitted `isError: true` results that are not
|
|
32
|
+
* validation failures. Use after `result.isError` to branch between
|
|
33
|
+
* BridgeKit-emitted validation failures and tool-emitted domain failures.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const result = await executePortableTool(tool, args, ctx);
|
|
37
|
+
* if (isDomainFailure(result)) {
|
|
38
|
+
* metrics.increment("tool.domain_error", { tool: tool.name });
|
|
39
|
+
* }
|
|
40
|
+
*/
|
|
41
|
+
export function isDomainFailure(result) {
|
|
42
|
+
return result.isError === true && !isValidationFailure(result);
|
|
43
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { definePortableTool, type PortableTool, type PortableToolBuiltInHost, type PortableToolContext, type PortableToolHost, type PortableToolResult, } from "./core/define-tool.js";
|
|
2
|
-
export { executePortableTool,
|
|
1
|
+
export { definePortableTool, type PortableTool, type PortableToolBuiltInHost, type PortableToolContext, type PortableToolErrorDetails, type PortableToolHost, type PortableToolResult, type PortableValidationError, } from "./core/define-tool.js";
|
|
2
|
+
export { executePortableTool, validatePortableToolArgs } from "./core/execute-tool.js";
|
|
3
|
+
export { isDomainFailure, isValidationFailure, type PortableDomainFailure, type PortableValidationFailure, } from "./core/result-guards.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { definePortableTool, } from "./core/define-tool.js";
|
|
2
|
-
export { executePortableTool, validatePortableToolArgs
|
|
2
|
+
export { executePortableTool, validatePortableToolArgs } from "./core/execute-tool.js";
|
|
3
|
+
export { isDomainFailure, isValidationFailure, } from "./core/result-guards.js";
|
package/dist/src/pi.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { isPortableToolExecutionError, type PiToolRegistration, PortableToolExecutionError, registerPiTools, } from "./adapters/pi.js";
|
|
1
|
+
export { isPortableToolExecutionError, type PiToolRegistration, PortableToolExecutionError, type RegisterPiToolsOptions, registerPiTools, } from "./adapters/pi.js";
|
package/llms.txt
CHANGED
|
@@ -12,7 +12,12 @@ Use this package when you need to define a tool once and expose it through both
|
|
|
12
12
|
## Import map
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
definePortableTool,
|
|
17
|
+
executePortableTool,
|
|
18
|
+
isDomainFailure,
|
|
19
|
+
isValidationFailure,
|
|
20
|
+
} from "@feniix/bridgekit";
|
|
16
21
|
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
17
22
|
import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
|
|
18
23
|
```
|
|
@@ -65,9 +70,27 @@ export function createTools() {
|
|
|
65
70
|
|
|
66
71
|
## Host behavior
|
|
67
72
|
|
|
68
|
-
- pi invalid arguments and portable `isError: true` results
|
|
69
|
-
- MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`.
|
|
73
|
+
- pi invalid arguments and portable `isError: true` results return `{ content, details, isError: true }` by default (as of 0.7). Unexpected handler exceptions are also caught in return mode and surfaced as `{ content, details: {}, isError: true }`. Branch on `result.isError` and narrow with `isValidationFailure` / `isDomainFailure` from `@feniix/bridgekit`.
|
|
74
|
+
- MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`. Same guards narrow the result.
|
|
70
75
|
- MCP preserves `structuredContent`; pi maps `structuredContent` into the pi `details` field. Both adapters fall back to `details` for legacy/debug payloads when `structuredContent` is absent.
|
|
76
|
+
- The result guards take a `PortableToolResult` — the value `executePortableTool` returns at the seam between BridgeKit and a host. They are not designed to be called on the pi adapter's wire object (`{ content, details, isError }`), which exposes `details` instead of `structuredContent`.
|
|
77
|
+
- `registerPiTools(pi, tools, { errorHandling: "throw" })` opts into the pre-0.7 behavior where `isError` results throw `PortableToolExecutionError`. Selecting this value emits a `DeprecationWarning` (code `BRIDGEKIT_PI_THROW_DEPRECATED`) once per process; only the `"throw"` value is deprecated, not the option itself. Scheduled for removal in 1.0; do not adopt it for new code.
|
|
78
|
+
|
|
79
|
+
### Result-guard usage
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const result = await executePortableTool(tool, args, { host: "test" });
|
|
83
|
+
if (result.isError) {
|
|
84
|
+
if (isValidationFailure(result)) {
|
|
85
|
+
// result.structuredContent.validationErrors is typed.
|
|
86
|
+
log.warn({ tool: result.structuredContent.tool }, "bad args");
|
|
87
|
+
} else if (isDomainFailure(result)) {
|
|
88
|
+
// Handler-emitted failure; result.structuredContent is whatever the
|
|
89
|
+
// handler chose. No synthesized `kind` discriminator.
|
|
90
|
+
metrics.increment("tool.domain_error", { tool: tool.name });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
71
94
|
|
|
72
95
|
## Custom host typing
|
|
73
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feniix/bridgekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -48,10 +48,9 @@
|
|
|
48
48
|
"build": "npm run clean && tsc -b tsconfig.json",
|
|
49
49
|
"test": "npm run build && node scripts/run-built-tests.mjs",
|
|
50
50
|
"test:coverage": "npm run build && node scripts/run-built-tests-coverage.mjs",
|
|
51
|
-
"verify:dist": "node scripts/verify-bridgekit-dist.mjs",
|
|
52
51
|
"pack:dry-run": "npm pack --dry-run --json",
|
|
53
52
|
"package-smoke": "node scripts/smoke-package.mjs",
|
|
54
|
-
"prepack": "npm run build
|
|
53
|
+
"prepack": "npm run build"
|
|
55
54
|
},
|
|
56
55
|
"files": [
|
|
57
56
|
"dist/**/*.js",
|
|
@@ -61,6 +60,7 @@
|
|
|
61
60
|
"!dist/**/*.map",
|
|
62
61
|
"!dist/tsconfig.tsbuildinfo",
|
|
63
62
|
"README.md",
|
|
63
|
+
"CHANGELOG.md",
|
|
64
64
|
"llms.txt",
|
|
65
65
|
"examples/README.md"
|
|
66
66
|
],
|