@smooai/smooth-extension-sdk 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smooai/smooth-extension-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript SDK for building Smooth Extension Protocol (SEP) extensions: `defineExtension`, `defineTool`, a stdio JSON-RPC transport, an in-process test host, and a conformance runner. Extensions are subprocesses speaking JSON-RPC 2.0 ndjson to any SEP host (smooth-operator-core and its polyglot servers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/extension.ts CHANGED
@@ -12,10 +12,14 @@
12
12
  */
13
13
  import { Peer } from './jsonrpc.js';
14
14
  import { PROTOCOL_VERSION, method } from './protocol.js';
15
- import type { Context, EventParams, InitializeParams, InitializeResult, ToolExecuteParams, ToolExecuteResult, ToolUpdateParams } from './protocol.js';
15
+ import type { Context, EventParams, HookOutcome, HookParams, InitializeParams, InitializeResult, ToolExecuteParams, ToolExecuteResult, ToolUpdateParams } from './protocol.js';
16
16
  import { toJsonSchema, type ParameterSchema } from './schema.js';
17
17
  import { stdioTransport, type Transport } from './transport.js';
18
18
 
19
+ /** The intercept hooks (awaited, host-orchestrated); every other `on(...)` name
20
+ * is a fire-and-forget observe event. Kept in sync with the engine's HookType. */
21
+ const HOOK_NAMES = new Set(['tool_call', 'tool_result', 'before_agent_start', 'message_end', 'context', 'before_provider_request', 'input', 'user_bash']);
22
+
19
23
  /** Progress + cancellation handed to a tool while it runs. */
20
24
  export interface ToolContext {
21
25
  /** Correlates `onUpdate` calls with this execution. */
@@ -44,8 +48,14 @@ export function defineTool<TArgs = Record<string, unknown>>(def: ToolDef<TArgs>)
44
48
  return def;
45
49
  }
46
50
 
47
- /** Handler for an observe `event`. Fire-and-forget; return value ignored. */
48
- export type EventHandler = (payload: Record<string, unknown> | undefined, ctx: Context) => void | Promise<void>;
51
+ /** A hook handler's friendly return: veto the operation, or replace its input
52
+ * with a patch (shallow-merged onto the input). Returning nothing = continue. */
53
+ export type HookResult = { block: true; reason?: string } | { patch: Record<string, unknown> };
54
+
55
+ /** Handler for an `on(name, ...)` registration. For an observe event the return
56
+ * is ignored; for an intercept hook (see {@link HOOK_NAMES}) return a
57
+ * {@link HookResult} to veto or patch. Mirrors pi's single `on`. */
58
+ export type EventHandler = (data: Record<string, unknown> | undefined, ctx: Context) => void | HookResult | Promise<void | HookResult>;
49
59
 
50
60
  /** The builder passed to `defineExtension`'s setup. Mirrors pi's `ExtensionAPI`. */
51
61
  export interface SmoothApi {
@@ -114,6 +124,7 @@ export class Extension {
114
124
  return {};
115
125
  });
116
126
  peer.setRequestHandler(method.TOOL_EXECUTE, (params, signal) => this.executeTool(params as ToolExecuteParams, peer, signal));
127
+ peer.setRequestHandler(method.HOOK, (params) => this.dispatchHook(params as HookParams));
117
128
  peer.setNotificationHandler(method.EVENT, (params) => this.dispatchEvent(params as EventParams));
118
129
 
119
130
  transport.start((frame) => peer.receive(frame));
@@ -141,10 +152,13 @@ export class Extension {
141
152
  parameters: toJsonSchema(t.parameters),
142
153
  ...(t.deferred ? { deferred: true } : {}),
143
154
  }));
155
+ // Only observe events go in `subscriptions` — hook names are intercepts
156
+ // the host always calls, not events it filters by subscription.
157
+ const subscriptions = [...this.events.keys()].filter((name) => !HOOK_NAMES.has(name));
144
158
  return {
145
159
  protocol_version: PROTOCOL_VERSION,
146
160
  extension: { name: this.name, version: this.version },
147
- registrations: { tools, subscriptions: [...this.events.keys()] },
161
+ registrations: { tools, subscriptions },
148
162
  };
149
163
  }
150
164
 
@@ -166,6 +180,23 @@ export class Extension {
166
180
  void handler(params.payload, params.context);
167
181
  }
168
182
  }
183
+
184
+ /** Fold this extension's handlers for one `hook` into a single outcome: the
185
+ * first `block` short-circuits; `patch`es shallow-merge onto the input and
186
+ * thread to the next handler; no result = continue. The host chains the
187
+ * outcome across extensions in load order. */
188
+ private async dispatchHook(params: HookParams): Promise<HookOutcome> {
189
+ let input = params.input;
190
+ let modified = false;
191
+ for (const handler of this.events.get(params.hook) ?? []) {
192
+ const result = await handler(input, params.context);
193
+ if (!result) continue;
194
+ if ('block' in result) return { action: 'block', ...(result.reason ? { reason: result.reason } : {}) };
195
+ input = { ...input, ...result.patch };
196
+ modified = true;
197
+ }
198
+ return modified ? { action: 'modify', patch: input } : { action: 'continue' };
199
+ }
169
200
  }
170
201
 
171
202
  /** Define an extension. Set `smooth.name`/`smooth.version` and register tools. */
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * `runConformance`.
10
10
  */
11
11
  export { defineExtension, defineTool, Extension } from './extension.js';
12
- export type { ExtensionSetup, SmoothApi, ToolDef, ToolContext, ToolReturn, EventHandler, ConnectHandle } from './extension.js';
12
+ export type { ExtensionSetup, SmoothApi, ToolDef, ToolContext, ToolReturn, EventHandler, HookResult, ConnectHandle } from './extension.js';
13
13
  export { createTestHost } from './test-host.js';
14
14
  export type { TestHost, CallToolOptions } from './test-host.js';
15
15
  export { runConformance, DEFAULT_SPEC_DIR } from './conformance.js';
@@ -31,4 +31,6 @@ export type {
31
31
  ToolExecuteResult,
32
32
  ToolUpdateParams,
33
33
  EventParams,
34
+ HookParams,
35
+ HookOutcome,
34
36
  } from './protocol.js';
package/src/protocol.ts CHANGED
@@ -112,6 +112,21 @@ export interface ToolUpdateParams {
112
112
 
113
113
  export interface EventParams {
114
114
  event: string;
115
+ /** Per-connection monotonic sequence; absent on the `events_lost` marker. */
116
+ seq?: number;
115
117
  context: Context;
116
118
  payload?: Record<string, unknown>;
117
119
  }
120
+
121
+ /** Host → ext `hook` request: an awaited intercept the extension can veto/patch. */
122
+ export interface HookParams {
123
+ hook: string;
124
+ context: Context;
125
+ input: Record<string, unknown>;
126
+ }
127
+
128
+ /** The extension's reply to a `hook`, tagged by `action`. */
129
+ export type HookOutcome =
130
+ | { action: 'continue' }
131
+ | { action: 'block'; reason?: string }
132
+ | { action: 'modify'; patch: Record<string, unknown> };
package/src/test-host.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Peer } from './jsonrpc.js';
9
9
  import { PROTOCOL_VERSION, method } from './protocol.js';
10
- import type { Context, InitializeParams, InitializeResult, ToolExecuteResult, ToolUpdateParams } from './protocol.js';
10
+ import type { Context, HookOutcome, InitializeParams, InitializeResult, ToolExecuteResult, ToolUpdateParams } from './protocol.js';
11
11
  import type { Extension } from './extension.js';
12
12
  import { linkedPair } from './transport.js';
13
13
 
@@ -25,6 +25,8 @@ export interface CallToolOptions {
25
25
  export interface TestHost {
26
26
  initialize(overrides?: Partial<InitializeParams>): Promise<InitializeResult>;
27
27
  callTool(tool: string, args: Record<string, unknown>, opts?: CallToolOptions): Promise<ToolExecuteResult>;
28
+ /** Drive a `hook` request and get back the extension's folded outcome. */
29
+ callHook(hook: string, input: Record<string, unknown>, context?: Context): Promise<HookOutcome>;
28
30
  ping(): Promise<Record<string, unknown>>;
29
31
  sendEvent(event: string, payload?: Record<string, unknown>, context?: Context): void;
30
32
  shutdown(): Promise<void>;
@@ -74,6 +76,9 @@ export function createTestHost(extension: Extension): TestHost {
74
76
  updateSinks.delete(call_id);
75
77
  }
76
78
  },
79
+ callHook(hook, input, context) {
80
+ return host.request<HookOutcome>(method.HOOK, { hook, input, context: context ?? DEFAULT_CONTEXT });
81
+ },
77
82
  ping() {
78
83
  return host.request<Record<string, unknown>>(method.PING, {});
79
84
  },