@feniix/bridgekit 0.6.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 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
- Portable validation failures and portable results with `isError: true` reject with `PortableToolExecutionError` in pi so the host sees a native tool failure. The error message is the portable `text`, and `.details` is populated from `structuredContent` or `details`. Progress updates from `ctx.progress?.(...)` map to pi tool updates.
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
- MCP invalid input and portable `isError: true` results return `CallToolResult` with `isError: true`. Exporting a server-options factory keeps MCP entrypoints import-passive and easy to test without starting stdio.
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
 
@@ -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
51
  readonly details: PortableToolErrorDetails;
27
52
  constructor(result: PortableToolResult);
28
53
  }
29
54
  export declare function isPortableToolExecutionError(error: unknown): error is PortableToolExecutionError;
30
- export declare function registerPiTools(pi: PiToolRegistration, tools: readonly PortableTool<TSchema>[]): void;
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 {};
@@ -1,16 +1,13 @@
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
  }
5
- function isValidationDetails(source) {
6
- return source.kind === "validation" && typeof source.tool === "string" && Array.isArray(source.validationErrors);
7
- }
8
6
  function toPortableToolErrorDetails(result) {
9
- const source = toPiDetails(result);
10
- if (isValidationDetails(source)) {
11
- return source;
7
+ if (isValidationFailure(result)) {
8
+ return result.structuredContent;
12
9
  }
13
- const { kind: _ignored, ...rest } = source;
10
+ const { kind: _ignored, ...rest } = toPiDetails(result);
14
11
  return { kind: "domain", ...rest };
15
12
  }
16
13
  export class PortableToolExecutionError extends Error {
@@ -24,7 +21,34 @@ export class PortableToolExecutionError extends Error {
24
21
  export function isPortableToolExecutionError(error) {
25
22
  return error instanceof PortableToolExecutionError;
26
23
  }
27
- export function registerPiTools(pi, tools) {
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
+ }
28
52
  for (const tool of tools) {
29
53
  pi.registerTool({
30
54
  name: tool.name,
@@ -32,22 +56,37 @@ export function registerPiTools(pi, tools) {
32
56
  description: tool.description,
33
57
  parameters: tool.parameters,
34
58
  async execute(_toolCallId, params, signal, onUpdate, _ctx) {
35
- const result = await executePortableTool(tool, params, {
36
- host: "pi",
37
- signal,
38
- progress(update) {
39
- onUpdate?.({
40
- content: [{ type: "text", text: update.text }],
41
- details: toPiDetails(update),
42
- });
43
- },
44
- });
45
- if (result.isError) {
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") {
46
84
  throw new PortableToolExecutionError(result);
47
85
  }
48
86
  return {
49
87
  content: [{ type: "text", text: result.text }],
50
88
  details: toPiDetails(result),
89
+ isError: result.isError === true,
51
90
  };
52
91
  },
53
92
  });
@@ -1,6 +1,5 @@
1
1
  import type { TSchema } from "typebox";
2
2
  import type { PortableTool, PortableToolBuiltInHost, PortableToolContext, PortableToolResult, PortableValidationError } from "./define-tool.js";
3
- export type { PortableValidationError };
4
3
  export declare function validatePortableToolArgs<THost extends string = PortableToolBuiltInHost>(tool: PortableTool<TSchema, THost>, args: unknown): {
5
4
  ok: true;
6
5
  } | {
@@ -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
+ }
@@ -1,2 +1,3 @@
1
- export { definePortableTool, type PortableTool, type PortableToolBuiltInHost, type PortableToolContext, type PortableToolErrorDetails, type PortableToolHost, type PortableToolResult, } from "./core/define-tool.js";
2
- export { executePortableTool, type PortableValidationError, validatePortableToolArgs, } from "./core/execute-tool.js";
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, } from "./core/execute-tool.js";
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 { definePortableTool, executePortableTool } from "@feniix/bridgekit";
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 throw `PortableToolExecutionError`, because pi expects native tool failures. Tests should assert rejected execution plus `.details`, not a returned `{ isError: true }` object.
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.6.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",
@@ -60,6 +60,7 @@
60
60
  "!dist/**/*.map",
61
61
  "!dist/tsconfig.tsbuildinfo",
62
62
  "README.md",
63
+ "CHANGELOG.md",
63
64
  "llms.txt",
64
65
  "examples/README.md"
65
66
  ],