@vellumai/assistant 0.10.2-dev.202606251104.36cd100 → 0.10.2-dev.202606251257.2eba8a4
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/__tests__/plugin-bootstrap.test.ts +5 -5
- package/src/__tests__/plugin-route-contribution.test.ts +2 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +2 -2
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/daemon/external-plugins-bootstrap.ts +5 -5
- package/src/hooks/hook-loader.ts +13 -13
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +9 -9
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +2 -2
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +2 -2
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
- package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +2 -2
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
- package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
- package/src/plugins/external-plugin-loader.ts +2 -2
- package/src/plugins/mtime-cache.ts +5 -8
- package/src/plugins/pipeline.ts +2 -2
- package/src/plugins/registry.ts +5 -5
- package/src/plugins/types.ts +7 -7
- package/src/tools/types.ts +114 -23
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for plugin bootstrap (PR 14).
|
|
3
3
|
*
|
|
4
4
|
* Covers:
|
|
5
|
-
* - A noop `init()` fires with a valid `
|
|
5
|
+
* - A noop `init()` fires with a valid `InitContext` that exposes every
|
|
6
6
|
* documented field.
|
|
7
7
|
* - Version-mismatch registration fails with an error that names the plugin
|
|
8
8
|
* (the registry enforces this at `registerPlugin` time, so bootstrap never
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
registerPlugin,
|
|
30
30
|
resetPluginRegistryForTests,
|
|
31
31
|
} from "../plugins/registry.js";
|
|
32
|
-
import { type
|
|
32
|
+
import { type InitContext, type Plugin } from "../plugins/types.js";
|
|
33
33
|
import { APP_VERSION } from "../version.js";
|
|
34
34
|
import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
|
|
35
35
|
|
|
@@ -51,7 +51,7 @@ function buildPlugin(
|
|
|
51
51
|
name: string,
|
|
52
52
|
extras: Partial<Omit<Plugin, "manifest" | "hooks">> & {
|
|
53
53
|
hooks?: Plugin["hooks"];
|
|
54
|
-
init?: (ctx:
|
|
54
|
+
init?: (ctx: InitContext) => Promise<void>;
|
|
55
55
|
onShutdown?: () => Promise<void>;
|
|
56
56
|
} = {},
|
|
57
57
|
options: {
|
|
@@ -98,8 +98,8 @@ describe("plugin bootstrap", () => {
|
|
|
98
98
|
await rm(TEST_WORKSPACE_DIR, { recursive: true, force: true });
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
test("noop plugin: init fires with a fully-populated
|
|
102
|
-
let received:
|
|
101
|
+
test("noop plugin: init fires with a fully-populated InitContext", async () => {
|
|
102
|
+
let received: InitContext | undefined;
|
|
103
103
|
const plugin: Plugin = buildPlugin("alpha", {
|
|
104
104
|
async init(ctx) {
|
|
105
105
|
received = ctx;
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
registerPlugin,
|
|
39
39
|
resetPluginRegistryForTests,
|
|
40
40
|
} from "../plugins/registry.js";
|
|
41
|
-
import type {
|
|
41
|
+
import type { InitContext, Plugin } from "../plugins/types.js";
|
|
42
42
|
import {
|
|
43
43
|
matchSkillRoute,
|
|
44
44
|
resetSkillRoutesForTests,
|
|
@@ -64,7 +64,7 @@ function buildPlugin(
|
|
|
64
64
|
name: string,
|
|
65
65
|
extras: Partial<Omit<Plugin, "manifest" | "hooks">> & {
|
|
66
66
|
hooks?: Plugin["hooks"];
|
|
67
|
-
init?: (ctx:
|
|
67
|
+
init?: (ctx: InitContext) => Promise<void>;
|
|
68
68
|
onShutdown?: () => Promise<void>;
|
|
69
69
|
} = {},
|
|
70
70
|
): Plugin {
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
registerPlugin,
|
|
34
34
|
resetPluginRegistryForTests,
|
|
35
35
|
} from "../plugins/registry.js";
|
|
36
|
-
import type {
|
|
36
|
+
import type { InitContext, Plugin } from "../plugins/types.js";
|
|
37
37
|
import {
|
|
38
38
|
__clearRegistryForTesting,
|
|
39
39
|
__resetRegistryForTesting,
|
|
@@ -82,7 +82,7 @@ function buildPlugin(
|
|
|
82
82
|
name: string,
|
|
83
83
|
extras: Partial<Omit<Plugin, "manifest" | "hooks">> & {
|
|
84
84
|
hooks?: Plugin["hooks"];
|
|
85
|
-
init?: (ctx:
|
|
85
|
+
init?: (ctx: InitContext) => Promise<void>;
|
|
86
86
|
onShutdown?: () => Promise<void>;
|
|
87
87
|
} = {},
|
|
88
88
|
): Plugin {
|
|
@@ -13,9 +13,9 @@ import { describe, expect, test } from "bun:test";
|
|
|
13
13
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
14
14
|
import { RiskLevel } from "../permissions/types.js";
|
|
15
15
|
import {
|
|
16
|
+
type InitContext,
|
|
16
17
|
type Plugin,
|
|
17
18
|
PluginExecutionError,
|
|
18
|
-
type PluginInitContext,
|
|
19
19
|
type PluginManifest,
|
|
20
20
|
type TurnContext,
|
|
21
21
|
} from "../plugins/types.js";
|
|
@@ -57,7 +57,7 @@ describe("plugin core types", () => {
|
|
|
57
57
|
const plugin = {
|
|
58
58
|
manifest,
|
|
59
59
|
hooks: {
|
|
60
|
-
async init(ctx:
|
|
60
|
+
async init(ctx: InitContext) {
|
|
61
61
|
// Touch every field so refactors that rename any of them break here.
|
|
62
62
|
void ctx.config;
|
|
63
63
|
void ctx.logger;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* (Zod schemas with `.parse()` are supported; anything else is passed
|
|
25
25
|
* through untouched).
|
|
26
26
|
* 5. Creates `<workspaceDir>/plugins-data/<plugin>/` on demand for per-plugin
|
|
27
|
-
* writable state and exposes it via {@link
|
|
27
|
+
* writable state and exposes it via {@link InitContext.pluginStorageDir}.
|
|
28
28
|
* 6. For each surviving plugin, registers its contributed tools and routes
|
|
29
29
|
* into their global registries via {@link registerPluginTools} and
|
|
30
30
|
* {@link registerSkillRoute}. Contributions land BEFORE `init()` so
|
|
@@ -64,7 +64,7 @@ import { getRegisteredPlugins, unregisterPlugin } from "../plugins/registry.js";
|
|
|
64
64
|
import {
|
|
65
65
|
type Plugin,
|
|
66
66
|
PluginExecutionError,
|
|
67
|
-
type
|
|
67
|
+
type ShutdownContext,
|
|
68
68
|
} from "../plugins/types.js";
|
|
69
69
|
import { loadUserPlugins } from "../plugins/user-loader.js";
|
|
70
70
|
import {
|
|
@@ -222,8 +222,8 @@ export async function bootstrapPlugins(): Promise<void> {
|
|
|
222
222
|
// Shutdown context is identical for every plugin in this boot — construct
|
|
223
223
|
// once and reuse across the per-plugin teardown and the normal shutdown
|
|
224
224
|
// hook below. Only `assistantVersion` is exposed today; future additions
|
|
225
|
-
// live on {@link
|
|
226
|
-
const shutdownContext:
|
|
225
|
+
// live on {@link ShutdownContext}.
|
|
226
|
+
const shutdownContext: ShutdownContext = {
|
|
227
227
|
assistantVersion: APP_VERSION,
|
|
228
228
|
};
|
|
229
229
|
|
|
@@ -407,7 +407,7 @@ async function initializePlugin(
|
|
|
407
407
|
async function teardownPlugin(
|
|
408
408
|
active: ActivePlugin,
|
|
409
409
|
reason: string,
|
|
410
|
-
shutdownContext:
|
|
410
|
+
shutdownContext: ShutdownContext,
|
|
411
411
|
): Promise<void> {
|
|
412
412
|
const { plugin, routeHandles } = active;
|
|
413
413
|
const name = plugin.manifest.name;
|
package/src/hooks/hook-loader.ts
CHANGED
|
@@ -25,9 +25,9 @@ import { join } from "node:path";
|
|
|
25
25
|
import { getConfig } from "../config/loader.js";
|
|
26
26
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
27
27
|
import type {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
HookFunction,
|
|
29
|
+
InitContext,
|
|
30
|
+
ShutdownContext,
|
|
31
31
|
} from "../plugin-api/types.js";
|
|
32
32
|
import { listSurfaceDir } from "../plugins/external-plugin-loader.js";
|
|
33
33
|
import { getMtime, importWithTimeout } from "../plugins/surface-import.js";
|
|
@@ -53,7 +53,7 @@ export const WORKSPACE_HOOKS_OWNER = "__workspace__";
|
|
|
53
53
|
* mtime changes, the hook is re-imported and the entry is replaced.
|
|
54
54
|
*/
|
|
55
55
|
interface CachedHook {
|
|
56
|
-
readonly hook:
|
|
56
|
+
readonly hook: HookFunction;
|
|
57
57
|
/** mtimeMs of the source file this hook was imported from. */
|
|
58
58
|
readonly sourceMtime: number;
|
|
59
59
|
}
|
|
@@ -83,7 +83,7 @@ async function resolveCachedHook<TCtx>(
|
|
|
83
83
|
ownerName: string,
|
|
84
84
|
hookName: string,
|
|
85
85
|
filePath: string,
|
|
86
|
-
): Promise<
|
|
86
|
+
): Promise<HookFunction<TCtx> | undefined> {
|
|
87
87
|
const key = hookKey(ownerName, hookName);
|
|
88
88
|
const currentMtime = getMtime(filePath);
|
|
89
89
|
|
|
@@ -94,7 +94,7 @@ async function resolveCachedHook<TCtx>(
|
|
|
94
94
|
cached.sourceMtime === currentMtime &&
|
|
95
95
|
currentMtime > 0
|
|
96
96
|
) {
|
|
97
|
-
return cached.hook as
|
|
97
|
+
return cached.hook as HookFunction<TCtx>;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Cache miss — re-import.
|
|
@@ -105,7 +105,7 @@ async function resolveCachedHook<TCtx>(
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
try {
|
|
108
|
-
const hook = await importWithTimeout<
|
|
108
|
+
const hook = await importWithTimeout<HookFunction>(filePath);
|
|
109
109
|
if (hook === undefined || typeof hook !== "function") {
|
|
110
110
|
log.error(
|
|
111
111
|
{ plugin: ownerName, hook: hookName, path: filePath },
|
|
@@ -114,7 +114,7 @@ async function resolveCachedHook<TCtx>(
|
|
|
114
114
|
return undefined;
|
|
115
115
|
}
|
|
116
116
|
hookCache.set(key, { hook, sourceMtime: currentMtime });
|
|
117
|
-
return hook as
|
|
117
|
+
return hook as HookFunction<TCtx>;
|
|
118
118
|
} catch (err) {
|
|
119
119
|
log.error(
|
|
120
120
|
{ err, plugin: ownerName, hook: hookName, path: filePath },
|
|
@@ -141,8 +141,8 @@ async function resolveCachedHook<TCtx>(
|
|
|
141
141
|
export async function collectUserHooks<TCtx = unknown>(
|
|
142
142
|
hookName: string,
|
|
143
143
|
pluginDirs: Iterable<readonly [string, string]>,
|
|
144
|
-
): Promise<
|
|
145
|
-
const out:
|
|
144
|
+
): Promise<HookFunction<TCtx>[]> {
|
|
145
|
+
const out: HookFunction<TCtx>[] = [];
|
|
146
146
|
|
|
147
147
|
for (const [pluginDir, pluginName] of pluginDirs) {
|
|
148
148
|
const hookFile = listSurfaceDir(join(pluginDir, "hooks")).find(
|
|
@@ -191,7 +191,7 @@ export async function preImportHooksDir(
|
|
|
191
191
|
if (currentMtime === 0) continue;
|
|
192
192
|
|
|
193
193
|
try {
|
|
194
|
-
const hook = await importWithTimeout<
|
|
194
|
+
const hook = await importWithTimeout<HookFunction>(file.path);
|
|
195
195
|
if (hook !== undefined && typeof hook === "function") {
|
|
196
196
|
hookCache.set(key, { hook, sourceMtime: currentMtime });
|
|
197
197
|
}
|
|
@@ -233,7 +233,7 @@ export async function runInitHook(ownerName: string): Promise<void> {
|
|
|
233
233
|
if (initHookEntry === undefined) return;
|
|
234
234
|
|
|
235
235
|
try {
|
|
236
|
-
const initContext:
|
|
236
|
+
const initContext: InitContext = {
|
|
237
237
|
config: getConfig().plugins?.[ownerName],
|
|
238
238
|
logger: log.child({ plugin: ownerName }),
|
|
239
239
|
pluginStorageDir: ensureHookStorageDir(ownerName),
|
|
@@ -256,7 +256,7 @@ export async function runInitHook(ownerName: string): Promise<void> {
|
|
|
256
256
|
*/
|
|
257
257
|
export async function runShutdownHook(
|
|
258
258
|
ownerName: string,
|
|
259
|
-
context:
|
|
259
|
+
context: ShutdownContext,
|
|
260
260
|
reason: string,
|
|
261
261
|
): Promise<void> {
|
|
262
262
|
const shutdownHookEntry = hookCache.get(hookKey(ownerName, HOOKS.SHUTDOWN));
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
* (optionally overriding the profile) and run inference through the
|
|
38
38
|
* workspace's configured profiles and credentials — no plugin-supplied API key
|
|
39
39
|
*
|
|
40
|
-
* - {@link
|
|
41
|
-
* - {@link
|
|
40
|
+
* - {@link InitContext} — passed to `init` hook at bootstrap
|
|
41
|
+
* - {@link ShutdownContext} — passed to `shutdown` hook at teardown
|
|
42
42
|
* - {@link UserPromptSubmitContext} — passed to `user-prompt-submit` hook,
|
|
43
43
|
* fired immediately before the agent loop receives a user's prompt
|
|
44
44
|
* - {@link PostCompactContext} — passed to `post-compact` hook, fired after
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* - {@link PostModelCallContext} — passed to `post-model-call` hook, fired at
|
|
56
56
|
* every model-call outcome (a finalized reply or a provider rejection) to
|
|
57
57
|
* transform content and decide whether to retry
|
|
58
|
-
* - {@link
|
|
58
|
+
* - {@link HookFunction} — signature every lifecycle hook implements
|
|
59
59
|
* - {@link PluginLogger} — pino-compatible logger shape on the contexts
|
|
60
60
|
* - {@link ToolDefinition} — author-facing tool spec (default-export shape
|
|
61
61
|
* for both plugin tool files and workspace tool files)
|
|
@@ -98,16 +98,16 @@ export type {
|
|
|
98
98
|
export type { LLMCallSite } from "../config/schemas/llm.js";
|
|
99
99
|
export type {
|
|
100
100
|
AgentLoopExitReason,
|
|
101
|
+
HookFunction,
|
|
102
|
+
InitContext,
|
|
101
103
|
ModelProfileInfo,
|
|
102
|
-
PluginHookFn,
|
|
103
|
-
PluginInitContext,
|
|
104
104
|
PluginLogger,
|
|
105
|
-
PluginShutdownContext,
|
|
106
105
|
PostCompactContext,
|
|
107
106
|
PostModelCallContext,
|
|
108
107
|
PostModelCallDecision,
|
|
109
108
|
PostToolUseContext,
|
|
110
109
|
PreModelCallContext,
|
|
110
|
+
ShutdownContext,
|
|
111
111
|
StopContext,
|
|
112
112
|
ToolContext,
|
|
113
113
|
ToolDefinition,
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -57,15 +57,15 @@ export interface PluginLogger {
|
|
|
57
57
|
* empty model that with `| null`, not `| undefined`.
|
|
58
58
|
*
|
|
59
59
|
* Each known hook key has a documented context shape:
|
|
60
|
-
* - `init` — {@link
|
|
61
|
-
* - `shutdown` — {@link
|
|
60
|
+
* - `init` — {@link InitContext}
|
|
61
|
+
* - `shutdown` — {@link ShutdownContext}
|
|
62
62
|
* - `user-prompt-submit` — {@link UserPromptSubmitContext}
|
|
63
63
|
* - `pre-model-call` — {@link PreModelCallContext}
|
|
64
64
|
* - `post-tool-use` — {@link PostToolUseContext}
|
|
65
65
|
* - `stop` — {@link StopContext}
|
|
66
66
|
* - `post-model-call` — {@link PostModelCallContext}
|
|
67
67
|
*/
|
|
68
|
-
export type
|
|
68
|
+
export type HookFunction<TCtx = unknown> = (
|
|
69
69
|
ctx: TCtx,
|
|
70
70
|
) => Promise<Partial<TCtx> | void>;
|
|
71
71
|
|
|
@@ -76,7 +76,7 @@ export type PluginHookFn<TCtx = unknown> = (
|
|
|
76
76
|
* config, a pino-compatible logger scoped to the plugin, a per-plugin
|
|
77
77
|
* writable data directory, and the assistant's version metadata.
|
|
78
78
|
*/
|
|
79
|
-
export interface
|
|
79
|
+
export interface InitContext {
|
|
80
80
|
/** Parsed config for this plugin (may be `unknown` until the manifest validates). */
|
|
81
81
|
config: unknown;
|
|
82
82
|
/** Pino-compatible child logger bound to `{ plugin: <name> }`. */
|
|
@@ -125,7 +125,7 @@ export interface ModelProfileInfo {
|
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
127
|
* Context passed to the `shutdown` hook during daemon teardown. Kept
|
|
128
|
-
* intentionally narrower than {@link
|
|
128
|
+
* intentionally narrower than {@link InitContext} — most teardown
|
|
129
129
|
* paths only need to know which assistant version they're shutting
|
|
130
130
|
* down against (e.g. for version-conditional cleanup of state files
|
|
131
131
|
* written by a previous boot).
|
|
@@ -135,7 +135,7 @@ export interface ModelProfileInfo {
|
|
|
135
135
|
* stash a version stamp at init can compare against the same name on
|
|
136
136
|
* tear-down without keeping their own copy.
|
|
137
137
|
*/
|
|
138
|
-
export interface
|
|
138
|
+
export interface ShutdownContext {
|
|
139
139
|
/** Assistant semver for compatibility checks inside the plugin. */
|
|
140
140
|
assistantVersion: string;
|
|
141
141
|
}
|
|
@@ -151,7 +151,7 @@ export interface PluginShutdownContext {
|
|
|
151
151
|
*
|
|
152
152
|
* The hook may transform `latestMessages` either by mutating it in place
|
|
153
153
|
* (`push` / `splice` / `length = 0`) or by returning a new context with
|
|
154
|
-
* a fresh `latestMessages` array — see {@link
|
|
154
|
+
* a fresh `latestMessages` array — see {@link HookFunction}'s polymorphic
|
|
155
155
|
* return shape. The daemon threads the final `latestMessages` value into
|
|
156
156
|
* `agentLoop.run()` as the run-messages argument.
|
|
157
157
|
*
|
|
@@ -244,7 +244,7 @@ export interface UserPromptSubmitContext {
|
|
|
244
244
|
* their own injected context the same way.
|
|
245
245
|
*
|
|
246
246
|
* The hook re-injects by mutating `history` in place (or returning a new
|
|
247
|
-
* context with a replacement `history`) — see {@link
|
|
247
|
+
* context with a replacement `history`) — see {@link HookFunction}'s
|
|
248
248
|
* polymorphic return shape. The agent loop reads the settled `history` back off
|
|
249
249
|
* the context and resumes the turn from it. Multiple plugins' hooks chain in
|
|
250
250
|
* registration order, each seeing the previous plugin's edits.
|
|
@@ -301,7 +301,7 @@ export interface PostCompactContext {
|
|
|
301
301
|
*
|
|
302
302
|
* The hook may transform the result either by mutating `toolResponse` in
|
|
303
303
|
* place (e.g. reassigning `toolResponse.content`) or by returning a new
|
|
304
|
-
* context with a fresh `toolResponse` — see {@link
|
|
304
|
+
* context with a fresh `toolResponse` — see {@link HookFunction}'s
|
|
305
305
|
* polymorphic return shape. The daemon threads the final `toolResponse`
|
|
306
306
|
* into the provider-bound history.
|
|
307
307
|
*
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* advisor's own `inference`-call-site sub-call — are ignored.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
|
|
13
13
|
|
|
14
14
|
import { recordMessages } from "../advisor-state-store.js";
|
|
15
15
|
|
|
16
|
-
const postModelCall:
|
|
16
|
+
const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
|
|
17
17
|
if (ctx.callSite !== "mainAgent") return;
|
|
18
18
|
if (ctx.error) return;
|
|
19
19
|
// `ctx.messages` is the pre-reply history; the turn the model just produced —
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* tool. Idempotent via the steering marker.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type { HookFunction, PreModelCallContext } from "@vellumai/plugin-api";
|
|
9
9
|
|
|
10
10
|
import { advisorEnabledForProfile } from "../advisor-gate.js";
|
|
11
11
|
import { recordSystemPrompt } from "../advisor-state-store.js";
|
|
12
12
|
import { ADVISOR_CONFIG } from "../config.js";
|
|
13
13
|
import { appendSteering, stripSteering } from "../steering.js";
|
|
14
14
|
|
|
15
|
-
const preModelCall:
|
|
15
|
+
const preModelCall: HookFunction<PreModelCallContext> = async (ctx) => {
|
|
16
16
|
if (ctx.callSite !== "mainAgent") return;
|
|
17
17
|
|
|
18
18
|
recordSystemPrompt(ctx.conversationId, stripSteering(ctx.systemPrompt));
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
|
-
|
|
9
|
+
HookFunction,
|
|
10
10
|
UserPromptSubmitContext,
|
|
11
11
|
} from "@vellumai/plugin-api";
|
|
12
12
|
|
|
13
13
|
import { seedCapture } from "../advisor-state-store.js";
|
|
14
14
|
|
|
15
|
-
const userPromptSubmit:
|
|
15
|
+
const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
|
|
16
16
|
seedCapture(ctx.conversationId, ctx.latestMessages);
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
* ignores the decision — so the hook returns early for both.
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
-
import type {
|
|
49
|
+
import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
|
|
50
50
|
|
|
51
51
|
import type { ContentBlock, Message } from "../../../../providers/types.js";
|
|
52
52
|
import {
|
|
@@ -112,7 +112,7 @@ function currentCycleMessages(
|
|
|
112
112
|
return messages;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
const postModelCall:
|
|
115
|
+
const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
|
|
116
116
|
// A provider rejection carries no turn content to assess (a recovery hook
|
|
117
117
|
// owns the rejection); the sibling `stop` hook clears the mark when the turn
|
|
118
118
|
// terminates.
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* refused).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type { HookFunction, StopContext } from "@vellumai/plugin-api";
|
|
15
15
|
|
|
16
16
|
import { clearEmptyResponseNudged } from "../nudge-state-store.js";
|
|
17
17
|
|
|
18
|
-
const stop:
|
|
18
|
+
const stop: HookFunction<StopContext> = async (ctx) => {
|
|
19
19
|
clearEmptyResponseNudged(ctx.conversationId);
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* per-tool-result hot path.
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
-
import type {
|
|
58
|
+
import type { HookFunction, PostToolUseContext } from "@vellumai/plugin-api";
|
|
59
59
|
|
|
60
60
|
import type { Message } from "../../../../providers/types.js";
|
|
61
61
|
|
|
@@ -233,7 +233,7 @@ function currentCallRepeatCount(
|
|
|
233
233
|
return count;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
const postToolUse:
|
|
236
|
+
const postToolUse: HookFunction<PostToolUseContext> = async (ctx) => {
|
|
237
237
|
const usesById = toolUsesById(ctx.messages);
|
|
238
238
|
const currentUse = usesById.get(ctx.toolResponse.tool_use_id);
|
|
239
239
|
if (currentUse === undefined || !EXPLORATION_TOOL_NAMES.has(currentUse.name))
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* empty-response hook; only a provider rejection is this hook's to act on.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import type {
|
|
26
|
+
import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
isOrderingRepairAttempted,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "../repair-state-store.js";
|
|
32
32
|
import { deepRepairHistory, isRepairableOrderingError } from "../terminal.js";
|
|
33
33
|
|
|
34
|
-
const postModelCall:
|
|
34
|
+
const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
|
|
35
35
|
if (
|
|
36
36
|
ctx.error &&
|
|
37
37
|
isRepairableOrderingError(ctx.error.message) &&
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* retry the loop's per-run backstop refused).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type { HookFunction, StopContext } from "@vellumai/plugin-api";
|
|
15
15
|
|
|
16
16
|
import { clearOrderingRepairAttempted } from "../repair-state-store.js";
|
|
17
17
|
|
|
18
|
-
const stop:
|
|
18
|
+
const stop: HookFunction<StopContext> = async (ctx) => {
|
|
19
19
|
clearOrderingRepairAttempted(ctx.conversationId);
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
|
-
|
|
13
|
+
HookFunction,
|
|
14
14
|
UserPromptSubmitContext,
|
|
15
15
|
} from "@vellumai/plugin-api";
|
|
16
16
|
|
|
17
17
|
import { repairHistory } from "../terminal.js";
|
|
18
18
|
|
|
19
|
-
const userPromptSubmit:
|
|
19
|
+
const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
|
|
20
20
|
const { messages, stats } = repairHistory(ctx.latestMessages);
|
|
21
21
|
ctx.latestMessages = messages;
|
|
22
22
|
if (
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
doesSupportVision,
|
|
20
|
-
type
|
|
20
|
+
type HookFunction,
|
|
21
21
|
type PostToolUseContext,
|
|
22
22
|
} from "@vellumai/plugin-api";
|
|
23
23
|
|
|
24
24
|
import { captionImageBlocks } from "../src/caption-blocks.js";
|
|
25
25
|
import { findVisionProfile } from "../src/vision-caption.js";
|
|
26
26
|
|
|
27
|
-
const postToolUse:
|
|
27
|
+
const postToolUse: HookFunction<PostToolUseContext> = async (ctx) => {
|
|
28
28
|
// Cheapest gate first: bail unless the tool actually returned an image,
|
|
29
29
|
// before touching the model catalog or resolving a vision profile.
|
|
30
30
|
const blocks = ctx.toolResponse.contentBlocks;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
|
-
type
|
|
26
|
+
type HookFunction,
|
|
27
27
|
type UserPromptSubmitContext,
|
|
28
28
|
} from "@vellumai/plugin-api";
|
|
29
29
|
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "../src/caption-blocks.js";
|
|
34
34
|
import { findVisionProfile } from "../src/vision-caption.js";
|
|
35
35
|
|
|
36
|
-
const userPromptSubmit:
|
|
36
|
+
const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
|
|
37
37
|
// If the turn's model already supports vision, nothing to do.
|
|
38
38
|
if (!needsImageFallback(ctx.modelProfileKey)) return;
|
|
39
39
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* empty-response hook; only a provider rejection is this hook's to act on.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import type {
|
|
27
|
+
import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
|
|
28
28
|
|
|
29
29
|
import { isImageDimensionsTooLargeError } from "../detect.js";
|
|
30
30
|
import {
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
recoverOversizedImages,
|
|
37
37
|
} from "../recover.js";
|
|
38
38
|
|
|
39
|
-
const postModelCall:
|
|
39
|
+
const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
|
|
40
40
|
if (
|
|
41
41
|
ctx.error &&
|
|
42
42
|
isImageDimensionsTooLargeError(ctx.error.message) &&
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* retry the loop's per-run backstop refused).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type { HookFunction, StopContext } from "@vellumai/plugin-api";
|
|
15
15
|
|
|
16
16
|
import { clearImageRecoveryAttempted } from "../image-recovery-state-store.js";
|
|
17
17
|
|
|
18
|
-
const stop:
|
|
18
|
+
const stop: HookFunction<StopContext> = async (ctx) => {
|
|
19
19
|
clearImageRecoveryAttempted(ctx.conversationId);
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* reject.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import type {
|
|
33
|
+
import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
|
|
34
34
|
|
|
35
35
|
import { isMaxTokensStopReason } from "../../../../agent/loop.js";
|
|
36
36
|
import {
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
export const MAX_TOKENS_CONTINUE_NUDGE_TEXT =
|
|
46
46
|
"<system_notice>Your previous response was cut off because it reached the maximum output length. Continue exactly where you stopped — do not repeat content you already sent and do not start over.</system_notice>";
|
|
47
47
|
|
|
48
|
-
const postModelCall:
|
|
48
|
+
const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
|
|
49
49
|
if (ctx.error) return;
|
|
50
50
|
if (!isMaxTokensStopReason(ctx.stopReason)) return;
|
|
51
51
|
if (ctx.callSite !== "mainAgent") return;
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* budget, no matter how the turn ended.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type { HookFunction, StopContext } from "@vellumai/plugin-api";
|
|
13
13
|
|
|
14
14
|
import { clearMaxTokensContinueBudget } from "../continue-state-store.js";
|
|
15
15
|
|
|
16
|
-
const stop:
|
|
16
|
+
const stop: HookFunction<StopContext> = async (ctx) => {
|
|
17
17
|
clearMaxTokensContinueBudget(ctx.conversationId);
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* {@link PostCompactContext}.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import type {
|
|
41
|
+
import type { HookFunction, PostCompactContext } from "@vellumai/plugin-api";
|
|
42
42
|
|
|
43
43
|
import { getConfig } from "../../../../config/loader.js";
|
|
44
44
|
import { findConversationOrSubagent } from "../../../../daemon/conversation-registry.js";
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
import { getLiveGraphMemory } from "../../../../memory/graph/conversation-graph-memory.js";
|
|
54
54
|
import { stripTailInjectionsForReinjection } from "../tail-reinjection-strip.js";
|
|
55
55
|
|
|
56
|
-
const postCompact:
|
|
56
|
+
const postCompact: HookFunction<PostCompactContext> = async (ctx) => {
|
|
57
57
|
const {
|
|
58
58
|
history,
|
|
59
59
|
requestId,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import type {
|
|
31
|
-
|
|
31
|
+
HookFunction,
|
|
32
32
|
UserPromptSubmitContext,
|
|
33
33
|
} from "@vellumai/plugin-api";
|
|
34
34
|
|
|
@@ -264,7 +264,7 @@ function persistInjectionBlocks(
|
|
|
264
264
|
* slower turn. Cancellation still works via `ctx.signal`, which is threaded
|
|
265
265
|
* into `prepareMemory`.
|
|
266
266
|
*/
|
|
267
|
-
const userPromptSubmitMemoryRetrieval:
|
|
267
|
+
const userPromptSubmitMemoryRetrieval: HookFunction<
|
|
268
268
|
UserPromptSubmitContext
|
|
269
269
|
> = async (ctx) => {
|
|
270
270
|
// The conversation-scoped retrieval state — graph handle, abort signal, and
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* `../injector.js` and re-apply it here. Until then this hook does nothing.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type { HookFunction } from "@vellumai/plugin-api";
|
|
11
11
|
|
|
12
|
-
const postCompact:
|
|
12
|
+
const postCompact: HookFunction = async () => {};
|
|
13
13
|
|
|
14
14
|
export default postCompact;
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
|
-
|
|
13
|
+
HookFunction,
|
|
14
14
|
UserPromptSubmitContext,
|
|
15
15
|
} from "@vellumai/plugin-api";
|
|
16
16
|
|
|
17
|
-
const userPromptSubmit:
|
|
17
|
+
const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async () => {};
|
|
18
18
|
|
|
19
19
|
export default userPromptSubmit;
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* the `post-model-call` chain — later hooks see (and may override) its decision.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
-
import type {
|
|
48
|
+
import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
|
|
49
49
|
|
|
50
50
|
import type { ContentBlock, Message } from "../../../../providers/types.js";
|
|
51
51
|
import {
|
|
@@ -246,7 +246,7 @@ function hasDanglingProgressSurface(messages: ReadonlyArray<Message>): boolean {
|
|
|
246
246
|
return false;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
const postModelCall:
|
|
249
|
+
const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
|
|
250
250
|
// A provider rejection carries no turn content to assess (a recovery hook
|
|
251
251
|
// owns the rejection).
|
|
252
252
|
if (ctx.error) return;
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* refused).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type { HookFunction, StopContext } from "@vellumai/plugin-api";
|
|
15
15
|
|
|
16
16
|
import { clearSurfaceCompletionNudged } from "../nudge-state-store.js";
|
|
17
17
|
|
|
18
|
-
const stop:
|
|
18
|
+
const stop: HookFunction<StopContext> = async (ctx) => {
|
|
19
19
|
clearSurfaceCompletionNudged(ctx.conversationId);
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* below it (a new turn restarts counting low).
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
-
import type {
|
|
48
|
+
import type { HookFunction, PostToolUseContext } from "@vellumai/plugin-api";
|
|
49
49
|
|
|
50
50
|
import type { ContentBlock, Message } from "../../../../providers/types.js";
|
|
51
51
|
import { isWeakOpenModel } from "../../../../providers/weak-open-model.js";
|
|
@@ -134,7 +134,7 @@ function scanTurn(messages: ReadonlyArray<Message>): {
|
|
|
134
134
|
return { rounds, taskProgressShown };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
const postToolUse:
|
|
137
|
+
const postToolUse: HookFunction<PostToolUseContext> = async (ctx) => {
|
|
138
138
|
if (!isWeakOpenModel(ctx.model)) return;
|
|
139
139
|
|
|
140
140
|
const { rounds, taskProgressShown } = scanTurn(ctx.messages);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* stateless and means a mid-run array rewrite (compaction) can't invalidate it.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type {
|
|
21
|
+
import type { HookFunction, StopContext } from "@vellumai/plugin-api";
|
|
22
22
|
|
|
23
23
|
import { getConfig } from "../../../../config/loader.js";
|
|
24
24
|
import { getConversation } from "../../../../memory/conversation-crud.js";
|
|
@@ -65,7 +65,7 @@ function shouldRetryFallbackTitle(conversation: {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const stop:
|
|
68
|
+
const stop: HookFunction<StopContext> = async (ctx) => {
|
|
69
69
|
// Re-title only at a genuine successful turn end (the model returned a reply
|
|
70
70
|
// with no tool calls). Any other terminal — a provider rejection, abort, or
|
|
71
71
|
// an output-limit cutoff — produced no new topic to re-title from.
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
|
-
|
|
14
|
+
HookFunction,
|
|
15
15
|
UserPromptSubmitContext,
|
|
16
16
|
} from "@vellumai/plugin-api";
|
|
17
17
|
|
|
18
18
|
import { getConversation } from "../../../../memory/conversation-crud.js";
|
|
19
19
|
import { queueGenerateConversationTitle } from "../../../../memory/conversation-title-service.js";
|
|
20
20
|
|
|
21
|
-
const userPromptSubmit:
|
|
21
|
+
const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
|
|
22
22
|
// System conversations (background/scheduled) carry a deterministic title
|
|
23
23
|
// from bootstrap. Their own job prompts arrive as non-interactive turns and
|
|
24
24
|
// must not spend an LLM call on a title nobody reads — only a genuine user
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* result for the tool resets its streak.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import type {
|
|
25
|
+
import type { HookFunction, PostToolUseContext } from "@vellumai/plugin-api";
|
|
26
26
|
|
|
27
27
|
import type { Message } from "../../../../providers/types.js";
|
|
28
28
|
|
|
@@ -85,7 +85,7 @@ function priorConsecutiveErrors(
|
|
|
85
85
|
return streak;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const postToolUse:
|
|
88
|
+
const postToolUse: HookFunction<PostToolUseContext> = async (ctx) => {
|
|
89
89
|
if (ctx.toolResponse.is_error !== true) return;
|
|
90
90
|
|
|
91
91
|
const namesById = toolNamesById(ctx.messages);
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* The hook mutates `toolResponse.content` in place.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {
|
|
11
|
+
import type { HookFunction, PostToolUseContext } from "@vellumai/plugin-api";
|
|
12
12
|
|
|
13
13
|
import { truncateToolResult } from "../terminal.js";
|
|
14
14
|
|
|
15
|
-
const postToolUse:
|
|
15
|
+
const postToolUse: HookFunction<PostToolUseContext> = async (ctx) => {
|
|
16
16
|
const { content, truncated } = truncateToolResult(
|
|
17
17
|
ctx.toolResponse.content,
|
|
18
18
|
ctx.maxInputTokens,
|
|
@@ -54,8 +54,8 @@ import type { Tool, ToolDefinition } from "../tools/types.js";
|
|
|
54
54
|
import { getLogger } from "../util/logger.js";
|
|
55
55
|
import { registerPlugin } from "./registry.js";
|
|
56
56
|
import type {
|
|
57
|
+
HookFunction,
|
|
57
58
|
Plugin,
|
|
58
|
-
PluginHookFn,
|
|
59
59
|
PluginHooks,
|
|
60
60
|
PluginManifest,
|
|
61
61
|
} from "./types.js";
|
|
@@ -195,7 +195,7 @@ async function loadHooks(
|
|
|
195
195
|
if (files.length === 0) return undefined;
|
|
196
196
|
const hooks: PluginHooks = {};
|
|
197
197
|
for (const { name, path } of files) {
|
|
198
|
-
const fn = await importDefault<
|
|
198
|
+
const fn = await importDefault<HookFunction>(path);
|
|
199
199
|
if (typeof fn !== "function") {
|
|
200
200
|
throw new Error(
|
|
201
201
|
`external plugin ${pluginName}: hooks/${name} default export must be a function (got ${typeof fn})`,
|
|
@@ -35,10 +35,7 @@ import {
|
|
|
35
35
|
runShutdownHook,
|
|
36
36
|
WORKSPACE_HOOKS_OWNER,
|
|
37
37
|
} from "../hooks/hook-loader.js";
|
|
38
|
-
import type {
|
|
39
|
-
PluginHookFn,
|
|
40
|
-
PluginShutdownContext,
|
|
41
|
-
} from "../plugin-api/types.js";
|
|
38
|
+
import type { HookFunction, ShutdownContext } from "../plugin-api/types.js";
|
|
42
39
|
import {
|
|
43
40
|
registerPluginTools,
|
|
44
41
|
unregisterPluginTools,
|
|
@@ -60,9 +57,9 @@ import {
|
|
|
60
57
|
setSurfaceImportTimeout,
|
|
61
58
|
} from "./surface-import.js";
|
|
62
59
|
|
|
63
|
-
// Re-export for type compat — consumers that import
|
|
60
|
+
// Re-export for type compat — consumers that import HookFunction from
|
|
64
61
|
// the mtime cache module still resolve.
|
|
65
|
-
export type {
|
|
62
|
+
export type { HookFunction } from "./types.js";
|
|
66
63
|
|
|
67
64
|
const log = getLogger("plugin-mtime-cache");
|
|
68
65
|
|
|
@@ -184,7 +181,7 @@ const disabledPluginDirs = new Set<string>();
|
|
|
184
181
|
*/
|
|
185
182
|
export async function getUserHooksFor<TCtx = unknown>(
|
|
186
183
|
hookName: string,
|
|
187
|
-
): Promise<
|
|
184
|
+
): Promise<HookFunction<TCtx>[]> {
|
|
188
185
|
await scanPlugins();
|
|
189
186
|
return collectUserHooks<TCtx>(hookName, discoveredPluginDirs);
|
|
190
187
|
}
|
|
@@ -439,7 +436,7 @@ const activatedPlugins: Array<{ name: string }> = [];
|
|
|
439
436
|
*/
|
|
440
437
|
const activatedNames = new Set<string>();
|
|
441
438
|
|
|
442
|
-
const shutdownContext:
|
|
439
|
+
const shutdownContext: ShutdownContext = {
|
|
443
440
|
assistantVersion: APP_VERSION,
|
|
444
441
|
};
|
|
445
442
|
|
package/src/plugins/pipeline.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import type { HookName } from "../plugin-api/constants.js";
|
|
20
20
|
import { getLogger } from "../util/logger.js";
|
|
21
21
|
import { getHooksFor } from "./registry.js";
|
|
22
|
-
import type {
|
|
22
|
+
import type { HookFunction } from "./types.js";
|
|
23
23
|
|
|
24
24
|
// ─── Hook runner ────────────────────────────────────────────────────────────
|
|
25
25
|
|
|
@@ -116,7 +116,7 @@ export async function runHook<TCtx>(
|
|
|
116
116
|
name: HookName,
|
|
117
117
|
initialCtx: TCtx,
|
|
118
118
|
): Promise<TCtx> {
|
|
119
|
-
let hooks:
|
|
119
|
+
let hooks: HookFunction<TCtx>[];
|
|
120
120
|
try {
|
|
121
121
|
hooks = await getHooksFor<TCtx>(name);
|
|
122
122
|
} catch (err) {
|
package/src/plugins/registry.ts
CHANGED
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
|
|
22
22
|
import { getUserHooksFor } from "./mtime-cache.js";
|
|
23
23
|
import {
|
|
24
|
+
type HookFunction,
|
|
24
25
|
type Plugin,
|
|
25
26
|
PluginExecutionError,
|
|
26
|
-
type PluginHookFn,
|
|
27
27
|
} from "./types.js";
|
|
28
28
|
|
|
29
29
|
// ─── Internal state ──────────────────────────────────────────────────────────
|
|
@@ -151,20 +151,20 @@ export function getRegisteredPlugins(): Plugin[] {
|
|
|
151
151
|
* files. First-party default plugin hooks are read synchronously from the
|
|
152
152
|
* registry and prepended.
|
|
153
153
|
*
|
|
154
|
-
* The `TCtx` generic mirrors {@link
|
|
154
|
+
* The `TCtx` generic mirrors {@link HookFunction}'s — callers parameterize
|
|
155
155
|
* over the concrete context type their hook receives. Hooks that mutate
|
|
156
156
|
* the context in place return `void`; hooks that return a new context
|
|
157
157
|
* replace the threaded value for the next hook in the chain.
|
|
158
158
|
*/
|
|
159
159
|
export async function getHooksFor<TCtx = unknown>(
|
|
160
160
|
name: string,
|
|
161
|
-
): Promise<
|
|
161
|
+
): Promise<HookFunction<TCtx>[]> {
|
|
162
162
|
// First-party defaults from the registry (synchronous).
|
|
163
|
-
const defaultHooks:
|
|
163
|
+
const defaultHooks: HookFunction<TCtx>[] = [];
|
|
164
164
|
for (const plugin of registeredPlugins.values()) {
|
|
165
165
|
const hook = plugin.hooks?.[name];
|
|
166
166
|
if (hook) {
|
|
167
|
-
defaultHooks.push(hook as
|
|
167
|
+
defaultHooks.push(hook as HookFunction<TCtx>);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
package/src/plugins/types.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
} from "../daemon/conversation-runtime-assembly.js";
|
|
20
20
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
21
21
|
import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
|
|
22
|
-
import type {
|
|
22
|
+
import type { HookFunction } from "../plugin-api/types.js";
|
|
23
23
|
import type { Message } from "../providers/types.js";
|
|
24
24
|
import type { SkillRoute } from "../runtime/skill-route-registry.js";
|
|
25
25
|
import type { Tool } from "../tools/types.js";
|
|
@@ -67,9 +67,9 @@ export interface PluginManifest {
|
|
|
67
67
|
// existing internal call sites keep working. Plugin authors import these from
|
|
68
68
|
// `@vellumai/plugin-api`.
|
|
69
69
|
export type {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
HookFunction,
|
|
71
|
+
InitContext,
|
|
72
|
+
ShutdownContext,
|
|
73
73
|
} from "../plugin-api/types.js";
|
|
74
74
|
|
|
75
75
|
// ─── Memory-graph result ─────────────────────────────────────────────────────
|
|
@@ -343,14 +343,14 @@ export type PluginRouteRegistration = SkillRoute;
|
|
|
343
343
|
* unknown keys are forward-compat scaffolding.
|
|
344
344
|
*
|
|
345
345
|
* See `assistant/src/daemon/external-plugins-bootstrap.ts` for the
|
|
346
|
-
* full lifecycle, and {@link
|
|
346
|
+
* full lifecycle, and {@link HookFunction} for the per-entry signature.
|
|
347
347
|
*/
|
|
348
348
|
// The map stores hooks for arbitrary keys with arbitrary context shapes.
|
|
349
349
|
// `any` (rather than `unknown`) is required so concrete plugin signatures
|
|
350
|
-
// like `(ctx:
|
|
350
|
+
// like `(ctx: InitContext) => Promise<void>` and `() => Promise<void>`
|
|
351
351
|
// both assign in/out of slot entries under strict-function-types contravariance.
|
|
352
352
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
353
|
-
export type PluginHooks = Record<string,
|
|
353
|
+
export type PluginHooks = Record<string, HookFunction<any>>;
|
|
354
354
|
|
|
355
355
|
/**
|
|
356
356
|
* A registered plugin. Every field besides `manifest` is optional — a plugin
|
package/src/tools/types.ts
CHANGED
|
@@ -249,6 +249,26 @@ export function stringifyToolInput(input: Record<string, unknown>): string {
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Runtime context passed as the second argument to every tool's `execute`.
|
|
254
|
+
*
|
|
255
|
+
* The fields fall into two groups:
|
|
256
|
+
*
|
|
257
|
+
* - A small, stable core that we are comfortable exposing to any tool —
|
|
258
|
+
* including workspace- and plugin-authored tools via `@vellumai/plugin-api`:
|
|
259
|
+
* `conversationId`, `workingDir`, `requestId`, `signal`, `onOutput`,
|
|
260
|
+
* `assistantId`, `isInteractive`.
|
|
261
|
+
* - Everything tagged `@legacy` below: host-internal routing, permission,
|
|
262
|
+
* trust, requester-identity, proxy, and telemetry metadata that historically
|
|
263
|
+
* accreted on this single context. These are NOT a surface we want third-party
|
|
264
|
+
* tools to depend on; we are triaging them post-launch with the goal of
|
|
265
|
+
* moving them off the public context (or removing them) over time. Grep for
|
|
266
|
+
* `@legacy` to enumerate the set. Do not add new fields here — extend the
|
|
267
|
+
* stable core only when a field is genuinely safe and stable to expose.
|
|
268
|
+
*
|
|
269
|
+
* The daemon constructs and passes the full object to every tool at runtime; a
|
|
270
|
+
* tool that only reads the stable core is unaffected by the eventual cleanup.
|
|
271
|
+
*/
|
|
252
272
|
export interface ToolContext {
|
|
253
273
|
/** Identifier of the conversation this tool invocation belongs to. */
|
|
254
274
|
conversationId: string;
|
|
@@ -262,23 +282,44 @@ export interface ToolContext {
|
|
|
262
282
|
onOutput?: (chunk: string) => void;
|
|
263
283
|
/** Logical assistant scope for multi-assistant routing. */
|
|
264
284
|
assistantId?: string;
|
|
265
|
-
/**
|
|
285
|
+
/** True when an interactive client is connected (not just a no-op callback). */
|
|
286
|
+
isInteractive?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* When set, the tool execution is part of a task run. Used to retrieve ephemeral permission rules.
|
|
289
|
+
* @legacy
|
|
290
|
+
*/
|
|
266
291
|
taskRunId?: string;
|
|
267
292
|
/**
|
|
268
293
|
* Model attribution snapshot for the conversation at invocation time
|
|
269
294
|
* (provider/model/profile that issued this tool call). Used by tool
|
|
270
295
|
* telemetry; never sent to the tool itself.
|
|
296
|
+
* @legacy
|
|
271
297
|
*/
|
|
272
298
|
attribution?: UsageAttributionSnapshot | null;
|
|
273
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* Optional callback for tool lifecycle events (start/prompt/deny/execute/error).
|
|
301
|
+
* @legacy
|
|
302
|
+
*/
|
|
274
303
|
onToolLifecycleEvent?: ToolLifecycleEventHandler;
|
|
275
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* Optional resolver for proxy tools - delegates execution to an external client.
|
|
306
|
+
* @legacy
|
|
307
|
+
*/
|
|
276
308
|
proxyToolResolver?: ProxyToolResolver;
|
|
277
|
-
/**
|
|
309
|
+
/**
|
|
310
|
+
* When set, only tools in this set may execute. Tools outside the set are blocked with an error.
|
|
311
|
+
* @legacy
|
|
312
|
+
*/
|
|
278
313
|
allowedToolNames?: Set<string>;
|
|
279
|
-
/**
|
|
314
|
+
/**
|
|
315
|
+
* True when this turn is restricted to storage cleanup-safe tools.
|
|
316
|
+
* @legacy
|
|
317
|
+
*/
|
|
280
318
|
diskPressureCleanupModeActive?: boolean;
|
|
281
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* Prompt the user for a secret value via native SecureField UI.
|
|
321
|
+
* @legacy
|
|
322
|
+
*/
|
|
282
323
|
requestSecret?: (params: {
|
|
283
324
|
service: string;
|
|
284
325
|
field: string;
|
|
@@ -289,11 +330,15 @@ export interface ToolContext {
|
|
|
289
330
|
allowedTools?: string[];
|
|
290
331
|
allowedDomains?: string[];
|
|
291
332
|
}) => Promise<SecretPromptResult>;
|
|
292
|
-
/**
|
|
333
|
+
/**
|
|
334
|
+
* Optional callback to send a message to the connected client (e.g. open_url).
|
|
335
|
+
* @legacy
|
|
336
|
+
*/
|
|
293
337
|
sendToClient?: (msg: { type: string; [key: string]: unknown }) => void;
|
|
294
|
-
/**
|
|
295
|
-
|
|
296
|
-
|
|
338
|
+
/**
|
|
339
|
+
* When true, tools with side effects should always prompt for confirmation.
|
|
340
|
+
* @legacy
|
|
341
|
+
*/
|
|
297
342
|
forcePromptSideEffects?: boolean;
|
|
298
343
|
/**
|
|
299
344
|
* When true, the tool requires a fresh interactive approval for every
|
|
@@ -304,26 +349,46 @@ export interface ToolContext {
|
|
|
304
349
|
* temporary override options in the prompt UI. Used by
|
|
305
350
|
* `manage_secure_command_tool` to ensure a human reviews each secure
|
|
306
351
|
* bundle installation.
|
|
352
|
+
* @legacy
|
|
307
353
|
*/
|
|
308
354
|
requireFreshApproval?: boolean;
|
|
309
|
-
/**
|
|
355
|
+
/**
|
|
356
|
+
* Approval callback for proxy policy decisions that require user confirmation.
|
|
357
|
+
* @legacy
|
|
358
|
+
*/
|
|
310
359
|
proxyApprovalCallback?: ProxyApprovalCallback;
|
|
311
|
-
/**
|
|
360
|
+
/**
|
|
361
|
+
* Optional principal identifier propagated to sub-tool confirmation flows.
|
|
362
|
+
* @legacy
|
|
363
|
+
*/
|
|
312
364
|
principal?: string;
|
|
313
365
|
/**
|
|
314
366
|
* Trust classification of the actor who initiated this tool invocation.
|
|
315
367
|
* Determines permission level: guardians self-approve, trusted contacts
|
|
316
368
|
* may escalate to guardian for approval, unknown actors are fail-closed.
|
|
317
369
|
* See {@link TrustClass} in actor-trust-resolver.ts for value semantics.
|
|
370
|
+
* @legacy
|
|
318
371
|
*/
|
|
319
372
|
trustClass: TrustClass;
|
|
320
|
-
/**
|
|
373
|
+
/**
|
|
374
|
+
* Channel through which the tool invocation originates (e.g. 'telegram', 'phone'). Used for scoped grant consumption.
|
|
375
|
+
* @legacy
|
|
376
|
+
*/
|
|
321
377
|
executionChannel?: string;
|
|
322
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* Voice/call session ID, if the invocation originates from a call. Used for scoped grant consumption.
|
|
380
|
+
* @legacy
|
|
381
|
+
*/
|
|
323
382
|
callSessionId?: string;
|
|
324
|
-
/**
|
|
383
|
+
/**
|
|
384
|
+
* True when the tool invocation was triggered by a user clicking a surface action button (not a regular message).
|
|
385
|
+
* @legacy
|
|
386
|
+
*/
|
|
325
387
|
triggeredBySurfaceAction?: boolean;
|
|
326
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* True when the user explicitly approved this tool invocation via the interactive permission prompt (not auto-approved by trust rules or temporary overrides).
|
|
390
|
+
* @legacy
|
|
391
|
+
*/
|
|
327
392
|
approvedViaPrompt?: boolean;
|
|
328
393
|
/**
|
|
329
394
|
* True when the invocation is inside a scheduled task run whose
|
|
@@ -331,21 +396,43 @@ export interface ToolContext {
|
|
|
331
396
|
* Tools that normally require a surface-action click (e.g. bulk archive,
|
|
332
397
|
* unsubscribe) may treat this as equivalent consent, since the user
|
|
333
398
|
* already reviewed the tool list when the task was saved.
|
|
399
|
+
* @legacy
|
|
334
400
|
*/
|
|
335
401
|
batchAuthorizedByTask?: boolean;
|
|
336
|
-
/**
|
|
402
|
+
/**
|
|
403
|
+
* External user ID of the requester (non-guardian actor). Used for scoped grant consumption.
|
|
404
|
+
* @legacy
|
|
405
|
+
*/
|
|
337
406
|
requesterExternalUserId?: string;
|
|
338
|
-
/**
|
|
407
|
+
/**
|
|
408
|
+
* Chat ID of the requester (non-guardian actor). Used for tool grant request escalation notifications.
|
|
409
|
+
* @legacy
|
|
410
|
+
*/
|
|
339
411
|
requesterChatId?: string;
|
|
340
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* Human-readable identifier for the requester (e.g., @username).
|
|
414
|
+
* @legacy
|
|
415
|
+
*/
|
|
341
416
|
requesterIdentifier?: string;
|
|
342
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* Preferred display name for the requester.
|
|
419
|
+
* @legacy
|
|
420
|
+
*/
|
|
343
421
|
requesterDisplayName?: string;
|
|
344
|
-
/**
|
|
422
|
+
/**
|
|
423
|
+
* Slack channel ID for channel-scoped permission enforcement. When set, tools are checked against the channel's permission profile.
|
|
424
|
+
* @legacy
|
|
425
|
+
*/
|
|
345
426
|
channelPermissionChannelId?: string;
|
|
346
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* The tool_use block ID from the LLM response, used to correlate confirmation prompts with specific tool invocations.
|
|
429
|
+
* @legacy
|
|
430
|
+
*/
|
|
347
431
|
toolUseId?: string;
|
|
348
|
-
/**
|
|
432
|
+
/**
|
|
433
|
+
* True when the assistant is running as a platform-managed remote instance. Used to auto-approve sandboxed bash tools.
|
|
434
|
+
* @legacy
|
|
435
|
+
*/
|
|
349
436
|
isPlatformHosted?: boolean;
|
|
350
437
|
/**
|
|
351
438
|
* The interface ID of the connected client driving the current turn (e.g.
|
|
@@ -353,6 +440,7 @@ export interface ToolContext {
|
|
|
353
440
|
* transport preference — for example, macOS-originated turns prefer the
|
|
354
441
|
* user's real Chrome session via the paired extension before falling back
|
|
355
442
|
* to cdp-inspect or local Playwright.
|
|
443
|
+
* @legacy
|
|
356
444
|
*/
|
|
357
445
|
transportInterface?: InterfaceId;
|
|
358
446
|
/**
|
|
@@ -363,6 +451,7 @@ export interface ToolContext {
|
|
|
363
451
|
* has `inferenceProfile` set — the override only flows through the
|
|
364
452
|
* in-memory `SubagentConfig.overrideProfile` chain. See
|
|
365
453
|
* `executeSubagentSpawn` in tools/subagent/spawn.ts.
|
|
454
|
+
* @legacy
|
|
366
455
|
*/
|
|
367
456
|
overrideProfile?: string;
|
|
368
457
|
/**
|
|
@@ -371,6 +460,7 @@ export interface ToolContext {
|
|
|
371
460
|
* default a spawned subagent's inference profile to the profile the invoking
|
|
372
461
|
* turn resolved to, so subagents match whatever agent invoked them rather
|
|
373
462
|
* than always falling back to the static `subagentSpawn` call-site default.
|
|
463
|
+
* @legacy
|
|
374
464
|
*/
|
|
375
465
|
invokingCallSite?: LLMCallSite;
|
|
376
466
|
/**
|
|
@@ -379,6 +469,7 @@ export interface ToolContext {
|
|
|
379
469
|
* Used by host proxies to bind cross-client targeted execution to the same
|
|
380
470
|
* authenticated user identity. May be undefined for legacy/internal flows
|
|
381
471
|
* with no resolved actor identity.
|
|
472
|
+
* @legacy
|
|
382
473
|
*/
|
|
383
474
|
sourceActorPrincipalId?: string;
|
|
384
475
|
}
|