@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 +1 -1
- package/src/extension.ts +35 -4
- package/src/index.ts +3 -1
- package/src/protocol.ts +15 -0
- package/src/test-host.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smooai/smooth-extension-sdk",
|
|
3
|
-
"version": "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
|
-
/**
|
|
48
|
-
|
|
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
|
|
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
|
},
|