@dbx-tools/appkit 0.9.69 → 0.9.71
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/README.md +39 -18
- package/index.ts +4 -3
- package/lib/index.d.ts +4 -3
- package/lib/index.js +2 -2
- package/lib/src/appkit.d.ts +3 -3
- package/lib/src/appkit.js +2 -2
- package/lib/src/child-process.d.ts +42 -16
- package/lib/src/child-process.js +125 -73
- package/lib/src/interceptor.d.ts +3 -33
- package/lib/src/interceptor.js +2 -71
- package/lib/src/tool-provider.d.ts +10 -1
- package/lib/src/tool-provider.js +2 -2
- package/package.json +7 -7
- package/src/appkit.ts +3 -3
- package/src/child-process.ts +170 -89
- package/src/interceptor.ts +3 -97
- package/src/tool-provider.ts +16 -1
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@databricks/appkit": "0.81.0",
|
|
23
|
-
"@dbx-tools/projen": "^0.9.
|
|
23
|
+
"@dbx-tools/projen": "^0.9.71",
|
|
24
24
|
"@types/node": "^24.6.0",
|
|
25
25
|
"typescript": "^5.9.3",
|
|
26
26
|
"vitest": "3.2.4"
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@databricks/sdk-experimental": "^0.17.0",
|
|
33
|
-
"@dbx-tools/auth": "^0.9.
|
|
34
|
-
"@dbx-tools/core": "^0.9.
|
|
35
|
-
"@dbx-tools/lakebase": "^0.9.
|
|
36
|
-
"@dbx-tools/postgres": "^0.9.
|
|
37
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
+
"@dbx-tools/auth": "^0.9.71",
|
|
34
|
+
"@dbx-tools/core": "^0.9.71",
|
|
35
|
+
"@dbx-tools/lakebase": "^0.9.71",
|
|
36
|
+
"@dbx-tools/postgres": "^0.9.71",
|
|
37
|
+
"@dbx-tools/shared-core": "^0.9.71",
|
|
38
38
|
"zod": "4.3.6"
|
|
39
39
|
},
|
|
40
40
|
"main": "./lib/index.js",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"./package.json": "./package.json"
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
|
-
"version": "0.9.
|
|
118
|
+
"version": "0.9.71",
|
|
119
119
|
"types": "./lib/index.d.ts",
|
|
120
120
|
"dbxToolsConfig": {
|
|
121
121
|
"tags": [
|
package/src/appkit.ts
CHANGED
|
@@ -103,8 +103,8 @@ export type CreateAppConfig<T extends AppKitPlugins = AppKitPlugins> = Omit<
|
|
|
103
103
|
/**
|
|
104
104
|
* One or many {@link Interceptor}s handed an {@link InterceptorContext} once
|
|
105
105
|
* auto-configuration has computed the env. Each receives the resolved env, an
|
|
106
|
-
* AppKit
|
|
107
|
-
*
|
|
106
|
+
* AppKit lifecycle hook. See `./interceptor`. The tunnel is the primary
|
|
107
|
+
* consumer.
|
|
108
108
|
*/
|
|
109
109
|
interceptor?: Interceptor | Interceptor[];
|
|
110
110
|
};
|
|
@@ -293,7 +293,7 @@ function interceptorList(interceptor: CreateAppConfig["interceptor"]): Intercept
|
|
|
293
293
|
* When {@link CreateAppConfig.interceptor}s are given, each is invoked with an
|
|
294
294
|
* {@link InterceptorContext} AFTER auto-configuration computes the env and BEFORE
|
|
295
295
|
* AppKit boots - so an interceptor can read the resolved connection, register
|
|
296
|
-
* lifecycle handlers, and
|
|
296
|
+
* lifecycle handlers, and start auxiliary work. A hidden {@link lifecycleBridge}
|
|
297
297
|
* plugin is injected so those `onLifecycle` handlers fire on the genuine AppKit
|
|
298
298
|
* events; it has no exports, so the returned {@link PluginMap} is unchanged.
|
|
299
299
|
*
|
package/src/child-process.ts
CHANGED
|
@@ -4,20 +4,44 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import * as exec from "@dbx-tools/core/exec";
|
|
7
|
+
import { asyncUtils } from "@dbx-tools/shared-core";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_HEALTH_CHECK_INTERVAL_MS = 200;
|
|
10
|
+
const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 30_000;
|
|
11
|
+
const DEFAULT_RUN_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
|
|
7
12
|
|
|
8
13
|
/** Spawn arguments accepted by {@link AppKitChildProcess}. */
|
|
9
14
|
export type AppKitSpawnArgs = exec.SpawnArgs<exec.ExecOptions>;
|
|
10
15
|
|
|
11
|
-
/**
|
|
12
|
-
export interface
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
/** Context passed to one child-process readiness probe. */
|
|
17
|
+
export interface AppKitChildProcessHealthCheckContext {
|
|
18
|
+
/** Spawned process being checked. */
|
|
19
|
+
readonly process: exec.ChildProcessResult;
|
|
20
|
+
/** Aborts when shutdown begins or the readiness timeout expires. */
|
|
21
|
+
readonly signal: AbortSignal;
|
|
22
|
+
/** Zero-based readiness attempt. */
|
|
23
|
+
readonly attempt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Return `true` when the spawned process is ready to serve callers. */
|
|
27
|
+
export type AppKitChildProcessHealthCheck = (
|
|
28
|
+
context: AppKitChildProcessHealthCheckContext,
|
|
29
|
+
) => boolean | PromiseLike<boolean>;
|
|
30
|
+
|
|
31
|
+
/** Readiness and tree-shutdown policy for an AppKit child process. */
|
|
32
|
+
export interface AppKitChildProcessOptions extends exec.KillOptions {
|
|
33
|
+
/** Optional readiness probe polled after the process is spawned. */
|
|
34
|
+
healthCheck?: AppKitChildProcessHealthCheck;
|
|
35
|
+
/** Delay between readiness probes. Defaults to 200 milliseconds. */
|
|
36
|
+
healthCheckIntervalMs?: number;
|
|
37
|
+
/** Maximum time allowed for readiness. Defaults to 30 seconds. */
|
|
38
|
+
healthCheckTimeoutMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Foreground signal policy for {@link AppKitChildProcess.run}. */
|
|
42
|
+
export interface AppKitChildProcessRunOptions {
|
|
43
|
+
/** Parent signals that trigger tree shutdown before being re-raised. */
|
|
44
|
+
signals?: readonly NodeJS.Signals[];
|
|
21
45
|
}
|
|
22
46
|
|
|
23
47
|
/**
|
|
@@ -25,26 +49,36 @@ export interface AppKitChildProcessOptions {
|
|
|
25
49
|
*
|
|
26
50
|
* The spawn arguments use {@link exec.ExecOptions}, including core exec's
|
|
27
51
|
* stdin, output capture, line handlers, exit checking, and trim policy.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
52
|
+
* When configured, {@link AppKitChildProcessOptions.healthCheck} is polled
|
|
53
|
+
* until ready before {@link start} resolves. Shutdown aborts any active probe
|
|
54
|
+
* and delegates process-tree termination to {@link exec.kill}.
|
|
30
55
|
*/
|
|
31
56
|
export class AppKitChildProcess {
|
|
32
57
|
readonly #spawnArgs: AppKitSpawnArgs;
|
|
33
|
-
readonly #
|
|
34
|
-
readonly #
|
|
58
|
+
readonly #killOptions: exec.KillOptions;
|
|
59
|
+
readonly #healthCheck?: AppKitChildProcessHealthCheck;
|
|
60
|
+
readonly #healthCheckIntervalMs: number;
|
|
61
|
+
readonly #healthCheckTimeoutMs: number;
|
|
35
62
|
|
|
36
63
|
#child?: exec.ChildProcessResult;
|
|
64
|
+
#healthCheckController?: AbortController;
|
|
37
65
|
#shutdownPromise?: Promise<void>;
|
|
38
66
|
|
|
39
67
|
constructor(spawnArgs: AppKitSpawnArgs, options: AppKitChildProcessOptions = {}) {
|
|
40
68
|
this.#spawnArgs = spawnArgs;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
this.#
|
|
69
|
+
const {
|
|
70
|
+
healthCheck,
|
|
71
|
+
healthCheckIntervalMs = DEFAULT_HEALTH_CHECK_INTERVAL_MS,
|
|
72
|
+
healthCheckTimeoutMs = DEFAULT_HEALTH_CHECK_TIMEOUT_MS,
|
|
73
|
+
...killOptions
|
|
74
|
+
} = options;
|
|
75
|
+
this.#healthCheck = healthCheck;
|
|
76
|
+
this.#healthCheckIntervalMs = positiveMilliseconds(
|
|
77
|
+
healthCheckIntervalMs,
|
|
78
|
+
"healthCheckIntervalMs",
|
|
79
|
+
);
|
|
80
|
+
this.#healthCheckTimeoutMs = positiveMilliseconds(healthCheckTimeoutMs, "healthCheckTimeoutMs");
|
|
81
|
+
this.#killOptions = killOptions;
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
/** Current child handle, or `undefined` when no child is running. */
|
|
@@ -57,101 +91,148 @@ export class AppKitChildProcess {
|
|
|
57
91
|
return this.#child !== undefined && !hasExited(this.#child);
|
|
58
92
|
}
|
|
59
93
|
|
|
60
|
-
/**
|
|
61
|
-
start():
|
|
94
|
+
/** Spawn the child and resolve only after its optional readiness probe succeeds. */
|
|
95
|
+
start(): Promise<void> {
|
|
62
96
|
if (this.running) throw new Error("Child process is already running");
|
|
63
97
|
if (this.#shutdownPromise) {
|
|
64
98
|
throw new Error("Child process is shutting down or has been shut down");
|
|
65
99
|
}
|
|
66
100
|
|
|
67
101
|
const child = exec.spawn(...this.#spawnArgs);
|
|
102
|
+
const controller = new AbortController();
|
|
68
103
|
this.#child = child;
|
|
104
|
+
this.#healthCheckController = controller;
|
|
69
105
|
void child.once("close", () => {
|
|
70
106
|
if (this.#child === child) this.#child = undefined;
|
|
71
107
|
});
|
|
72
|
-
return child;
|
|
108
|
+
return this.#completeStart(child, controller);
|
|
73
109
|
}
|
|
74
110
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Start, await, and shut down one foreground child while preserving parent
|
|
113
|
+
* termination signals.
|
|
114
|
+
*/
|
|
115
|
+
async run(options: AppKitChildProcessRunOptions = {}): Promise<exec.ExecResult> {
|
|
116
|
+
const signals = options.signals ?? DEFAULT_RUN_SIGNALS;
|
|
117
|
+
if (signals.includes("SIGKILL")) {
|
|
118
|
+
throw new Error("SIGKILL cannot be intercepted by a Node process");
|
|
119
|
+
}
|
|
120
|
+
const handlers = new Map<NodeJS.Signals, () => void>();
|
|
121
|
+
let stopping = false;
|
|
122
|
+
const removeHandlers = () => {
|
|
123
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
124
|
+
};
|
|
125
|
+
for (const signal of signals) {
|
|
126
|
+
const handler = () => {
|
|
127
|
+
if (stopping) return;
|
|
128
|
+
stopping = true;
|
|
129
|
+
removeHandlers();
|
|
130
|
+
void this.shutdown()
|
|
131
|
+
.finally(() => process.kill(process.pid, signal))
|
|
132
|
+
.catch(() => undefined);
|
|
133
|
+
};
|
|
134
|
+
handlers.set(signal, handler);
|
|
135
|
+
process.once(signal, handler);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await this.start();
|
|
140
|
+
const child = this.#child;
|
|
141
|
+
if (!child) throw new Error("Child process exited after startup");
|
|
142
|
+
return await child;
|
|
143
|
+
} finally {
|
|
144
|
+
removeHandlers();
|
|
145
|
+
await this.shutdown();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Abort readiness and stop the process tree once.
|
|
151
|
+
*
|
|
152
|
+
* An explicit non-`SIGKILL` signal runs only the graceful polling phase.
|
|
153
|
+
* Explicit `SIGKILL` skips directly to forced polling. Omitting the signal
|
|
154
|
+
* uses the configured graceful and forced policy.
|
|
155
|
+
*/
|
|
156
|
+
shutdown(signal?: NodeJS.Signals): Promise<void> {
|
|
157
|
+
this.#healthCheckController?.abort(
|
|
158
|
+
new DOMException("Child process shutdown requested", "AbortError"),
|
|
159
|
+
);
|
|
160
|
+
const killOptions: exec.KillOptions =
|
|
161
|
+
signal === undefined
|
|
162
|
+
? this.#killOptions
|
|
163
|
+
: signal === "SIGKILL"
|
|
164
|
+
? { ...this.#killOptions, gracefulSignal: false, forceSignal: signal }
|
|
165
|
+
: { ...this.#killOptions, gracefulSignal: signal, forceSignal: false };
|
|
166
|
+
this.#shutdownPromise ??= this.#performShutdown(killOptions);
|
|
78
167
|
return this.#shutdownPromise;
|
|
79
168
|
}
|
|
80
169
|
|
|
81
|
-
async #
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
170
|
+
async #completeStart(child: exec.ChildProcessResult, controller: AbortController): Promise<void> {
|
|
171
|
+
try {
|
|
172
|
+
if (this.#healthCheck) {
|
|
173
|
+
await Promise.race([
|
|
174
|
+
this.#waitUntilHealthy(child, controller.signal),
|
|
175
|
+
child.then((result) => {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Child process ${child.pid ?? "<unknown>"} exited before becoming ready ` +
|
|
178
|
+
`(exit ${result.exitCode})`,
|
|
179
|
+
);
|
|
180
|
+
}),
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
try {
|
|
185
|
+
await this.shutdown();
|
|
186
|
+
} catch (shutdownError) {
|
|
187
|
+
throw new AggregateError(
|
|
188
|
+
[error, shutdownError],
|
|
189
|
+
`Child process ${child.pid ?? "<unknown>"} failed readiness and shutdown`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
} finally {
|
|
194
|
+
if (this.#healthCheckController === controller) {
|
|
195
|
+
this.#healthCheckController = undefined;
|
|
196
|
+
}
|
|
86
197
|
}
|
|
198
|
+
}
|
|
87
199
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
200
|
+
async #waitUntilHealthy(child: exec.ChildProcessResult, signal: AbortSignal): Promise<void> {
|
|
201
|
+
const healthCheck = this.#healthCheck;
|
|
202
|
+
if (!healthCheck) return;
|
|
203
|
+
for await (const healthy of asyncUtils.poll(
|
|
204
|
+
({ attempt, signal: pollSignal }) =>
|
|
205
|
+
healthCheck({ process: child, signal: pollSignal, attempt }),
|
|
206
|
+
{
|
|
207
|
+
intervalMs: this.#healthCheckIntervalMs,
|
|
208
|
+
timeoutMs: this.#healthCheckTimeoutMs,
|
|
209
|
+
signal,
|
|
210
|
+
predicate: (healthy) => !healthy,
|
|
211
|
+
},
|
|
212
|
+
)) {
|
|
213
|
+
if (healthy) return;
|
|
92
214
|
}
|
|
215
|
+
}
|
|
93
216
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
217
|
+
async #performShutdown(options: exec.KillOptions): Promise<void> {
|
|
218
|
+
const child = this.#child;
|
|
219
|
+
if (!child) {
|
|
220
|
+
this.#child = undefined;
|
|
221
|
+
return;
|
|
97
222
|
}
|
|
223
|
+
|
|
224
|
+
await exec.kill(child, options);
|
|
98
225
|
if (this.#child === child) this.#child = undefined;
|
|
99
226
|
}
|
|
100
227
|
}
|
|
101
228
|
|
|
102
|
-
function spawnOptions(args: AppKitSpawnArgs): exec.ExecOptions | undefined {
|
|
103
|
-
const value = args.at(-1);
|
|
104
|
-
return isExecOptions(value) ? value : undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function isExecOptions(value: unknown): value is exec.ExecOptions {
|
|
108
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
229
|
function hasExited(child: exec.ChildProcessResult): boolean {
|
|
112
230
|
return child.exitCode !== null || child.signalCode !== null;
|
|
113
231
|
}
|
|
114
232
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
detached: boolean,
|
|
119
|
-
): void {
|
|
120
|
-
if (detached && process.platform !== "win32" && child.pid) {
|
|
121
|
-
try {
|
|
122
|
-
process.kill(-child.pid, signal);
|
|
123
|
-
return;
|
|
124
|
-
} catch {
|
|
125
|
-
// Fall back to the direct child when the process group already exited.
|
|
126
|
-
}
|
|
233
|
+
function positiveMilliseconds(value: number, name: string): number {
|
|
234
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
235
|
+
throw new RangeError(`${name} must be a finite positive number`);
|
|
127
236
|
}
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function waitForExit(child: exec.ChildProcessResult, timeoutMs: number): Promise<boolean> {
|
|
132
|
-
if (hasExited(child)) return Promise.resolve(true);
|
|
133
|
-
|
|
134
|
-
return new Promise((resolve) => {
|
|
135
|
-
let timer: NodeJS.Timeout | undefined;
|
|
136
|
-
let settled = false;
|
|
137
|
-
|
|
138
|
-
const finish = (exited: boolean) => {
|
|
139
|
-
if (settled) return;
|
|
140
|
-
settled = true;
|
|
141
|
-
if (timer) clearTimeout(timer);
|
|
142
|
-
void child.off("exit", onExit);
|
|
143
|
-
void child.off("error", onError);
|
|
144
|
-
resolve(exited);
|
|
145
|
-
};
|
|
146
|
-
const onExit = () => finish(true);
|
|
147
|
-
const onError = () => finish(true);
|
|
148
|
-
|
|
149
|
-
void child.once("exit", onExit);
|
|
150
|
-
void child.once("error", onError);
|
|
151
|
-
if (hasExited(child)) {
|
|
152
|
-
finish(true);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
timer = setTimeout(() => finish(false), timeoutMs);
|
|
156
|
-
});
|
|
237
|
+
return value;
|
|
157
238
|
}
|
package/src/interceptor.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `createApp` INTERCEPTOR context: an in-process handle an app hands to
|
|
3
3
|
* add-ons (the tunnel, chiefly) so they can read the env auto-configuration
|
|
4
|
-
* computed
|
|
5
|
-
* unit - concurrently-style, where any death takes the whole set down.
|
|
4
|
+
* computed and hook AppKit's lifecycle.
|
|
6
5
|
*
|
|
7
6
|
* An interceptor is a plain function `(ctx) => void | Promise<void>` passed to
|
|
8
7
|
* `appkit.createApp`'s `interceptor` option ("one or many"). `appkit.createApp`
|
|
@@ -20,7 +19,6 @@
|
|
|
20
19
|
* @module
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
|
-
import type { ChildProcess } from "node:child_process";
|
|
24
22
|
import { Plugin, toPlugin, type BasePluginConfig, type PluginManifest } from "@databricks/appkit";
|
|
25
23
|
import { log } from "@dbx-tools/shared-core";
|
|
26
24
|
import type { LakebaseConnection } from "./lakebase-resolver.ts";
|
|
@@ -60,14 +58,6 @@ export interface ResolvedAppEnv {
|
|
|
60
58
|
readonly databricksHost?: string;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
/**
|
|
64
|
-
* A process {@link InterceptorContext.bindProcess} can supervise. A bare
|
|
65
|
-
* `node:child_process` `ChildProcess` (e.g. portr) satisfies this, and so does
|
|
66
|
-
* `@dbx-tools/core`'s `spawn()` result (a `ChildProcessResult` IS a
|
|
67
|
-
* `ChildProcess`) - one code path handles both.
|
|
68
|
-
*/
|
|
69
|
-
export type BindableProcess = Pick<ChildProcess, "pid" | "kill" | "killed" | "once">;
|
|
70
|
-
|
|
71
61
|
/**
|
|
72
62
|
* The handle passed to each interceptor.
|
|
73
63
|
*
|
|
@@ -77,9 +67,8 @@ export type BindableProcess = Pick<ChildProcess, "pid" | "kill" | "killed" | "on
|
|
|
77
67
|
* await appkit.createApp({
|
|
78
68
|
* plugins: [server()],
|
|
79
69
|
* interceptor: (ctx) => {
|
|
80
|
-
* const
|
|
81
|
-
* ctx.
|
|
82
|
-
* ctx.onLifecycle("shutdown", () => portr.kill("SIGTERM"));
|
|
70
|
+
* const auxiliary = startAuxiliary(ctx.env.databricksHost);
|
|
71
|
+
* ctx.onLifecycle("shutdown", () => auxiliary.stop());
|
|
83
72
|
* },
|
|
84
73
|
* });
|
|
85
74
|
*/
|
|
@@ -93,66 +82,11 @@ export interface InterceptorContext {
|
|
|
93
82
|
* retroactively (same semantics as AppKit).
|
|
94
83
|
*/
|
|
95
84
|
onLifecycle(event: LifecycleEvent, fn: LifecycleHandler): void;
|
|
96
|
-
/** Register synchronous cleanup that runs before any bound process is killed. */
|
|
97
|
-
onTeardown(fn: () => void): void;
|
|
98
|
-
/**
|
|
99
|
-
* Broadcast a termination signal from the main app to every bound process.
|
|
100
|
-
* Called automatically when this process receives `SIGINT`/`SIGTERM`/`SIGHUP`;
|
|
101
|
-
* exposed so an interceptor can trigger teardown itself.
|
|
102
|
-
*/
|
|
103
|
-
broadcastSignal(signal: NodeJS.Signals): void;
|
|
104
|
-
/**
|
|
105
|
-
* Supervise a child process alongside the app, concurrently-style: signals pass
|
|
106
|
-
* through, and if EITHER the child or the app dies the whole set comes down.
|
|
107
|
-
* Safe to call for several children; teardown is idempotent.
|
|
108
|
-
*/
|
|
109
|
-
bindProcess(child: BindableProcess): void;
|
|
110
85
|
}
|
|
111
86
|
|
|
112
87
|
/** A single interceptor, or several. The `createApp` `interceptor?:` option. */
|
|
113
88
|
export type Interceptor = (ctx: InterceptorContext) => void | Promise<void>;
|
|
114
89
|
|
|
115
|
-
/** How long bound children get to exit on `SIGTERM` before the app force-exits. */
|
|
116
|
-
const TEARDOWN_GRACE_MS = 10_000;
|
|
117
|
-
|
|
118
|
-
/** The signals that trigger teardown when the MAIN process receives them. */
|
|
119
|
-
const TEARDOWN_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
|
|
120
|
-
const boundChildren = new Set<BindableProcess>();
|
|
121
|
-
const teardownHandlers = new Set<() => void>();
|
|
122
|
-
let processShuttingDown = false;
|
|
123
|
-
let processSignalsBound = false;
|
|
124
|
-
|
|
125
|
-
function teardownProcess(code: number): void {
|
|
126
|
-
if (processShuttingDown) return;
|
|
127
|
-
processShuttingDown = true;
|
|
128
|
-
for (const handler of teardownHandlers) handler();
|
|
129
|
-
teardownHandlers.clear();
|
|
130
|
-
const children = [...boundChildren];
|
|
131
|
-
for (const child of children) {
|
|
132
|
-
if (!child.killed) child.kill("SIGTERM");
|
|
133
|
-
}
|
|
134
|
-
boundChildren.clear();
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
for (const child of children) child.kill("SIGKILL");
|
|
137
|
-
process.exit(code);
|
|
138
|
-
}, TEARDOWN_GRACE_MS).unref();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function broadcastProcessSignal(signal: NodeJS.Signals): void {
|
|
142
|
-
for (const child of boundChildren) {
|
|
143
|
-
if (!child.killed) child.kill(signal);
|
|
144
|
-
}
|
|
145
|
-
teardownProcess(0);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function bindProcessSignals(): void {
|
|
149
|
-
if (processSignalsBound) return;
|
|
150
|
-
processSignalsBound = true;
|
|
151
|
-
for (const signal of TEARDOWN_SIGNALS) {
|
|
152
|
-
process.on(signal, () => broadcastProcessSignal(signal));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
90
|
/**
|
|
157
91
|
* The mutable machinery behind an {@link InterceptorContext}. Split out so
|
|
158
92
|
* {@link createInterceptorContext} can hand the public context to interceptors
|
|
@@ -167,31 +101,9 @@ export interface InterceptorRuntime {
|
|
|
167
101
|
|
|
168
102
|
/**
|
|
169
103
|
* Build an {@link InterceptorContext} + its {@link InterceptorRuntime}.
|
|
170
|
-
*
|
|
171
|
-
* Teardown is the generalized `superviseExit`: the first child exit or main-process
|
|
172
|
-
* termination signal flips a one-shot guard, `SIGTERM`s the other bound children,
|
|
173
|
-
* then `process.exit`s after {@link TEARDOWN_GRACE_MS} (an `unref`'d timer, so it
|
|
174
|
-
* never itself holds the loop open). The process-signal listeners are installed
|
|
175
|
-
* lazily on the first `bindProcess` so an app that binds nothing is untouched.
|
|
176
104
|
*/
|
|
177
105
|
export function createInterceptorContext(env: ResolvedAppEnv): InterceptorRuntime {
|
|
178
106
|
const handlers = new Map<LifecycleEvent, LifecycleHandler[]>();
|
|
179
|
-
if (processShuttingDown && boundChildren.size === 0) processShuttingDown = false;
|
|
180
|
-
if (
|
|
181
|
-
processSignalsBound &&
|
|
182
|
-
TEARDOWN_SIGNALS.every((signal) => process.listenerCount(signal) === 0)
|
|
183
|
-
) {
|
|
184
|
-
processSignalsBound = false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const bindProcess = (child: BindableProcess): void => {
|
|
188
|
-
bindProcessSignals();
|
|
189
|
-
boundChildren.add(child);
|
|
190
|
-
child.once("exit", (code) => {
|
|
191
|
-
logger.warn("bound process exited; tearing down the app", { code });
|
|
192
|
-
teardownProcess(typeof code === "number" ? code : 1);
|
|
193
|
-
});
|
|
194
|
-
};
|
|
195
107
|
|
|
196
108
|
const onLifecycle = (event: LifecycleEvent, fn: LifecycleHandler): void => {
|
|
197
109
|
const list = handlers.get(event);
|
|
@@ -216,12 +128,6 @@ export function createInterceptorContext(env: ResolvedAppEnv): InterceptorRuntim
|
|
|
216
128
|
const context: InterceptorContext = {
|
|
217
129
|
env,
|
|
218
130
|
onLifecycle,
|
|
219
|
-
onTeardown: (fn) => {
|
|
220
|
-
bindProcessSignals();
|
|
221
|
-
teardownHandlers.add(fn);
|
|
222
|
-
},
|
|
223
|
-
broadcastSignal: broadcastProcessSignal,
|
|
224
|
-
bindProcess,
|
|
225
131
|
};
|
|
226
132
|
|
|
227
133
|
return { context, emitLifecycle };
|
package/src/tool-provider.ts
CHANGED
|
@@ -9,6 +9,16 @@ import {
|
|
|
9
9
|
type ToolRegistry,
|
|
10
10
|
} from "@databricks/appkit/beta";
|
|
11
11
|
|
|
12
|
+
/** Optional request state supplied by an agent host when dispatching a plugin tool. */
|
|
13
|
+
export interface AgentToolExecutionContext {
|
|
14
|
+
/** Resource identity associated with the current agent turn. */
|
|
15
|
+
readonly resourceId?: string;
|
|
16
|
+
/** Model endpoint selected for the current agent turn. */
|
|
17
|
+
readonly model?: string;
|
|
18
|
+
/** Best-effort progress sink validated by the hosting integration. */
|
|
19
|
+
readonly writeProgress?: (event: unknown) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* Implements AppKit's `ToolProvider` contract from one native tool registry.
|
|
14
24
|
*
|
|
@@ -28,7 +38,12 @@ export abstract class ToolRegistryPlugin<TConfig extends BasePluginConfig = Base
|
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
/** Validate and dispatch one AppKit agent tool call. */
|
|
31
|
-
executeAgentTool(
|
|
41
|
+
executeAgentTool(
|
|
42
|
+
name: string,
|
|
43
|
+
args: unknown,
|
|
44
|
+
signal?: AbortSignal,
|
|
45
|
+
_context?: AgentToolExecutionContext,
|
|
46
|
+
): Promise<unknown> {
|
|
32
47
|
return executeFromRegistry(this.toolRegistry, name, args, signal);
|
|
33
48
|
}
|
|
34
49
|
}
|