@librechat/agents 3.4.0 → 3.4.2
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/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +165 -57
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
- package/dist/cjs/graphs/createGraph.cjs +15 -0
- package/dist/cjs/graphs/createGraph.cjs.map +1 -0
- package/dist/cjs/graphs/index.cjs +1 -0
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/main.cjs +34 -24
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +130 -53
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +4 -4
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +321 -95
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/search/rerankers.cjs +154 -1
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
- package/dist/cjs/tools/subagent/index.cjs +1 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +165 -57
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
- package/dist/esm/graphs/createGraph.mjs +15 -0
- package/dist/esm/graphs/createGraph.mjs.map +1 -0
- package/dist/esm/graphs/index.mjs +1 -0
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/main.mjs +21 -19
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +131 -54
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +2 -2
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/SubagentTool.mjs +4 -4
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +322 -96
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
- package/dist/esm/tools/search/rerankers.mjs +154 -1
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
- package/dist/esm/tools/subagent/index.mjs +1 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/agents/AgentContext.d.ts +1 -1
- package/dist/types/graphs/Graph.d.ts +22 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
- package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
- package/dist/types/graphs/createGraph.d.ts +13 -0
- package/dist/types/graphs/graphFactory.d.ts +13 -0
- package/dist/types/graphs/index.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/run.d.ts +6 -3
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/SubagentTool.d.ts +1 -1
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/search/rerankers.d.ts +28 -0
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
- package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
- package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
- package/dist/types/tools/subagent/index.d.ts +1 -1
- package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +189 -24
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +8 -2
- package/src/agents/AgentContext.ts +1 -1
- package/src/graphs/Graph.ts +349 -104
- package/src/graphs/MultiAgentGraph.ts +149 -47
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
- package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
- package/src/graphs/__tests__/createGraph.test.ts +206 -0
- package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
- package/src/graphs/createGraph.ts +32 -0
- package/src/graphs/graphFactory.ts +12 -0
- package/src/graphs/index.ts +2 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +214 -56
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/specs/graph-subagent.live.test.ts +242 -0
- package/src/specs/graph-subagent.test.ts +322 -0
- package/src/specs/langfuse-routing.integration.test.ts +120 -1
- package/src/specs/subagent.test.ts +130 -4
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/SubagentTool.ts +4 -4
- package/src/tools/ToolNode.ts +643 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
- package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/search/rag-api-reranker.test.ts +891 -0
- package/src/tools/search/rerankers.ts +260 -0
- package/src/tools/search/tool.ts +6 -0
- package/src/tools/search/types.ts +45 -1
- package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
- package/src/tools/subagent/SubagentExecutor.ts +2232 -312
- package/src/tools/subagent/SubagentReplay.ts +584 -0
- package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
- package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
- package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
- package/src/tools/subagent/childGraphConfig.ts +694 -0
- package/src/tools/subagent/index.ts +6 -0
- package/src/tools/subagent/runtimeLimits.ts +7 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +222 -24
- package/src/types/hitl.ts +16 -0
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
//#region src/hooks/HookRegistry.ts
|
|
2
|
+
function serializeApprovalKey(key) {
|
|
3
|
+
return JSON.stringify([
|
|
4
|
+
key.executionScope,
|
|
5
|
+
key.agentId,
|
|
6
|
+
key.toolUseId
|
|
7
|
+
]);
|
|
8
|
+
}
|
|
9
|
+
function deserializeApprovalKey(value) {
|
|
10
|
+
const parsed = JSON.parse(value);
|
|
11
|
+
if (!Array.isArray(parsed) || parsed.length !== 3 || parsed.some((part) => typeof part !== "string")) return null;
|
|
12
|
+
return {
|
|
13
|
+
executionScope: parsed[0],
|
|
14
|
+
agentId: parsed[1],
|
|
15
|
+
toolUseId: parsed[2]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
2
18
|
/**
|
|
3
19
|
* Events whose hooks can change a tool call's input or output. Presence of
|
|
4
20
|
* any of these disables eager tool execution and early completion emission;
|
|
@@ -42,6 +58,8 @@ var HookRegistry = class {
|
|
|
42
58
|
* O(1) insertion in hot paths, no spread-on-write.
|
|
43
59
|
*/
|
|
44
60
|
haltSignals = /* @__PURE__ */ new Map();
|
|
61
|
+
/** One-shot hook contributions retained until approval is consumed. */
|
|
62
|
+
pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
45
63
|
/**
|
|
46
64
|
* Register a matcher for the lifetime of this registry (= one Run).
|
|
47
65
|
* Returns an unregister function that removes the matcher by reference.
|
|
@@ -100,6 +118,71 @@ var HookRegistry = class {
|
|
|
100
118
|
*/
|
|
101
119
|
clearSession(sessionId) {
|
|
102
120
|
this.sessions.delete(sessionId);
|
|
121
|
+
this.pendingToolApprovals.delete(sessionId);
|
|
122
|
+
}
|
|
123
|
+
/** Copies session-scoped policy into a rebuilt or branched Run. */
|
|
124
|
+
copySession(sourceSessionId, targetSessionId) {
|
|
125
|
+
if (sourceSessionId === targetSessionId) return;
|
|
126
|
+
const source = this.sessions.get(sourceSessionId);
|
|
127
|
+
if (source != null) {
|
|
128
|
+
const target = this.ensureSessionBucket(targetSessionId);
|
|
129
|
+
for (const event of Object.keys(source)) {
|
|
130
|
+
const targetList = ensureList(target, event);
|
|
131
|
+
for (const matcher of readList(source, event)) if (!targetList.includes(matcher)) targetList.push(matcher);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const pending = this.pendingToolApprovals.get(sourceSessionId);
|
|
135
|
+
if (pending == null) return;
|
|
136
|
+
let targetPending = this.pendingToolApprovals.get(targetSessionId);
|
|
137
|
+
if (targetPending == null) {
|
|
138
|
+
targetPending = /* @__PURE__ */ new Map();
|
|
139
|
+
this.pendingToolApprovals.set(targetSessionId, targetPending);
|
|
140
|
+
}
|
|
141
|
+
for (const [toolUseId, result] of pending) if (!targetPending.has(toolUseId)) targetPending.set(toolUseId, result);
|
|
142
|
+
}
|
|
143
|
+
getPendingToolApproval(sessionId, key) {
|
|
144
|
+
return this.pendingToolApprovals.get(sessionId)?.get(serializeApprovalKey(key));
|
|
145
|
+
}
|
|
146
|
+
setPendingToolApproval(sessionId, key, result) {
|
|
147
|
+
let pending = this.pendingToolApprovals.get(sessionId);
|
|
148
|
+
if (pending == null) {
|
|
149
|
+
pending = /* @__PURE__ */ new Map();
|
|
150
|
+
this.pendingToolApprovals.set(sessionId, pending);
|
|
151
|
+
}
|
|
152
|
+
pending.set(serializeApprovalKey(key), result);
|
|
153
|
+
}
|
|
154
|
+
clearPendingToolApproval(sessionId, key) {
|
|
155
|
+
const pending = this.pendingToolApprovals.get(sessionId);
|
|
156
|
+
if (pending == null) return;
|
|
157
|
+
pending.delete(serializeApprovalKey(key));
|
|
158
|
+
if (pending.size === 0) this.pendingToolApprovals.delete(sessionId);
|
|
159
|
+
}
|
|
160
|
+
snapshotPendingToolApprovals(sessionId, executionScope) {
|
|
161
|
+
const pending = this.pendingToolApprovals.get(sessionId);
|
|
162
|
+
if (pending == null) return [];
|
|
163
|
+
const snapshots = [];
|
|
164
|
+
for (const [serializedKey, result] of pending) {
|
|
165
|
+
const key = deserializeApprovalKey(serializedKey);
|
|
166
|
+
if (key == null || key.executionScope !== executionScope) continue;
|
|
167
|
+
snapshots.push({
|
|
168
|
+
key,
|
|
169
|
+
result
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return snapshots;
|
|
173
|
+
}
|
|
174
|
+
restorePendingToolApprovals(sessionId, targetExecutionScope, snapshots) {
|
|
175
|
+
const restored = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const [serializedKey, result] of this.pendingToolApprovals.get(sessionId) ?? []) if (deserializeApprovalKey(serializedKey)?.executionScope !== targetExecutionScope) restored.set(serializedKey, result);
|
|
177
|
+
for (const snapshot of snapshots) restored.set(serializeApprovalKey({
|
|
178
|
+
...snapshot.key,
|
|
179
|
+
executionScope: targetExecutionScope
|
|
180
|
+
}), snapshot.result);
|
|
181
|
+
if (restored.size === 0) {
|
|
182
|
+
this.pendingToolApprovals.delete(sessionId);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.pendingToolApprovals.set(sessionId, restored);
|
|
103
186
|
}
|
|
104
187
|
/**
|
|
105
188
|
* Raise a halt signal scoped to `sessionId` (= the run id the hook
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HookRegistry.cjs","names":[],"sources":["../../../src/hooks/HookRegistry.ts"],"sourcesContent":["// src/hooks/HookRegistry.ts\nimport type { HookEvent, HookMatcher } from './types';\n\n/**\n * Internal matcher storage type.\n *\n * Matchers registered via the public `register<E>` API are strictly typed\n * to a single `E`, but the storage needs one uniform slot type per event.\n * We store them as `HookMatcher<HookEvent>` and cast once at the variance\n * boundary — see `ensureList` and `snapshot` below. The invariant (every\n * matcher in `bucket[event]` was registered with that exact event) is\n * enforced by the public API; breaking it requires bypassing the types.\n */\ntype MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;\n\n/**\n * Events whose hooks can change a tool call's input or output. Presence of\n * any of these disables eager tool execution and early completion emission;\n * observation-only events (`PostToolBatch`, `Stop`, telemetry hooks) do not.\n */\nconst RESULT_ALTERING_HOOK_EVENTS = [\n 'PreToolUse',\n 'PostToolUse',\n 'PostToolUseFailure',\n] as const satisfies readonly HookEvent[];\n\n/**\n * Snapshot of a halt request raised by a hook returning\n * `preventContinuation: true`. The SDK's run loop polls for this between\n * stream events and exits cleanly when set, skipping the `Stop` hook\n * (the run is being halted, not naturally completing). One per registry\n * instance — the first hook to halt wins; subsequent halts are ignored\n * so the original reason isn't clobbered.\n */\nexport interface HookHaltSignal {\n reason: string;\n /** Event of the hook that triggered the halt (for diagnostics). */\n source: HookEvent;\n}\n\n/**\n * Run-scoped storage for hook matchers with an additional layer for\n * session-scoped matchers that should be cleaned up between sessions.\n *\n * Hosts construct one registry per `Run` (mirroring how `HandlerRegistry` is\n * scoped) and register global matchers + per-session matchers against it.\n * Registration is strictly additive — nothing in this class mutates a\n * matcher's callbacks or flags after insertion.\n *\n * ## Why `Map<sessionId, MatcherBucket>` and not `Record`\n *\n * LibreChat runs thousands of parallel sessions in one Node process, and\n * hook registration happens inside hot paths (tool loading, agent spawning).\n * A `Record<sessionId, ...>` has to be spread on every insertion, which is\n * O(n) per call and O(n²) total for a batch of parallel registrations. A\n * Map mutates in place, keeping insertions O(1). This mirrors the reasoning\n * Claude Code documents at `utils/hooks/sessionHooks.ts:62`.\n */\nexport class HookRegistry {\n private readonly global: MatcherBucket = {};\n private readonly sessions: Map<string, MatcherBucket> = new Map();\n /**\n * Per-session halt signals. Scoped by `sessionId` (= the run id the\n * hook fired under) so a host that shares one registry across\n * concurrent runs cannot leak `preventContinuation` from one run\n * into another. Without scoping, a halt raised by run A's hook\n * would trip run B's stream-loop poll on the next iteration —\n * silently terminating an unrelated run.\n *\n * Map storage mirrors the reasoning above for session matchers:\n * O(1) insertion in hot paths, no spread-on-write.\n */\n private readonly haltSignals: Map<string, HookHaltSignal> = new Map();\n\n /**\n * Register a matcher for the lifetime of this registry (= one Run).\n * Returns an unregister function that removes the matcher by reference.\n */\n register<E extends HookEvent>(event: E, matcher: HookMatcher<E>): () => void {\n const list = ensureList(this.global, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Register a matcher for a specific session. Cleared automatically when\n * `clearSession(sessionId)` is called, or can be removed directly via the\n * returned unregister function.\n */\n registerSession<E extends HookEvent>(\n sessionId: string,\n event: E,\n matcher: HookMatcher<E>\n ): () => void {\n const bucket = this.ensureSessionBucket(sessionId);\n const list = ensureList(bucket, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Returns all matchers registered for `event`, concatenating global first\n * and then session-specific (when `sessionId` is supplied). The caller\n * receives a fresh array, so iterating it is safe even if a matcher is\n * removed mid-iteration (e.g. via `once: true`).\n */\n getMatchers<E extends HookEvent>(\n event: E,\n sessionId?: string\n ): HookMatcher<E>[] {\n const globalList = readList(this.global, event);\n if (sessionId === undefined) {\n return snapshot<E>(globalList);\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return snapshot<E>(globalList);\n }\n const sessionList = readList(bucket, event);\n if (globalList.length === 0) {\n return snapshot<E>(sessionList);\n }\n if (sessionList.length === 0) {\n return snapshot<E>(globalList);\n }\n return snapshot<E>([...globalList, ...sessionList]);\n }\n\n /**\n * Removes `matcher` by reference from global storage first, falling back\n * to the session bucket when `sessionId` is supplied. Used by\n * `executeHooks` to drop `once: true` matchers after they fire.\n */\n removeMatcher<E extends HookEvent>(\n event: E,\n matcher: HookMatcher<E>,\n sessionId?: string\n ): boolean {\n if (removeFromList(readList(this.global, event), matcher)) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return removeFromList(readList(bucket, event), matcher);\n }\n\n /**\n * Drops every session-scoped matcher for `sessionId`. Call this in the\n * `finally` block around a Run so a `once: true` hook that never fired\n * cannot leak into the next session on the same registry.\n */\n clearSession(sessionId: string): void {\n this.sessions.delete(sessionId);\n }\n\n /**\n * Raise a halt signal scoped to `sessionId` (= the run id the hook\n * fired under). The SDK's run loop polls for this between stream\n * events with the run's own id. First-write-wins per session: a\n * halt already raised by an earlier hook in the same run is\n * preserved so the original `reason` / `source` aren't overwritten.\n *\n * Per-session scoping is critical when hosts share one registry\n * across concurrent runs (e.g. a global policy registered once and\n * reused). Without it, a `preventContinuation` from run A would\n * trip run B's stream-loop poll on the next iteration and silently\n * terminate an unrelated run.\n *\n * Called by the SDK after `executeHooks` returns an aggregate with\n * `preventContinuation: true`. Hosts can also call it directly from\n * inside a hook callback if they want to halt without going through\n * the aggregated return value, but `preventContinuation` is the\n * canonical path.\n */\n haltRun(sessionId: string, reason: string, source: HookEvent): void {\n if (this.haltSignals.has(sessionId)) {\n return;\n }\n this.haltSignals.set(sessionId, { reason, source });\n }\n\n /**\n * Returns the halt signal raised by hooks running under `sessionId`,\n * or `undefined` if no hook in that run has halted. Polled by\n * `Run.processStream` between stream events using the run's own id.\n */\n getHaltSignal(sessionId: string): HookHaltSignal | undefined {\n return this.haltSignals.get(sessionId);\n }\n\n /**\n * Clears the halt signal for `sessionId`. Called by\n * `Run.processStream` in its `finally` block so a subsequent\n * invocation of the same Run (e.g. resume) starts with a fresh\n * halt state. No-op when no signal exists for that session.\n */\n clearHaltSignal(sessionId: string): void {\n this.haltSignals.delete(sessionId);\n }\n\n /**\n * True when any registered hook can alter a tool result before or after\n * execution (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`). Eager\n * tool execution and early completion emission gate on this instead of\n * registry presence, so observation-only registries (e.g. a\n * `PostToolBatch` steering drain) keep those fast paths. With\n * `sessionId`, checks global + that session; without it, conservatively\n * scans every session bucket.\n */\n hasResultAlteringHooks(sessionId?: string): boolean {\n if (hasResultAlteringInBucket(this.global)) {\n return true;\n }\n if (sessionId !== undefined) {\n const bucket = this.sessions.get(sessionId);\n return bucket !== undefined && hasResultAlteringInBucket(bucket);\n }\n for (const bucket of this.sessions.values()) {\n if (hasResultAlteringInBucket(bucket)) {\n return true;\n }\n }\n return false;\n }\n\n /** True if at least one matcher exists for `event` (global + session). */\n hasHookFor(event: HookEvent, sessionId?: string): boolean {\n if (readList(this.global, event).length > 0) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return readList(bucket, event).length > 0;\n }\n\n /**\n * True when at least one matcher for `event` would actually DISPATCH on a\n * query-less call — a wildcard pattern with a non-empty `hooks` array.\n *\n * `hasHookFor` answers \"is one registered\", which is not the same question:\n * a matcher carrying a pattern is inert for events that supply no\n * `matchQuery`, so a caller using registration as a proxy for \"something\n * will run\" can act on a hook that never fires. Mirrors the two skips in\n * `executeHooks` (pattern mismatch, empty `hooks`).\n *\n * Non-allocating on purpose — `StandardGraph.canClaimPreemptSeal` reads it\n * once per streamed chunk, where `getMatchers`' defensive `slice()` would\n * allocate on every delta.\n */\n hasDispatchableHookFor(event: HookEvent, sessionId?: string): boolean {\n if (hasDispatchableInList(readList(this.global, event))) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return hasDispatchableInList(readList(bucket, event));\n }\n\n private ensureSessionBucket(sessionId: string): MatcherBucket {\n const existing = this.sessions.get(sessionId);\n if (existing !== undefined) {\n return existing;\n }\n const fresh: MatcherBucket = {};\n this.sessions.set(sessionId, fresh);\n return fresh;\n }\n}\n\nfunction ensureList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n const existing = bucket[event];\n if (existing !== undefined) {\n return existing;\n }\n const fresh: HookMatcher<HookEvent>[] = [];\n bucket[event] = fresh;\n return fresh;\n}\n\nfunction readList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n return bucket[event] ?? [];\n}\n\nfunction hasDispatchableInList(list: HookMatcher<HookEvent>[]): boolean {\n for (const matcher of list) {\n if (\n (matcher.pattern === undefined || matcher.pattern === '') &&\n matcher.hooks.length > 0\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasResultAlteringInBucket(bucket: MatcherBucket): boolean {\n for (const event of RESULT_ALTERING_HOOK_EVENTS) {\n if (readList(bucket, event).length > 0) {\n return true;\n }\n }\n return false;\n}\n\nfunction removeFromList<E extends HookEvent>(\n list: HookMatcher<HookEvent>[],\n matcher: HookMatcher<E>\n): boolean {\n const idx = list.indexOf(widen(matcher));\n if (idx < 0) {\n return false;\n }\n list.splice(idx, 1);\n return true;\n}\n\n/**\n * Widen a per-event matcher to the storage's uniform slot type. Unsound at\n * the type level (function parameters are contravariant) but safe by\n * construction: `HookRegistry.register<E>` only ever puts matchers into the\n * bucket slot for their own event, and reads go through `snapshot<E>`\n * which is only called with the same `E`.\n */\nfunction widen<E extends HookEvent>(\n matcher: HookMatcher<E>\n): HookMatcher<HookEvent> {\n return matcher as unknown as HookMatcher<HookEvent>;\n}\n\n/**\n * Narrow a storage list back to a per-event matcher list on the way out.\n * Sound counterpart to `widen`: the list only contains matchers that were\n * registered against `E`, because the public API enforces it on insert.\n */\nfunction snapshot<E extends HookEvent>(\n list: readonly HookMatcher<HookEvent>[]\n): HookMatcher<E>[] {\n return list.slice() as unknown as HookMatcher<E>[];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HookRegistry.cjs","names":[],"sources":["../../../src/hooks/HookRegistry.ts"],"sourcesContent":["// src/hooks/HookRegistry.ts\nimport type {\n HookEvent,\n HookMatcher,\n ToolApprovalReplayKey,\n ToolApprovalReplaySnapshot,\n AggregatedHookResult,\n} from './types';\n\nfunction serializeApprovalKey(key: ToolApprovalReplayKey): string {\n return JSON.stringify([key.executionScope, key.agentId, key.toolUseId]);\n}\n\nfunction deserializeApprovalKey(value: string): ToolApprovalReplayKey | null {\n const parsed: unknown = JSON.parse(value);\n if (\n !Array.isArray(parsed) ||\n parsed.length !== 3 ||\n parsed.some((part) => typeof part !== 'string')\n ) {\n return null;\n }\n return {\n executionScope: parsed[0],\n agentId: parsed[1],\n toolUseId: parsed[2],\n };\n}\n\n/**\n * Internal matcher storage type.\n *\n * Matchers registered via the public `register<E>` API are strictly typed\n * to a single `E`, but the storage needs one uniform slot type per event.\n * We store them as `HookMatcher<HookEvent>` and cast once at the variance\n * boundary — see `ensureList` and `snapshot` below. The invariant (every\n * matcher in `bucket[event]` was registered with that exact event) is\n * enforced by the public API; breaking it requires bypassing the types.\n */\ntype MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;\n\n/**\n * Events whose hooks can change a tool call's input or output. Presence of\n * any of these disables eager tool execution and early completion emission;\n * observation-only events (`PostToolBatch`, `Stop`, telemetry hooks) do not.\n */\nconst RESULT_ALTERING_HOOK_EVENTS = [\n 'PreToolUse',\n 'PostToolUse',\n 'PostToolUseFailure',\n] as const satisfies readonly HookEvent[];\n\n/**\n * Snapshot of a halt request raised by a hook returning\n * `preventContinuation: true`. The SDK's run loop polls for this between\n * stream events and exits cleanly when set, skipping the `Stop` hook\n * (the run is being halted, not naturally completing). One per registry\n * instance — the first hook to halt wins; subsequent halts are ignored\n * so the original reason isn't clobbered.\n */\nexport interface HookHaltSignal {\n reason: string;\n /** Event of the hook that triggered the halt (for diagnostics). */\n source: HookEvent;\n}\n\n/**\n * Run-scoped storage for hook matchers with an additional layer for\n * session-scoped matchers that should be cleaned up between sessions.\n *\n * Hosts construct one registry per `Run` (mirroring how `HandlerRegistry` is\n * scoped) and register global matchers + per-session matchers against it.\n * Registration is strictly additive — nothing in this class mutates a\n * matcher's callbacks or flags after insertion.\n *\n * ## Why `Map<sessionId, MatcherBucket>` and not `Record`\n *\n * LibreChat runs thousands of parallel sessions in one Node process, and\n * hook registration happens inside hot paths (tool loading, agent spawning).\n * A `Record<sessionId, ...>` has to be spread on every insertion, which is\n * O(n) per call and O(n²) total for a batch of parallel registrations. A\n * Map mutates in place, keeping insertions O(1). This mirrors the reasoning\n * Claude Code documents at `utils/hooks/sessionHooks.ts:62`.\n */\nexport class HookRegistry {\n private readonly global: MatcherBucket = {};\n private readonly sessions: Map<string, MatcherBucket> = new Map();\n /**\n * Per-session halt signals. Scoped by `sessionId` (= the run id the\n * hook fired under) so a host that shares one registry across\n * concurrent runs cannot leak `preventContinuation` from one run\n * into another. Without scoping, a halt raised by run A's hook\n * would trip run B's stream-loop poll on the next iteration —\n * silently terminating an unrelated run.\n *\n * Map storage mirrors the reasoning above for session matchers:\n * O(1) insertion in hot paths, no spread-on-write.\n */\n private readonly haltSignals: Map<string, HookHaltSignal> = new Map();\n /** One-shot hook contributions retained until approval is consumed. */\n private readonly pendingToolApprovals = new Map<\n string,\n Map<string, AggregatedHookResult>\n >();\n\n /**\n * Register a matcher for the lifetime of this registry (= one Run).\n * Returns an unregister function that removes the matcher by reference.\n */\n register<E extends HookEvent>(event: E, matcher: HookMatcher<E>): () => void {\n const list = ensureList(this.global, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Register a matcher for a specific session. Cleared automatically when\n * `clearSession(sessionId)` is called, or can be removed directly via the\n * returned unregister function.\n */\n registerSession<E extends HookEvent>(\n sessionId: string,\n event: E,\n matcher: HookMatcher<E>\n ): () => void {\n const bucket = this.ensureSessionBucket(sessionId);\n const list = ensureList(bucket, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Returns all matchers registered for `event`, concatenating global first\n * and then session-specific (when `sessionId` is supplied). The caller\n * receives a fresh array, so iterating it is safe even if a matcher is\n * removed mid-iteration (e.g. via `once: true`).\n */\n getMatchers<E extends HookEvent>(\n event: E,\n sessionId?: string\n ): HookMatcher<E>[] {\n const globalList = readList(this.global, event);\n if (sessionId === undefined) {\n return snapshot<E>(globalList);\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return snapshot<E>(globalList);\n }\n const sessionList = readList(bucket, event);\n if (globalList.length === 0) {\n return snapshot<E>(sessionList);\n }\n if (sessionList.length === 0) {\n return snapshot<E>(globalList);\n }\n return snapshot<E>([...globalList, ...sessionList]);\n }\n\n /**\n * Removes `matcher` by reference from global storage first, falling back\n * to the session bucket when `sessionId` is supplied. Used by\n * `executeHooks` to drop `once: true` matchers after they fire.\n */\n removeMatcher<E extends HookEvent>(\n event: E,\n matcher: HookMatcher<E>,\n sessionId?: string\n ): boolean {\n if (removeFromList(readList(this.global, event), matcher)) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return removeFromList(readList(bucket, event), matcher);\n }\n\n /**\n * Drops every session-scoped matcher for `sessionId`. Call this in the\n * `finally` block around a Run so a `once: true` hook that never fired\n * cannot leak into the next session on the same registry.\n */\n clearSession(sessionId: string): void {\n this.sessions.delete(sessionId);\n this.pendingToolApprovals.delete(sessionId);\n }\n\n /** Copies session-scoped policy into a rebuilt or branched Run. */\n copySession(sourceSessionId: string, targetSessionId: string): void {\n if (sourceSessionId === targetSessionId) {\n return;\n }\n const source = this.sessions.get(sourceSessionId);\n if (source != null) {\n const target = this.ensureSessionBucket(targetSessionId);\n for (const event of Object.keys(source) as HookEvent[]) {\n const targetList = ensureList(target, event);\n for (const matcher of readList(source, event)) {\n if (!targetList.includes(matcher)) {\n targetList.push(matcher);\n }\n }\n }\n }\n const pending = this.pendingToolApprovals.get(sourceSessionId);\n if (pending == null) {\n return;\n }\n let targetPending = this.pendingToolApprovals.get(targetSessionId);\n if (targetPending == null) {\n targetPending = new Map();\n this.pendingToolApprovals.set(targetSessionId, targetPending);\n }\n for (const [toolUseId, result] of pending) {\n if (!targetPending.has(toolUseId)) {\n targetPending.set(toolUseId, result);\n }\n }\n }\n\n getPendingToolApproval(\n sessionId: string,\n key: ToolApprovalReplayKey\n ): AggregatedHookResult | undefined {\n return this.pendingToolApprovals\n .get(sessionId)\n ?.get(serializeApprovalKey(key));\n }\n\n setPendingToolApproval(\n sessionId: string,\n key: ToolApprovalReplayKey,\n result: AggregatedHookResult\n ): void {\n let pending = this.pendingToolApprovals.get(sessionId);\n if (pending == null) {\n pending = new Map();\n this.pendingToolApprovals.set(sessionId, pending);\n }\n pending.set(serializeApprovalKey(key), result);\n }\n\n clearPendingToolApproval(\n sessionId: string,\n key: ToolApprovalReplayKey\n ): void {\n const pending = this.pendingToolApprovals.get(sessionId);\n if (pending == null) {\n return;\n }\n pending.delete(serializeApprovalKey(key));\n if (pending.size === 0) {\n this.pendingToolApprovals.delete(sessionId);\n }\n }\n\n snapshotPendingToolApprovals(\n sessionId: string,\n executionScope: string\n ): ToolApprovalReplaySnapshot[] {\n const pending = this.pendingToolApprovals.get(sessionId);\n if (pending == null) {\n return [];\n }\n const snapshots: ToolApprovalReplaySnapshot[] = [];\n for (const [serializedKey, result] of pending) {\n const key = deserializeApprovalKey(serializedKey);\n if (key == null || key.executionScope !== executionScope) {\n continue;\n }\n snapshots.push({ key, result });\n }\n return snapshots;\n }\n\n restorePendingToolApprovals(\n sessionId: string,\n targetExecutionScope: string,\n snapshots: ReadonlyArray<ToolApprovalReplaySnapshot>\n ): void {\n const restored = new Map<string, AggregatedHookResult>();\n for (const [serializedKey, result] of this.pendingToolApprovals.get(\n sessionId\n ) ?? []) {\n const key = deserializeApprovalKey(serializedKey);\n if (key?.executionScope !== targetExecutionScope) {\n restored.set(serializedKey, result);\n }\n }\n for (const snapshot of snapshots) {\n restored.set(\n serializeApprovalKey({\n ...snapshot.key,\n executionScope: targetExecutionScope,\n }),\n snapshot.result\n );\n }\n if (restored.size === 0) {\n this.pendingToolApprovals.delete(sessionId);\n return;\n }\n this.pendingToolApprovals.set(sessionId, restored);\n }\n\n /**\n * Raise a halt signal scoped to `sessionId` (= the run id the hook\n * fired under). The SDK's run loop polls for this between stream\n * events with the run's own id. First-write-wins per session: a\n * halt already raised by an earlier hook in the same run is\n * preserved so the original `reason` / `source` aren't overwritten.\n *\n * Per-session scoping is critical when hosts share one registry\n * across concurrent runs (e.g. a global policy registered once and\n * reused). Without it, a `preventContinuation` from run A would\n * trip run B's stream-loop poll on the next iteration and silently\n * terminate an unrelated run.\n *\n * Called by the SDK after `executeHooks` returns an aggregate with\n * `preventContinuation: true`. Hosts can also call it directly from\n * inside a hook callback if they want to halt without going through\n * the aggregated return value, but `preventContinuation` is the\n * canonical path.\n */\n haltRun(sessionId: string, reason: string, source: HookEvent): void {\n if (this.haltSignals.has(sessionId)) {\n return;\n }\n this.haltSignals.set(sessionId, { reason, source });\n }\n\n /**\n * Returns the halt signal raised by hooks running under `sessionId`,\n * or `undefined` if no hook in that run has halted. Polled by\n * `Run.processStream` between stream events using the run's own id.\n */\n getHaltSignal(sessionId: string): HookHaltSignal | undefined {\n return this.haltSignals.get(sessionId);\n }\n\n /**\n * Clears the halt signal for `sessionId`. Called by\n * `Run.processStream` in its `finally` block so a subsequent\n * invocation of the same Run (e.g. resume) starts with a fresh\n * halt state. No-op when no signal exists for that session.\n */\n clearHaltSignal(sessionId: string): void {\n this.haltSignals.delete(sessionId);\n }\n\n /**\n * True when any registered hook can alter a tool result before or after\n * execution (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`). Eager\n * tool execution and early completion emission gate on this instead of\n * registry presence, so observation-only registries (e.g. a\n * `PostToolBatch` steering drain) keep those fast paths. With\n * `sessionId`, checks global + that session; without it, conservatively\n * scans every session bucket.\n */\n hasResultAlteringHooks(sessionId?: string): boolean {\n if (hasResultAlteringInBucket(this.global)) {\n return true;\n }\n if (sessionId !== undefined) {\n const bucket = this.sessions.get(sessionId);\n return bucket !== undefined && hasResultAlteringInBucket(bucket);\n }\n for (const bucket of this.sessions.values()) {\n if (hasResultAlteringInBucket(bucket)) {\n return true;\n }\n }\n return false;\n }\n\n /** True if at least one matcher exists for `event` (global + session). */\n hasHookFor(event: HookEvent, sessionId?: string): boolean {\n if (readList(this.global, event).length > 0) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return readList(bucket, event).length > 0;\n }\n\n /**\n * True when at least one matcher for `event` would actually DISPATCH on a\n * query-less call — a wildcard pattern with a non-empty `hooks` array.\n *\n * `hasHookFor` answers \"is one registered\", which is not the same question:\n * a matcher carrying a pattern is inert for events that supply no\n * `matchQuery`, so a caller using registration as a proxy for \"something\n * will run\" can act on a hook that never fires. Mirrors the two skips in\n * `executeHooks` (pattern mismatch, empty `hooks`).\n *\n * Non-allocating on purpose — `StandardGraph.canClaimPreemptSeal` reads it\n * once per streamed chunk, where `getMatchers`' defensive `slice()` would\n * allocate on every delta.\n */\n hasDispatchableHookFor(event: HookEvent, sessionId?: string): boolean {\n if (hasDispatchableInList(readList(this.global, event))) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return hasDispatchableInList(readList(bucket, event));\n }\n\n private ensureSessionBucket(sessionId: string): MatcherBucket {\n const existing = this.sessions.get(sessionId);\n if (existing !== undefined) {\n return existing;\n }\n const fresh: MatcherBucket = {};\n this.sessions.set(sessionId, fresh);\n return fresh;\n }\n}\n\nfunction ensureList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n const existing = bucket[event];\n if (existing !== undefined) {\n return existing;\n }\n const fresh: HookMatcher<HookEvent>[] = [];\n bucket[event] = fresh;\n return fresh;\n}\n\nfunction readList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n return bucket[event] ?? [];\n}\n\nfunction hasDispatchableInList(list: HookMatcher<HookEvent>[]): boolean {\n for (const matcher of list) {\n if (\n (matcher.pattern === undefined || matcher.pattern === '') &&\n matcher.hooks.length > 0\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasResultAlteringInBucket(bucket: MatcherBucket): boolean {\n for (const event of RESULT_ALTERING_HOOK_EVENTS) {\n if (readList(bucket, event).length > 0) {\n return true;\n }\n }\n return false;\n}\n\nfunction removeFromList<E extends HookEvent>(\n list: HookMatcher<HookEvent>[],\n matcher: HookMatcher<E>\n): boolean {\n const idx = list.indexOf(widen(matcher));\n if (idx < 0) {\n return false;\n }\n list.splice(idx, 1);\n return true;\n}\n\n/**\n * Widen a per-event matcher to the storage's uniform slot type. Unsound at\n * the type level (function parameters are contravariant) but safe by\n * construction: `HookRegistry.register<E>` only ever puts matchers into the\n * bucket slot for their own event, and reads go through `snapshot<E>`\n * which is only called with the same `E`.\n */\nfunction widen<E extends HookEvent>(\n matcher: HookMatcher<E>\n): HookMatcher<HookEvent> {\n return matcher as unknown as HookMatcher<HookEvent>;\n}\n\n/**\n * Narrow a storage list back to a per-event matcher list on the way out.\n * Sound counterpart to `widen`: the list only contains matchers that were\n * registered against `E`, because the public API enforces it on insert.\n */\nfunction snapshot<E extends HookEvent>(\n list: readonly HookMatcher<HookEvent>[]\n): HookMatcher<E>[] {\n return list.slice() as unknown as HookMatcher<E>[];\n}\n"],"mappings":";AASA,SAAS,qBAAqB,KAAoC;CAChE,OAAO,KAAK,UAAU;EAAC,IAAI;EAAgB,IAAI;EAAS,IAAI;CAAS,CAAC;AACxE;AAEA,SAAS,uBAAuB,OAA6C;CAC3E,MAAM,SAAkB,KAAK,MAAM,KAAK;CACxC,IACE,CAAC,MAAM,QAAQ,MAAM,KACrB,OAAO,WAAW,KAClB,OAAO,MAAM,SAAS,OAAO,SAAS,QAAQ,GAE9C,OAAO;CAET,OAAO;EACL,gBAAgB,OAAO;EACvB,SAAS,OAAO;EAChB,WAAW,OAAO;CACpB;AACF;;;;;;AAmBA,MAAM,8BAA8B;CAClC;CACA;CACA;AACF;;;;;;;;;;;;;;;;;;;AAkCA,IAAa,eAAb,MAA0B;CACxB,SAAyC,CAAC;CAC1C,2BAAwD,IAAI,IAAI;;;;;;;;;;;;CAYhE,8BAA4D,IAAI,IAAI;;CAEpE,uCAAwC,IAAI,IAG1C;;;;;CAMF,SAA8B,OAAU,SAAqC;EAC3E,MAAM,OAAO,WAAW,KAAK,QAAQ,KAAK;EAC1C,KAAK,KAAK,MAAM,OAAO,CAAC;EACxB,aAAa;GACX,eAAe,MAAM,OAAO;EAC9B;CACF;;;;;;CAOA,gBACE,WACA,OACA,SACY;EAEZ,MAAM,OAAO,WADE,KAAK,oBAAoB,SACX,GAAG,KAAK;EACrC,KAAK,KAAK,MAAM,OAAO,CAAC;EACxB,aAAa;GACX,eAAe,MAAM,OAAO;EAC9B;CACF;;;;;;;CAQA,YACE,OACA,WACkB;EAClB,MAAM,aAAa,SAAS,KAAK,QAAQ,KAAK;EAC9C,IAAI,cAAc,KAAA,GAChB,OAAO,SAAY,UAAU;EAE/B,MAAM,SAAS,KAAK,SAAS,IAAI,SAAS;EAC1C,IAAI,WAAW,KAAA,GACb,OAAO,SAAY,UAAU;EAE/B,MAAM,cAAc,SAAS,QAAQ,KAAK;EAC1C,IAAI,WAAW,WAAW,GACxB,OAAO,SAAY,WAAW;EAEhC,IAAI,YAAY,WAAW,GACzB,OAAO,SAAY,UAAU;EAE/B,OAAO,SAAY,CAAC,GAAG,YAAY,GAAG,WAAW,CAAC;CACpD;;;;;;CAOA,cACE,OACA,SACA,WACS;EACT,IAAI,eAAe,SAAS,KAAK,QAAQ,KAAK,GAAG,OAAO,GACtD,OAAO;EAET,IAAI,cAAc,KAAA,GAChB,OAAO;EAET,MAAM,SAAS,KAAK,SAAS,IAAI,SAAS;EAC1C,IAAI,WAAW,KAAA,GACb,OAAO;EAET,OAAO,eAAe,SAAS,QAAQ,KAAK,GAAG,OAAO;CACxD;;;;;;CAOA,aAAa,WAAyB;EACpC,KAAK,SAAS,OAAO,SAAS;EAC9B,KAAK,qBAAqB,OAAO,SAAS;CAC5C;;CAGA,YAAY,iBAAyB,iBAA+B;EAClE,IAAI,oBAAoB,iBACtB;EAEF,MAAM,SAAS,KAAK,SAAS,IAAI,eAAe;EAChD,IAAI,UAAU,MAAM;GAClB,MAAM,SAAS,KAAK,oBAAoB,eAAe;GACvD,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,GAAkB;IACtD,MAAM,aAAa,WAAW,QAAQ,KAAK;IAC3C,KAAK,MAAM,WAAW,SAAS,QAAQ,KAAK,GAC1C,IAAI,CAAC,WAAW,SAAS,OAAO,GAC9B,WAAW,KAAK,OAAO;GAG7B;EACF;EACA,MAAM,UAAU,KAAK,qBAAqB,IAAI,eAAe;EAC7D,IAAI,WAAW,MACb;EAEF,IAAI,gBAAgB,KAAK,qBAAqB,IAAI,eAAe;EACjE,IAAI,iBAAiB,MAAM;GACzB,gCAAgB,IAAI,IAAI;GACxB,KAAK,qBAAqB,IAAI,iBAAiB,aAAa;EAC9D;EACA,KAAK,MAAM,CAAC,WAAW,WAAW,SAChC,IAAI,CAAC,cAAc,IAAI,SAAS,GAC9B,cAAc,IAAI,WAAW,MAAM;CAGzC;CAEA,uBACE,WACA,KACkC;EAClC,OAAO,KAAK,qBACT,IAAI,SAAS,CAAC,EACb,IAAI,qBAAqB,GAAG,CAAC;CACnC;CAEA,uBACE,WACA,KACA,QACM;EACN,IAAI,UAAU,KAAK,qBAAqB,IAAI,SAAS;EACrD,IAAI,WAAW,MAAM;GACnB,0BAAU,IAAI,IAAI;GAClB,KAAK,qBAAqB,IAAI,WAAW,OAAO;EAClD;EACA,QAAQ,IAAI,qBAAqB,GAAG,GAAG,MAAM;CAC/C;CAEA,yBACE,WACA,KACM;EACN,MAAM,UAAU,KAAK,qBAAqB,IAAI,SAAS;EACvD,IAAI,WAAW,MACb;EAEF,QAAQ,OAAO,qBAAqB,GAAG,CAAC;EACxC,IAAI,QAAQ,SAAS,GACnB,KAAK,qBAAqB,OAAO,SAAS;CAE9C;CAEA,6BACE,WACA,gBAC8B;EAC9B,MAAM,UAAU,KAAK,qBAAqB,IAAI,SAAS;EACvD,IAAI,WAAW,MACb,OAAO,CAAC;EAEV,MAAM,YAA0C,CAAC;EACjD,KAAK,MAAM,CAAC,eAAe,WAAW,SAAS;GAC7C,MAAM,MAAM,uBAAuB,aAAa;GAChD,IAAI,OAAO,QAAQ,IAAI,mBAAmB,gBACxC;GAEF,UAAU,KAAK;IAAE;IAAK;GAAO,CAAC;EAChC;EACA,OAAO;CACT;CAEA,4BACE,WACA,sBACA,WACM;EACN,MAAM,2BAAW,IAAI,IAAkC;EACvD,KAAK,MAAM,CAAC,eAAe,WAAW,KAAK,qBAAqB,IAC9D,SACF,KAAK,CAAC,GAEJ,IADY,uBAAuB,aAC7B,CAAC,EAAE,mBAAmB,sBAC1B,SAAS,IAAI,eAAe,MAAM;EAGtC,KAAK,MAAM,YAAY,WACrB,SAAS,IACP,qBAAqB;GACnB,GAAG,SAAS;GACZ,gBAAgB;EAClB,CAAC,GACD,SAAS,MACX;EAEF,IAAI,SAAS,SAAS,GAAG;GACvB,KAAK,qBAAqB,OAAO,SAAS;GAC1C;EACF;EACA,KAAK,qBAAqB,IAAI,WAAW,QAAQ;CACnD;;;;;;;;;;;;;;;;;;;;CAqBA,QAAQ,WAAmB,QAAgB,QAAyB;EAClE,IAAI,KAAK,YAAY,IAAI,SAAS,GAChC;EAEF,KAAK,YAAY,IAAI,WAAW;GAAE;GAAQ;EAAO,CAAC;CACpD;;;;;;CAOA,cAAc,WAA+C;EAC3D,OAAO,KAAK,YAAY,IAAI,SAAS;CACvC;;;;;;;CAQA,gBAAgB,WAAyB;EACvC,KAAK,YAAY,OAAO,SAAS;CACnC;;;;;;;;;;CAWA,uBAAuB,WAA6B;EAClD,IAAI,0BAA0B,KAAK,MAAM,GACvC,OAAO;EAET,IAAI,cAAc,KAAA,GAAW;GAC3B,MAAM,SAAS,KAAK,SAAS,IAAI,SAAS;GAC1C,OAAO,WAAW,KAAA,KAAa,0BAA0B,MAAM;EACjE;EACA,KAAK,MAAM,UAAU,KAAK,SAAS,OAAO,GACxC,IAAI,0BAA0B,MAAM,GAClC,OAAO;EAGX,OAAO;CACT;;CAGA,WAAW,OAAkB,WAA6B;EACxD,IAAI,SAAS,KAAK,QAAQ,KAAK,CAAC,CAAC,SAAS,GACxC,OAAO;EAET,IAAI,cAAc,KAAA,GAChB,OAAO;EAET,MAAM,SAAS,KAAK,SAAS,IAAI,SAAS;EAC1C,IAAI,WAAW,KAAA,GACb,OAAO;EAET,OAAO,SAAS,QAAQ,KAAK,CAAC,CAAC,SAAS;CAC1C;;;;;;;;;;;;;;;CAgBA,uBAAuB,OAAkB,WAA6B;EACpE,IAAI,sBAAsB,SAAS,KAAK,QAAQ,KAAK,CAAC,GACpD,OAAO;EAET,IAAI,cAAc,KAAA,GAChB,OAAO;EAET,MAAM,SAAS,KAAK,SAAS,IAAI,SAAS;EAC1C,IAAI,WAAW,KAAA,GACb,OAAO;EAET,OAAO,sBAAsB,SAAS,QAAQ,KAAK,CAAC;CACtD;CAEA,oBAA4B,WAAkC;EAC5D,MAAM,WAAW,KAAK,SAAS,IAAI,SAAS;EAC5C,IAAI,aAAa,KAAA,GACf,OAAO;EAET,MAAM,QAAuB,CAAC;EAC9B,KAAK,SAAS,IAAI,WAAW,KAAK;EAClC,OAAO;CACT;AACF;AAEA,SAAS,WACP,QACA,OAC0B;CAC1B,MAAM,WAAW,OAAO;CACxB,IAAI,aAAa,KAAA,GACf,OAAO;CAET,MAAM,QAAkC,CAAC;CACzC,OAAO,SAAS;CAChB,OAAO;AACT;AAEA,SAAS,SACP,QACA,OAC0B;CAC1B,OAAO,OAAO,UAAU,CAAC;AAC3B;AAEA,SAAS,sBAAsB,MAAyC;CACtE,KAAK,MAAM,WAAW,MACpB,KACG,QAAQ,YAAY,KAAA,KAAa,QAAQ,YAAY,OACtD,QAAQ,MAAM,SAAS,GAEvB,OAAO;CAGX,OAAO;AACT;AAEA,SAAS,0BAA0B,QAAgC;CACjE,KAAK,MAAM,SAAS,6BAClB,IAAI,SAAS,QAAQ,KAAK,CAAC,CAAC,SAAS,GACnC,OAAO;CAGX,OAAO;AACT;AAEA,SAAS,eACP,MACA,SACS;CACT,MAAM,MAAM,KAAK,QAAQ,MAAM,OAAO,CAAC;CACvC,IAAI,MAAM,GACR,OAAO;CAET,KAAK,OAAO,KAAK,CAAC;CAClB,OAAO;AACT;;;;;;;;AASA,SAAS,MACP,SACwB;CACxB,OAAO;AACT;;;;;;AAOA,SAAS,SACP,MACkB;CAClB,OAAO,KAAK,MAAM;AACpB"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require("../common/enum.cjs");
|
|
2
2
|
require("../common/index.cjs");
|
|
3
|
-
let path = require("path");
|
|
4
3
|
let os = require("os");
|
|
5
4
|
let fs_promises = require("fs/promises");
|
|
5
|
+
let path = require("path");
|
|
6
6
|
//#region src/hooks/createWorkspacePolicyHook.ts
|
|
7
7
|
/**
|
|
8
8
|
* Workspace boundary policy as a `PreToolUse` hook.
|
|
@@ -148,11 +148,45 @@ function applyAllowedDecisions(agg, output) {
|
|
|
148
148
|
if (!("allowedDecisions" in output) || output.allowedDecisions === void 0) return;
|
|
149
149
|
agg.allowedDecisions = output.allowedDecisions;
|
|
150
150
|
}
|
|
151
|
+
function applyAggregatedResult(target, source) {
|
|
152
|
+
target.additionalContexts.push(...source.additionalContexts);
|
|
153
|
+
target.injectedMessages.push(...source.injectedMessages);
|
|
154
|
+
target.errors.push(...source.errors);
|
|
155
|
+
if (source.decision != null) applyToolDecision(target, source.decision, source.reason);
|
|
156
|
+
if (source.stopDecision != null) applyStopDecision(target, source.stopDecision, source.reason);
|
|
157
|
+
if (source.updatedInput != null) target.updatedInput = source.updatedInput;
|
|
158
|
+
if (source.updatedOutput !== void 0) target.updatedOutput = source.updatedOutput;
|
|
159
|
+
if (source.allowedDecisions != null) target.allowedDecisions = source.allowedDecisions;
|
|
160
|
+
if (source.preventContinuation === true) {
|
|
161
|
+
target.preventContinuation = true;
|
|
162
|
+
target.stopReason ??= source.stopReason;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function combineAggregatedResults(first, second) {
|
|
166
|
+
if (first == null) return second;
|
|
167
|
+
const combined = freshResult();
|
|
168
|
+
applyAggregatedResult(combined, first);
|
|
169
|
+
applyAggregatedResult(combined, second);
|
|
170
|
+
return combined;
|
|
171
|
+
}
|
|
172
|
+
function createPendingApprovalReplay(approvalResult, pendingApproval, onceContribution) {
|
|
173
|
+
const replay = freshResult();
|
|
174
|
+
if (pendingApproval != null) applyAggregatedResult(replay, pendingApproval);
|
|
175
|
+
applyAggregatedResult(replay, onceContribution);
|
|
176
|
+
replay.decision = "ask";
|
|
177
|
+
replay.reason = approvalResult.reason;
|
|
178
|
+
if (approvalResult.allowedDecisions == null) delete replay.allowedDecisions;
|
|
179
|
+
else replay.allowedDecisions = approvalResult.allowedDecisions;
|
|
180
|
+
return replay;
|
|
181
|
+
}
|
|
151
182
|
function fold(outcomes) {
|
|
152
|
-
const
|
|
183
|
+
const aggregated = freshResult();
|
|
184
|
+
const onceContribution = freshResult();
|
|
185
|
+
let hasOnceContribution = false;
|
|
153
186
|
for (const outcome of outcomes) {
|
|
187
|
+
const isOnceMatcher = outcome.matcher.once === true;
|
|
154
188
|
if (outcome.error !== null) {
|
|
155
|
-
if (outcome.matcher.internal !== true)
|
|
189
|
+
if (outcome.matcher.internal !== true) aggregated.errors.push(outcome.error);
|
|
156
190
|
continue;
|
|
157
191
|
}
|
|
158
192
|
const output = outcome.output;
|
|
@@ -164,15 +198,22 @@ function fold(outcomes) {
|
|
|
164
198
|
* just doesn't fold into the aggregate result.
|
|
165
199
|
*/
|
|
166
200
|
if (output.async === true) continue;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
201
|
+
const targets = isOnceMatcher ? [aggregated, onceContribution] : [aggregated];
|
|
202
|
+
if (isOnceMatcher) hasOnceContribution = true;
|
|
203
|
+
for (const target of targets) {
|
|
204
|
+
applyContext(target, output);
|
|
205
|
+
applyInjectedMessages(target, output);
|
|
206
|
+
applyStopFlag(target, output);
|
|
207
|
+
applyDecision(target, output);
|
|
208
|
+
applyUpdatedInput(target, output);
|
|
209
|
+
applyUpdatedOutput(target, output);
|
|
210
|
+
applyAllowedDecisions(target, output);
|
|
211
|
+
}
|
|
174
212
|
}
|
|
175
|
-
return
|
|
213
|
+
return {
|
|
214
|
+
aggregated,
|
|
215
|
+
...hasOnceContribution ? { onceReplayContribution: onceContribution } : {}
|
|
216
|
+
};
|
|
176
217
|
}
|
|
177
218
|
/**
|
|
178
219
|
* Fires every matcher registered against `input.hook_event_name`, folding
|
|
@@ -229,10 +270,12 @@ function fold(outcomes) {
|
|
|
229
270
|
* via the `unregister` callback returned from `registry.register`.
|
|
230
271
|
*/
|
|
231
272
|
async function executeHooks(opts) {
|
|
232
|
-
const { registry, input, sessionId, matchQuery, signal, timeoutMs = DEFAULT_HOOK_TIMEOUT_MS, logger } = opts;
|
|
273
|
+
const { registry, input, sessionId, matchQuery, onceReplayKey, onceReplaySessionId, signal, timeoutMs = DEFAULT_HOOK_TIMEOUT_MS, logger } = opts;
|
|
233
274
|
const event = input.hook_event_name;
|
|
275
|
+
const replaySessionId = onceReplaySessionId ?? sessionId;
|
|
276
|
+
const pendingApproval = event === "PreToolUse" && replaySessionId != null && onceReplayKey != null ? registry.getPendingToolApproval(replaySessionId, onceReplayKey) : void 0;
|
|
234
277
|
const matchers = registry.getMatchers(event, sessionId);
|
|
235
|
-
if (matchers.length === 0) return freshResult();
|
|
278
|
+
if (matchers.length === 0) return pendingApproval ?? freshResult();
|
|
236
279
|
const tasks = [];
|
|
237
280
|
for (const matcher of matchers) {
|
|
238
281
|
if (!require_matchers.matchesQuery(matcher.pattern, matchQuery)) continue;
|
|
@@ -241,10 +284,13 @@ async function executeHooks(opts) {
|
|
|
241
284
|
const matcherSignal = combineSignals(signal, matcher.timeout ?? timeoutMs);
|
|
242
285
|
tasks.push(runMatcherHooks(matcher, input, matcherSignal));
|
|
243
286
|
}
|
|
244
|
-
if (tasks.length === 0) return freshResult();
|
|
287
|
+
if (tasks.length === 0) return pendingApproval ?? freshResult();
|
|
245
288
|
const outcomes = (await Promise.all(tasks)).flat();
|
|
246
289
|
reportErrors(outcomes, event, logger);
|
|
247
|
-
const aggregated = fold(outcomes);
|
|
290
|
+
const { aggregated: liveResult, onceReplayContribution } = fold(outcomes);
|
|
291
|
+
const aggregated = combineAggregatedResults(pendingApproval, liveResult);
|
|
292
|
+
if (event === "PreToolUse" && aggregated.decision === "ask" && onceReplayContribution != null && replaySessionId != null && onceReplayKey != null) registry.setPendingToolApproval(replaySessionId, onceReplayKey, createPendingApprovalReplay(aggregated, pendingApproval, onceReplayContribution));
|
|
293
|
+
else if (event === "PreToolUse" && aggregated.decision !== "ask" && pendingApproval != null && replaySessionId != null && onceReplayKey != null) registry.clearPendingToolApproval(replaySessionId, onceReplayKey);
|
|
248
294
|
/**
|
|
249
295
|
* Centralized `preventContinuation` propagation: when any hook (across
|
|
250
296
|
* any callsite — RunStart, PreToolUse, PostToolBatch, SubagentStop,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executeHooks.cjs","names":["matchesQuery"],"sources":["../../../src/hooks/executeHooks.ts"],"sourcesContent":["// src/hooks/executeHooks.ts\nimport type { Logger } from 'winston';\nimport type {\n HookInput,\n HookEvent,\n HookOutput,\n HookMatcher,\n ToolDecision,\n StopDecision,\n HookCallback,\n AggregatedHookResult,\n} from './types';\nimport type { HookRegistry } from './HookRegistry';\nimport { matchesQuery } from './matchers';\n\n/** Default per-hook timeout when a matcher doesn't set its own. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 30_000;\n\n/**\n * Options for a single `executeHooks` call. The `input` drives everything —\n * the event name is read from `input.hook_event_name`, matchers are looked\n * up against that event, and each hook receives `input` directly.\n */\nexport interface ExecuteHooksOptions {\n registry: HookRegistry;\n input: HookInput;\n /** Scope lookup to this session (in addition to global matchers). */\n sessionId?: string;\n /** Query string matched against each matcher's pattern (tool name, etc.). */\n matchQuery?: string;\n /** Parent AbortSignal — combined with per-hook timeout into the hook signal. */\n signal?: AbortSignal;\n /** Default per-hook timeout; overridden by `matcher.timeout` when present. */\n timeoutMs?: number;\n /** Optional winston logger for non-internal hook errors. */\n logger?: Logger;\n}\n\ntype WideMatcher = HookMatcher<HookEvent>;\ntype WideCallback = HookCallback<HookEvent>;\n\ninterface HookOutcome {\n matcher: WideMatcher;\n output: HookOutput | null;\n error: string | null;\n timedOut: boolean;\n}\n\ninterface AbortRace {\n promise: Promise<never>;\n cleanup: () => void;\n}\n\nfunction freshResult(): AggregatedHookResult {\n return {\n additionalContexts: [],\n injectedMessages: [],\n errors: [],\n };\n}\n\nfunction combineSignals(\n parent: AbortSignal | undefined,\n timeoutMs: number\n): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (parent === undefined) {\n return timeoutSignal;\n }\n return AbortSignal.any([parent, timeoutSignal]);\n}\n\nfunction isTimeout(err: unknown): boolean {\n if (err instanceof Error) {\n return err.name === 'TimeoutError' || err.name === 'AbortError';\n }\n return false;\n}\n\nfunction describeError(err: unknown): string {\n if (err instanceof Error) {\n return err.message !== '' ? err.message : err.name;\n }\n return String(err);\n}\n\nfunction makeAbortPromise(signal: AbortSignal): {\n promise: Promise<never>;\n cleanup: () => void;\n} {\n let onAbort: (() => void) | undefined;\n const promise = new Promise<never>((_resolve, reject) => {\n if (signal.aborted) {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n return;\n }\n onAbort = (): void => {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n const cleanup = (): void => {\n if (onAbort !== undefined) {\n signal.removeEventListener('abort', onAbort);\n onAbort = undefined;\n }\n };\n return { promise, cleanup };\n}\n\nasync function runHook(\n hook: WideCallback,\n input: HookInput,\n signal: AbortSignal,\n abortPromise: Promise<never>,\n matcher: WideMatcher\n): Promise<HookOutcome> {\n const hookPromise = Promise.resolve().then(() => hook(input, signal));\n try {\n const output = await Promise.race([hookPromise, abortPromise]);\n return { matcher, output, error: null, timedOut: false };\n } catch (err) {\n return {\n matcher,\n output: null,\n error: describeError(err),\n timedOut: isTimeout(err),\n };\n }\n}\n\nasync function runMatcherHooks(\n matcher: WideMatcher,\n input: HookInput,\n signal: AbortSignal\n): Promise<HookOutcome[]> {\n const abortRace: AbortRace = makeAbortPromise(signal);\n const tasks = matcher.hooks.map((hook) =>\n runHook(hook, input, signal, abortRace.promise, matcher)\n );\n try {\n return await Promise.all(tasks);\n } finally {\n abortRace.cleanup();\n }\n}\n\nfunction reportErrors(\n outcomes: readonly HookOutcome[],\n event: HookEvent,\n logger: Logger | undefined\n): void {\n for (const outcome of outcomes) {\n if (outcome.error === null) {\n continue;\n }\n if (outcome.matcher.internal === true) {\n continue;\n }\n const label = outcome.timedOut ? 'timed out' : 'threw an error';\n const message = `Hook for ${event} ${label}: ${outcome.error}`;\n if (logger !== undefined) {\n logger.warn(message);\n continue;\n }\n // eslint-disable-next-line no-console\n console.warn(message);\n }\n}\n\nfunction applyToolDecision(\n agg: AggregatedHookResult,\n decision: ToolDecision,\n reason: string | undefined\n): void {\n if (decision === 'deny') {\n if (agg.decision === 'deny') {\n return;\n }\n agg.decision = 'deny';\n agg.reason = reason;\n return;\n }\n if (decision === 'ask') {\n if (agg.decision === 'deny' || agg.decision === 'ask') {\n return;\n }\n agg.decision = 'ask';\n agg.reason = reason;\n return;\n }\n if (agg.decision === undefined) {\n agg.decision = 'allow';\n agg.reason = reason;\n }\n}\n\nfunction applyStopDecision(\n agg: AggregatedHookResult,\n decision: StopDecision,\n reason: string | undefined\n): void {\n if (decision === 'block') {\n if (agg.stopDecision === 'block') {\n return;\n }\n agg.stopDecision = 'block';\n agg.reason = reason;\n return;\n }\n if (agg.stopDecision === undefined) {\n agg.stopDecision = 'continue';\n if (agg.reason === undefined) {\n agg.reason = reason;\n }\n }\n}\n\nfunction applyDecision(agg: AggregatedHookResult, output: HookOutput): void {\n if (!('decision' in output) || output.decision === undefined) {\n return;\n }\n const decision = output.decision;\n const reason =\n 'reason' in output && typeof output.reason === 'string'\n ? output.reason\n : undefined;\n if (decision === 'deny' || decision === 'ask' || decision === 'allow') {\n applyToolDecision(agg, decision, reason);\n return;\n }\n applyStopDecision(agg, decision, reason);\n}\n\nfunction applyContext(agg: AggregatedHookResult, output: HookOutput): void {\n if (\n typeof output.additionalContext === 'string' &&\n output.additionalContext.length > 0\n ) {\n agg.additionalContexts.push(output.additionalContext);\n }\n}\n\nfunction applyInjectedMessages(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (\n output.injectedMessages === undefined ||\n output.injectedMessages.length === 0\n ) {\n return;\n }\n agg.injectedMessages.push(...output.injectedMessages);\n}\n\nfunction applyStopFlag(agg: AggregatedHookResult, output: HookOutput): void {\n if (output.preventContinuation !== true) {\n return;\n }\n agg.preventContinuation = true;\n if (typeof output.stopReason === 'string' && agg.stopReason === undefined) {\n agg.stopReason = output.stopReason;\n }\n}\n\nfunction applyUpdatedInput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedInput' in output) || output.updatedInput === undefined) {\n return;\n }\n agg.updatedInput = output.updatedInput;\n}\n\nfunction applyUpdatedOutput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedOutput' in output) || output.updatedOutput === undefined) {\n return;\n }\n agg.updatedOutput = output.updatedOutput;\n}\n\nfunction applyAllowedDecisions(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (\n !('allowedDecisions' in output) ||\n output.allowedDecisions === undefined\n ) {\n return;\n }\n agg.allowedDecisions = output.allowedDecisions;\n}\n\nfunction fold(outcomes: readonly HookOutcome[]): AggregatedHookResult {\n const agg = freshResult();\n for (const outcome of outcomes) {\n if (outcome.error !== null) {\n if (outcome.matcher.internal !== true) {\n agg.errors.push(outcome.error);\n }\n continue;\n }\n const output = outcome.output;\n if (output === null) {\n continue;\n }\n /**\n * Skip fire-and-forget outputs entirely: the agent has already\n * moved on, so an async hook cannot influence the run. Background\n * work inside the hook body still runs (we don't cancel it), it\n * just doesn't fold into the aggregate result.\n */\n if (output.async === true) {\n continue;\n }\n applyContext(agg, output);\n applyInjectedMessages(agg, output);\n applyStopFlag(agg, output);\n applyDecision(agg, output);\n applyUpdatedInput(agg, output);\n applyUpdatedOutput(agg, output);\n applyAllowedDecisions(agg, output);\n }\n return agg;\n}\n\n/**\n * Fires every matcher registered against `input.hook_event_name`, folding\n * their results per `deny > ask > allow` precedence and accumulating\n * context/errors.\n *\n * ## Parallelism and determinism\n *\n * All matching hooks fire simultaneously and are awaited via `Promise.all`,\n * which preserves input-array order in its returned results. The fold\n * therefore iterates outcomes in **registration order** — outer loop over\n * matchers as they sit in the registry (global first, then session), inner\n * loop over each matcher's `hooks` array. Last-writer-wins fields\n * (`updatedInput`, `updatedOutput`) are deterministic in that order, even\n * though hooks may complete in arbitrary wall-clock order.\n *\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput`/`updatedOutput` to one hook per matcher to avoid subtle\n * precedence bugs when matchers are added in a different order than\n * expected.\n *\n * ## Timeouts and cancellation\n *\n * Each matcher receives **one shared `AbortSignal`** derived from the\n * caller's parent signal combined with `matcher.timeout` (falling back to\n * `opts.timeoutMs`, default {@link DEFAULT_HOOK_TIMEOUT_MS}). Sharing the\n * signal across hooks in a matcher collapses N timer allocations into\n * one, which matters on the PreToolUse hot path where a matcher with\n * several hooks fires on every tool call. Each hook call is raced\n * against the shared signal, so even a hook that ignores the signal is\n * force-unblocked when the timeout fires. Timeout/abort errors are\n * swallowed into the aggregated result's `errors` array (non-fatal by\n * default).\n *\n * ## Internal matchers\n *\n * A matcher with `internal: true` is excluded from both the `errors` array\n * and the logger output. Use it for infrastructure hooks whose failures\n * should not pollute user-visible diagnostics.\n *\n * ## Once semantics — atomic at-most-once\n *\n * A matcher with `once: true` is removed from the registry **before any\n * hook runs**, inside the synchronous prefix of `executeHooks` (between\n * `getMatchers` and the first `await`). Because Node's event loop serialises\n * sync work, two concurrent `executeHooks` calls can never both observe\n * and dispatch the same `once` matcher — whichever call runs its sync\n * prefix first consumes it, and the loser sees an empty bucket.\n *\n * Trade-off: if every hook in a `once` matcher throws, the matcher is\n * still gone. \"Once\" here means \"at most one dispatch, ever\", not \"at\n * most one successful execution with retry on failure\". Hosts that need\n * retry semantics should register a normal matcher and self-unregister\n * via the `unregister` callback returned from `registry.register`.\n */\nexport async function executeHooks(\n opts: ExecuteHooksOptions\n): Promise<AggregatedHookResult> {\n const {\n registry,\n input,\n sessionId,\n matchQuery,\n signal,\n timeoutMs = DEFAULT_HOOK_TIMEOUT_MS,\n logger,\n } = opts;\n const event = input.hook_event_name;\n const matchers = registry.getMatchers(event, sessionId);\n if (matchers.length === 0) {\n return freshResult();\n }\n\n // --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---\n const tasks: Promise<HookOutcome[]>[] = [];\n for (const matcher of matchers) {\n if (!matchesQuery(matcher.pattern, matchQuery)) {\n continue;\n }\n if (matcher.once === true) {\n registry.removeMatcher(event, matcher, sessionId);\n }\n if (matcher.hooks.length === 0) {\n continue;\n }\n const perHookTimeout = matcher.timeout ?? timeoutMs;\n const matcherSignal = combineSignals(signal, perHookTimeout);\n tasks.push(runMatcherHooks(matcher, input, matcherSignal));\n }\n // --- END SYNC CRITICAL SECTION ---\n if (tasks.length === 0) {\n return freshResult();\n }\n\n const outcomes = (await Promise.all(tasks)).flat();\n reportErrors(outcomes, event, logger);\n const aggregated = fold(outcomes);\n /**\n * Centralized `preventContinuation` propagation: when any hook (across\n * any callsite — RunStart, PreToolUse, PostToolBatch, SubagentStop,\n * etc.) returns `preventContinuation: true`, raise a halt signal on\n * the registry scoped to the run's `sessionId`. `Run.processStream`\n * polls the signal between stream events using its own id and exits\n * cleanly, skipping the `Stop` hook (since the run is being halted,\n * not naturally completing).\n *\n * First-write-wins per session inside the registry — a halt already\n * raised by an earlier hook in the same run is preserved so the\n * original `reason` / `source` are not clobbered. Hooks fired\n * without a `sessionId` cannot raise a halt (there's no run for the\n * loop to poll under), which is fine: every in-tree callsite passes\n * `sessionId: runId`. Pre-stream callsites in `Run.processStream`\n * still read `preventContinuation` directly off the result for an\n * early return because they have not yet entered the stream loop.\n */\n if (aggregated.preventContinuation === true && sessionId !== undefined) {\n registry.haltRun(\n sessionId,\n aggregated.stopReason ?? 'preventContinuation',\n event\n );\n }\n return aggregated;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"executeHooks.cjs","names":["matchesQuery"],"sources":["../../../src/hooks/executeHooks.ts"],"sourcesContent":["// src/hooks/executeHooks.ts\nimport type { Logger } from 'winston';\nimport type {\n HookInput,\n HookEvent,\n HookOutput,\n HookMatcher,\n ToolDecision,\n StopDecision,\n HookCallback,\n ToolApprovalReplayKey,\n AggregatedHookResult,\n} from './types';\nimport type { HookRegistry } from './HookRegistry';\nimport { matchesQuery } from './matchers';\n\n/** Default per-hook timeout when a matcher doesn't set its own. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 30_000;\n\n/**\n * Options for a single `executeHooks` call. The `input` drives everything —\n * the event name is read from `input.hook_event_name`, matchers are looked\n * up against that event, and each hook receives `input` directly.\n */\nexport interface ExecuteHooksOptions {\n registry: HookRegistry;\n input: HookInput;\n /** Scope lookup to this session (in addition to global matchers). */\n sessionId?: string;\n /** Query string matched against each matcher's pattern (tool name, etc.). */\n matchQuery?: string;\n /** Stable tool-call key for replaying consumed one-shot contributions. */\n onceReplayKey?: ToolApprovalReplayKey;\n /** Storage scope for replay state when it must outlive or branch from the hook session. */\n onceReplaySessionId?: string;\n /** Parent AbortSignal — combined with per-hook timeout into the hook signal. */\n signal?: AbortSignal;\n /** Default per-hook timeout; overridden by `matcher.timeout` when present. */\n timeoutMs?: number;\n /** Optional winston logger for non-internal hook errors. */\n logger?: Logger;\n}\n\ntype WideMatcher = HookMatcher<HookEvent>;\ntype WideCallback = HookCallback<HookEvent>;\n\ninterface HookOutcome {\n matcher: WideMatcher;\n output: HookOutput | null;\n error: string | null;\n timedOut: boolean;\n}\n\ninterface AbortRace {\n promise: Promise<never>;\n cleanup: () => void;\n}\n\nfunction freshResult(): AggregatedHookResult {\n return {\n additionalContexts: [],\n injectedMessages: [],\n errors: [],\n };\n}\n\nfunction combineSignals(\n parent: AbortSignal | undefined,\n timeoutMs: number\n): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (parent === undefined) {\n return timeoutSignal;\n }\n return AbortSignal.any([parent, timeoutSignal]);\n}\n\nfunction isTimeout(err: unknown): boolean {\n if (err instanceof Error) {\n return err.name === 'TimeoutError' || err.name === 'AbortError';\n }\n return false;\n}\n\nfunction describeError(err: unknown): string {\n if (err instanceof Error) {\n return err.message !== '' ? err.message : err.name;\n }\n return String(err);\n}\n\nfunction makeAbortPromise(signal: AbortSignal): {\n promise: Promise<never>;\n cleanup: () => void;\n} {\n let onAbort: (() => void) | undefined;\n const promise = new Promise<never>((_resolve, reject) => {\n if (signal.aborted) {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n return;\n }\n onAbort = (): void => {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n const cleanup = (): void => {\n if (onAbort !== undefined) {\n signal.removeEventListener('abort', onAbort);\n onAbort = undefined;\n }\n };\n return { promise, cleanup };\n}\n\nasync function runHook(\n hook: WideCallback,\n input: HookInput,\n signal: AbortSignal,\n abortPromise: Promise<never>,\n matcher: WideMatcher\n): Promise<HookOutcome> {\n const hookPromise = Promise.resolve().then(() => hook(input, signal));\n try {\n const output = await Promise.race([hookPromise, abortPromise]);\n return { matcher, output, error: null, timedOut: false };\n } catch (err) {\n return {\n matcher,\n output: null,\n error: describeError(err),\n timedOut: isTimeout(err),\n };\n }\n}\n\nasync function runMatcherHooks(\n matcher: WideMatcher,\n input: HookInput,\n signal: AbortSignal\n): Promise<HookOutcome[]> {\n const abortRace: AbortRace = makeAbortPromise(signal);\n const tasks = matcher.hooks.map((hook) =>\n runHook(hook, input, signal, abortRace.promise, matcher)\n );\n try {\n return await Promise.all(tasks);\n } finally {\n abortRace.cleanup();\n }\n}\n\nfunction reportErrors(\n outcomes: readonly HookOutcome[],\n event: HookEvent,\n logger: Logger | undefined\n): void {\n for (const outcome of outcomes) {\n if (outcome.error === null) {\n continue;\n }\n if (outcome.matcher.internal === true) {\n continue;\n }\n const label = outcome.timedOut ? 'timed out' : 'threw an error';\n const message = `Hook for ${event} ${label}: ${outcome.error}`;\n if (logger !== undefined) {\n logger.warn(message);\n continue;\n }\n // eslint-disable-next-line no-console\n console.warn(message);\n }\n}\n\nfunction applyToolDecision(\n agg: AggregatedHookResult,\n decision: ToolDecision,\n reason: string | undefined\n): void {\n if (decision === 'deny') {\n if (agg.decision === 'deny') {\n return;\n }\n agg.decision = 'deny';\n agg.reason = reason;\n return;\n }\n if (decision === 'ask') {\n if (agg.decision === 'deny' || agg.decision === 'ask') {\n return;\n }\n agg.decision = 'ask';\n agg.reason = reason;\n return;\n }\n if (agg.decision === undefined) {\n agg.decision = 'allow';\n agg.reason = reason;\n }\n}\n\nfunction applyStopDecision(\n agg: AggregatedHookResult,\n decision: StopDecision,\n reason: string | undefined\n): void {\n if (decision === 'block') {\n if (agg.stopDecision === 'block') {\n return;\n }\n agg.stopDecision = 'block';\n agg.reason = reason;\n return;\n }\n if (agg.stopDecision === undefined) {\n agg.stopDecision = 'continue';\n if (agg.reason === undefined) {\n agg.reason = reason;\n }\n }\n}\n\nfunction applyDecision(agg: AggregatedHookResult, output: HookOutput): void {\n if (!('decision' in output) || output.decision === undefined) {\n return;\n }\n const decision = output.decision;\n const reason =\n 'reason' in output && typeof output.reason === 'string'\n ? output.reason\n : undefined;\n if (decision === 'deny' || decision === 'ask' || decision === 'allow') {\n applyToolDecision(agg, decision, reason);\n return;\n }\n applyStopDecision(agg, decision, reason);\n}\n\nfunction applyContext(agg: AggregatedHookResult, output: HookOutput): void {\n if (\n typeof output.additionalContext === 'string' &&\n output.additionalContext.length > 0\n ) {\n agg.additionalContexts.push(output.additionalContext);\n }\n}\n\nfunction applyInjectedMessages(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (\n output.injectedMessages === undefined ||\n output.injectedMessages.length === 0\n ) {\n return;\n }\n agg.injectedMessages.push(...output.injectedMessages);\n}\n\nfunction applyStopFlag(agg: AggregatedHookResult, output: HookOutput): void {\n if (output.preventContinuation !== true) {\n return;\n }\n agg.preventContinuation = true;\n if (typeof output.stopReason === 'string' && agg.stopReason === undefined) {\n agg.stopReason = output.stopReason;\n }\n}\n\nfunction applyUpdatedInput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedInput' in output) || output.updatedInput === undefined) {\n return;\n }\n agg.updatedInput = output.updatedInput;\n}\n\nfunction applyUpdatedOutput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedOutput' in output) || output.updatedOutput === undefined) {\n return;\n }\n agg.updatedOutput = output.updatedOutput;\n}\n\nfunction applyAllowedDecisions(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (\n !('allowedDecisions' in output) ||\n output.allowedDecisions === undefined\n ) {\n return;\n }\n agg.allowedDecisions = output.allowedDecisions;\n}\n\nfunction applyAggregatedResult(\n target: AggregatedHookResult,\n source: AggregatedHookResult\n): void {\n target.additionalContexts.push(...source.additionalContexts);\n target.injectedMessages.push(...source.injectedMessages);\n target.errors.push(...source.errors);\n if (source.decision != null) {\n applyToolDecision(target, source.decision, source.reason);\n }\n if (source.stopDecision != null) {\n applyStopDecision(target, source.stopDecision, source.reason);\n }\n if (source.updatedInput != null) {\n target.updatedInput = source.updatedInput;\n }\n if (source.updatedOutput !== undefined) {\n target.updatedOutput = source.updatedOutput;\n }\n if (source.allowedDecisions != null) {\n target.allowedDecisions = source.allowedDecisions;\n }\n if (source.preventContinuation === true) {\n target.preventContinuation = true;\n target.stopReason ??= source.stopReason;\n }\n}\n\nfunction combineAggregatedResults(\n first: AggregatedHookResult | undefined,\n second: AggregatedHookResult\n): AggregatedHookResult {\n if (first == null) {\n return second;\n }\n const combined = freshResult();\n applyAggregatedResult(combined, first);\n applyAggregatedResult(combined, second);\n return combined;\n}\n\nfunction createPendingApprovalReplay(\n approvalResult: AggregatedHookResult,\n pendingApproval: AggregatedHookResult | undefined,\n onceContribution: AggregatedHookResult\n): AggregatedHookResult {\n const replay = freshResult();\n if (pendingApproval != null) {\n applyAggregatedResult(replay, pendingApproval);\n }\n applyAggregatedResult(replay, onceContribution);\n replay.decision = 'ask';\n replay.reason = approvalResult.reason;\n if (approvalResult.allowedDecisions == null) {\n delete replay.allowedDecisions;\n } else {\n replay.allowedDecisions = approvalResult.allowedDecisions;\n }\n return replay;\n}\n\nfunction fold(outcomes: readonly HookOutcome[]): {\n aggregated: AggregatedHookResult;\n onceReplayContribution?: AggregatedHookResult;\n} {\n const aggregated = freshResult();\n const onceContribution = freshResult();\n let hasOnceContribution = false;\n for (const outcome of outcomes) {\n const isOnceMatcher = outcome.matcher.once === true;\n if (outcome.error !== null) {\n if (outcome.matcher.internal !== true) {\n aggregated.errors.push(outcome.error);\n }\n continue;\n }\n const output = outcome.output;\n if (output === null) {\n continue;\n }\n /**\n * Skip fire-and-forget outputs entirely: the agent has already\n * moved on, so an async hook cannot influence the run. Background\n * work inside the hook body still runs (we don't cancel it), it\n * just doesn't fold into the aggregate result.\n */\n if (output.async === true) {\n continue;\n }\n const targets = isOnceMatcher\n ? [aggregated, onceContribution]\n : [aggregated];\n if (isOnceMatcher) {\n hasOnceContribution = true;\n }\n for (const target of targets) {\n applyContext(target, output);\n applyInjectedMessages(target, output);\n applyStopFlag(target, output);\n applyDecision(target, output);\n applyUpdatedInput(target, output);\n applyUpdatedOutput(target, output);\n applyAllowedDecisions(target, output);\n }\n }\n return {\n aggregated,\n ...(hasOnceContribution\n ? { onceReplayContribution: onceContribution }\n : {}),\n };\n}\n\n/**\n * Fires every matcher registered against `input.hook_event_name`, folding\n * their results per `deny > ask > allow` precedence and accumulating\n * context/errors.\n *\n * ## Parallelism and determinism\n *\n * All matching hooks fire simultaneously and are awaited via `Promise.all`,\n * which preserves input-array order in its returned results. The fold\n * therefore iterates outcomes in **registration order** — outer loop over\n * matchers as they sit in the registry (global first, then session), inner\n * loop over each matcher's `hooks` array. Last-writer-wins fields\n * (`updatedInput`, `updatedOutput`) are deterministic in that order, even\n * though hooks may complete in arbitrary wall-clock order.\n *\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput`/`updatedOutput` to one hook per matcher to avoid subtle\n * precedence bugs when matchers are added in a different order than\n * expected.\n *\n * ## Timeouts and cancellation\n *\n * Each matcher receives **one shared `AbortSignal`** derived from the\n * caller's parent signal combined with `matcher.timeout` (falling back to\n * `opts.timeoutMs`, default {@link DEFAULT_HOOK_TIMEOUT_MS}). Sharing the\n * signal across hooks in a matcher collapses N timer allocations into\n * one, which matters on the PreToolUse hot path where a matcher with\n * several hooks fires on every tool call. Each hook call is raced\n * against the shared signal, so even a hook that ignores the signal is\n * force-unblocked when the timeout fires. Timeout/abort errors are\n * swallowed into the aggregated result's `errors` array (non-fatal by\n * default).\n *\n * ## Internal matchers\n *\n * A matcher with `internal: true` is excluded from both the `errors` array\n * and the logger output. Use it for infrastructure hooks whose failures\n * should not pollute user-visible diagnostics.\n *\n * ## Once semantics — atomic at-most-once\n *\n * A matcher with `once: true` is removed from the registry **before any\n * hook runs**, inside the synchronous prefix of `executeHooks` (between\n * `getMatchers` and the first `await`). Because Node's event loop serialises\n * sync work, two concurrent `executeHooks` calls can never both observe\n * and dispatch the same `once` matcher — whichever call runs its sync\n * prefix first consumes it, and the loser sees an empty bucket.\n *\n * Trade-off: if every hook in a `once` matcher throws, the matcher is\n * still gone. \"Once\" here means \"at most one dispatch, ever\", not \"at\n * most one successful execution with retry on failure\". Hosts that need\n * retry semantics should register a normal matcher and self-unregister\n * via the `unregister` callback returned from `registry.register`.\n */\nexport async function executeHooks(\n opts: ExecuteHooksOptions\n): Promise<AggregatedHookResult> {\n const {\n registry,\n input,\n sessionId,\n matchQuery,\n onceReplayKey,\n onceReplaySessionId,\n signal,\n timeoutMs = DEFAULT_HOOK_TIMEOUT_MS,\n logger,\n } = opts;\n const event = input.hook_event_name;\n const replaySessionId = onceReplaySessionId ?? sessionId;\n const pendingApproval =\n event === 'PreToolUse' && replaySessionId != null && onceReplayKey != null\n ? registry.getPendingToolApproval(replaySessionId, onceReplayKey)\n : undefined;\n const matchers = registry.getMatchers(event, sessionId);\n if (matchers.length === 0) {\n return pendingApproval ?? freshResult();\n }\n\n // --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---\n const tasks: Promise<HookOutcome[]>[] = [];\n for (const matcher of matchers) {\n if (!matchesQuery(matcher.pattern, matchQuery)) {\n continue;\n }\n if (matcher.once === true) {\n registry.removeMatcher(event, matcher, sessionId);\n }\n if (matcher.hooks.length === 0) {\n continue;\n }\n const perHookTimeout = matcher.timeout ?? timeoutMs;\n const matcherSignal = combineSignals(signal, perHookTimeout);\n tasks.push(runMatcherHooks(matcher, input, matcherSignal));\n }\n // --- END SYNC CRITICAL SECTION ---\n if (tasks.length === 0) {\n return pendingApproval ?? freshResult();\n }\n\n const outcomes = (await Promise.all(tasks)).flat();\n reportErrors(outcomes, event, logger);\n const { aggregated: liveResult, onceReplayContribution } = fold(outcomes);\n const aggregated = combineAggregatedResults(pendingApproval, liveResult);\n if (\n event === 'PreToolUse' &&\n aggregated.decision === 'ask' &&\n onceReplayContribution != null &&\n replaySessionId != null &&\n onceReplayKey != null\n ) {\n registry.setPendingToolApproval(\n replaySessionId,\n onceReplayKey,\n createPendingApprovalReplay(\n aggregated,\n pendingApproval,\n onceReplayContribution\n )\n );\n } else if (\n event === 'PreToolUse' &&\n aggregated.decision !== 'ask' &&\n pendingApproval != null &&\n replaySessionId != null &&\n onceReplayKey != null\n ) {\n registry.clearPendingToolApproval(replaySessionId, onceReplayKey);\n }\n /**\n * Centralized `preventContinuation` propagation: when any hook (across\n * any callsite — RunStart, PreToolUse, PostToolBatch, SubagentStop,\n * etc.) returns `preventContinuation: true`, raise a halt signal on\n * the registry scoped to the run's `sessionId`. `Run.processStream`\n * polls the signal between stream events using its own id and exits\n * cleanly, skipping the `Stop` hook (since the run is being halted,\n * not naturally completing).\n *\n * First-write-wins per session inside the registry — a halt already\n * raised by an earlier hook in the same run is preserved so the\n * original `reason` / `source` are not clobbered. Hooks fired\n * without a `sessionId` cannot raise a halt (there's no run for the\n * loop to poll under), which is fine: every in-tree callsite passes\n * `sessionId: runId`. Pre-stream callsites in `Run.processStream`\n * still read `preventContinuation` directly off the result for an\n * early return because they have not yet entered the stream loop.\n */\n if (aggregated.preventContinuation === true && sessionId !== undefined) {\n registry.haltRun(\n sessionId,\n aggregated.stopReason ?? 'preventContinuation',\n event\n );\n }\n return aggregated;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/hooks/index.ts"],"sourcesContent":["// src/hooks/index.ts\n//\n// Hook lifecycle system for `@librechat/agents`. Re-exported from\n// `src/index.ts` and consumed by `Run.processStream` (RunStart,\n// UserPromptSubmit, Stop, StopFailure), `ToolNode.dispatchToolEvents`\n// (PreToolUse, PostToolUse, PostToolUseFailure, PermissionDenied),\n// `createSummarizeNode` (PreCompact, PostCompact),\n// `SubagentExecutor.execute` (SubagentStart, SubagentStop), and\n// `StandardGraph.createCallModel` (PreemptBoundary).\nexport { HookRegistry } from './HookRegistry';\nexport type { HookHaltSignal } from './HookRegistry';\nexport { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';\n/**\n * Feature probe for hosts: hook outputs support `injectedMessages`\n * (per-message graph-state injection at the `PostToolBatch` boundary).\n * Hosts must gate drain-style hooks on this so a queued message can never\n * be consumed by an SDK version that would silently drop it.\n */\nexport const HOOK_INJECTED_MESSAGES_CAPABLE = true;\n/**\n * Feature probe for hosts: this SDK dispatches `PreemptBoundary`, so a\n * cooperative mid-generation seal can drain into the run.\n *\n * Deliberately separate from {@link HOOK_INJECTED_MESSAGES_CAPABLE} — an SDK\n * version can support `injectedMessages` at the tool boundary and know\n * nothing about preemption. A host that probed the wrong flag would arm an\n * interrupt control whose seal request is silently ignored, which reads to\n * the user as a dead button rather than as an unsupported feature.\n */\nexport const HOOK_PREEMPT_BOUNDARY_CAPABLE = true;\nexport {\n matchesQuery,\n hasNestedQuantifier,\n MAX_PATTERN_LENGTH,\n MAX_CACHE_SIZE,\n} from './matchers';\nexport { createToolPolicyHook } from './createToolPolicyHook';\nexport type { ToolPolicyMode, ToolPolicyConfig } from './createToolPolicyHook';\nexport { createWorkspacePolicyHook } from './createWorkspacePolicyHook';\nexport type {\n OutsideAccessPolicy,\n WorkspacePolicyConfig,\n PathExtractor,\n} from './createWorkspacePolicyHook';\nexport { HOOK_EVENTS } from './types';\nexport type {\n HookEvent,\n HookInput,\n HookOutput,\n HookCallback,\n HookMatcher,\n HooksByEvent,\n HookInputByEvent,\n HookOutputByEvent,\n BaseHookInput,\n BaseHookOutput,\n ToolDecision,\n StopDecision,\n AggregatedHookResult,\n RunStartHookInput,\n UserPromptSubmitHookInput,\n PreToolUseHookInput,\n PostToolUseHookInput,\n PostToolUseFailureHookInput,\n PostToolBatchHookInput,\n PostToolBatchEntry,\n PreemptBoundaryHookInput,\n PermissionDeniedHookInput,\n SubagentStartHookInput,\n SubagentStopHookInput,\n StopHookInput,\n StopFailureHookInput,\n PreCompactHookInput,\n PostCompactHookInput,\n RunStartHookOutput,\n UserPromptSubmitHookOutput,\n PreToolUseHookOutput,\n PostToolUseHookOutput,\n PostToolUseFailureHookOutput,\n PostToolBatchHookOutput,\n PreemptBoundaryHookOutput,\n PermissionDeniedHookOutput,\n SubagentStartHookOutput,\n SubagentStopHookOutput,\n StopHookOutput,\n StopFailureHookOutput,\n PreCompactHookOutput,\n PostCompactHookOutput,\n} from './types';\nexport type { ExecuteHooksOptions } from './executeHooks';\n"],"mappings":";;;;;;;;;;;;;AAkBA,MAAa,iCAAiC;;;;;;;;;;;AAW9C,MAAa,gCAAgC"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/hooks/index.ts"],"sourcesContent":["// src/hooks/index.ts\n//\n// Hook lifecycle system for `@librechat/agents`. Re-exported from\n// `src/index.ts` and consumed by `Run.processStream` (RunStart,\n// UserPromptSubmit, Stop, StopFailure), `ToolNode.dispatchToolEvents`\n// (PreToolUse, PostToolUse, PostToolUseFailure, PermissionDenied),\n// `createSummarizeNode` (PreCompact, PostCompact),\n// `SubagentExecutor.execute` (SubagentStart, SubagentStop), and\n// `StandardGraph.createCallModel` (PreemptBoundary).\nexport { HookRegistry } from './HookRegistry';\nexport type { HookHaltSignal } from './HookRegistry';\nexport { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';\n/**\n * Feature probe for hosts: hook outputs support `injectedMessages`\n * (per-message graph-state injection at the `PostToolBatch` boundary).\n * Hosts must gate drain-style hooks on this so a queued message can never\n * be consumed by an SDK version that would silently drop it.\n */\nexport const HOOK_INJECTED_MESSAGES_CAPABLE = true;\n/**\n * Feature probe for hosts: this SDK dispatches `PreemptBoundary`, so a\n * cooperative mid-generation seal can drain into the run.\n *\n * Deliberately separate from {@link HOOK_INJECTED_MESSAGES_CAPABLE} — an SDK\n * version can support `injectedMessages` at the tool boundary and know\n * nothing about preemption. A host that probed the wrong flag would arm an\n * interrupt control whose seal request is silently ignored, which reads to\n * the user as a dead button rather than as an unsupported feature.\n */\nexport const HOOK_PREEMPT_BOUNDARY_CAPABLE = true;\nexport {\n matchesQuery,\n hasNestedQuantifier,\n MAX_PATTERN_LENGTH,\n MAX_CACHE_SIZE,\n} from './matchers';\nexport { createToolPolicyHook } from './createToolPolicyHook';\nexport type { ToolPolicyMode, ToolPolicyConfig } from './createToolPolicyHook';\nexport { createWorkspacePolicyHook } from './createWorkspacePolicyHook';\nexport type {\n OutsideAccessPolicy,\n WorkspacePolicyConfig,\n PathExtractor,\n} from './createWorkspacePolicyHook';\nexport { HOOK_EVENTS, TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from './types';\nexport type {\n HookEvent,\n HookInput,\n HookOutput,\n HookCallback,\n HookMatcher,\n HooksByEvent,\n HookInputByEvent,\n HookOutputByEvent,\n BaseHookInput,\n BaseHookOutput,\n ToolDecision,\n StopDecision,\n ToolApprovalReplayKey,\n ToolApprovalReplaySnapshot,\n AggregatedHookResult,\n RunStartHookInput,\n UserPromptSubmitHookInput,\n PreToolUseHookInput,\n PostToolUseHookInput,\n PostToolUseFailureHookInput,\n PostToolBatchHookInput,\n PostToolBatchEntry,\n PreemptBoundaryHookInput,\n PermissionDeniedHookInput,\n SubagentStartHookInput,\n SubagentStopHookInput,\n StopHookInput,\n StopFailureHookInput,\n PreCompactHookInput,\n PostCompactHookInput,\n RunStartHookOutput,\n UserPromptSubmitHookOutput,\n PreToolUseHookOutput,\n PostToolUseHookOutput,\n PostToolUseFailureHookOutput,\n PostToolBatchHookOutput,\n PreemptBoundaryHookOutput,\n PermissionDeniedHookOutput,\n SubagentStartHookOutput,\n SubagentStopHookOutput,\n StopHookOutput,\n StopFailureHookOutput,\n PreCompactHookOutput,\n PostCompactHookOutput,\n} from './types';\nexport type { ExecuteHooksOptions } from './executeHooks';\n"],"mappings":";;;;;;;;;;;;;AAkBA,MAAa,iCAAiC;;;;;;;;;;;AAW9C,MAAa,gCAAgC"}
|
package/dist/cjs/hooks/types.cjs
CHANGED
|
@@ -23,7 +23,9 @@ const HOOK_EVENTS = [
|
|
|
23
23
|
"PreCompact",
|
|
24
24
|
"PostCompact"
|
|
25
25
|
];
|
|
26
|
+
const TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY = "__librechat_tool_approval_execution_scope";
|
|
26
27
|
//#endregion
|
|
27
28
|
exports.HOOK_EVENTS = HOOK_EVENTS;
|
|
29
|
+
exports.TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY = TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY;
|
|
28
30
|
|
|
29
31
|
//# sourceMappingURL=types.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.cjs","names":[],"sources":["../../../src/hooks/types.ts"],"sourcesContent":["// src/hooks/types.ts\nimport type { BaseMessage } from '@langchain/core/messages';\nimport type { InjectedMessage } from '@/types/tools';\n\n/**\n * Closed set of hook lifecycle events supported by the hooks system.\n *\n * These mirror the subset of Claude Code's event surface that makes sense\n * for a library context (no filesystem/CLI-specific events). See\n * `docs/hooks-design-report.md` §3.2 for the mapping to existing\n * `@librechat/agents` emission points.\n */\nexport const HOOK_EVENTS = [\n 'RunStart',\n 'UserPromptSubmit',\n 'PreToolUse',\n 'PostToolUse',\n 'PostToolUseFailure',\n 'PostToolBatch',\n 'PreemptBoundary',\n 'PermissionDenied',\n 'SubagentStart',\n 'SubagentStop',\n 'Stop',\n 'StopFailure',\n 'PreCompact',\n 'PostCompact',\n] as const;\n\nexport type HookEvent = (typeof HOOK_EVENTS)[number];\n\n/** Tool-gating decision; executeHooks folds with `deny > ask > allow` precedence. */\nexport type ToolDecision = 'allow' | 'deny' | 'ask';\n\n/** Stop-loop decision; `block` means \"do not stop, run another turn\". Any `block` wins. */\nexport type StopDecision = 'continue' | 'block';\n\n/**\n * Fields shared by every `HookInput`. Discriminated by `hook_event_name`.\n *\n * - `runId` identifies the current agent run and is always present.\n * - `threadId` identifies the conversation thread when the host has one.\n * - `agentId` is only set when the hook fires inside a subagent scope.\n * - `executingAgentId` identifies the agent that owns the node emitting the hook,\n * whenever the graph knows it — including top-level agents in a multi-agent\n * graph (where `agentId` is intentionally undefined). Use this to attribute a\n * hook to a specific agent regardless of subagent scope.\n */\nexport interface BaseHookInput {\n runId: string;\n threadId?: string;\n agentId?: string;\n executingAgentId?: string;\n}\n\nexport interface RunStartHookInput extends BaseHookInput {\n hook_event_name: 'RunStart';\n messages: BaseMessage[];\n}\n\nexport interface UserPromptSubmitHookInput extends BaseHookInput {\n hook_event_name: 'UserPromptSubmit';\n prompt: string;\n attachments?: BaseMessage[];\n}\n\n/**\n * Fires before a tool is invoked. Hook may return `deny`/`ask`/`allow` and/or\n * an `updatedInput` that replaces the tool arguments before invocation.\n *\n * `toolInput` is intentionally typed as `Record<string, unknown>` because the\n * SDK is tool-agnostic — concrete tool argument shapes are only known at the\n * call site and are narrowed by the host. This is the one escape hatch in\n * the hook type system.\n */\nexport interface PreToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PreToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n stepId?: string;\n /**\n * Number of times this tool has been invoked in prior batches within the\n * current run. Within a single batch of parallel calls, all calls to the\n * same tool share the same turn value — per-call discrimination within a\n * batch is not supported in v1.\n */\n turn?: number;\n}\n\nexport interface PostToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolOutput: unknown;\n toolUseId: string;\n stepId?: string;\n turn?: number;\n}\n\nexport interface PostToolUseFailureHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUseFailure';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n error: string;\n stepId?: string;\n turn?: number;\n}\n\n/**\n * Per-tool result snapshot included in a `PostToolBatch` event. Mirrors\n * the data PostToolUse / PostToolUseFailure get individually, but the\n * batch view lets a single hook see the whole set so it can inject one\n * consolidated convention/audit message rather than N per-tool ones.\n */\nexport interface PostToolBatchEntry {\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n stepId?: string;\n turn?: number;\n /** Successful tool output, present only when `status === 'success'`. */\n toolOutput?: unknown;\n /** Error message, present only when `status === 'error'`. */\n error?: string;\n status: 'success' | 'error';\n}\n\n/**\n * Fires once after every tool call in a single batch finishes (including\n * any that were rejected via HITL). Lets a hook react to the batch as a\n * whole — useful for \"inject conventions once for the whole batch\", batch\n * audit logging, or coordinating cleanup that depends on knowing the full\n * result set rather than streaming each tool's result independently.\n *\n * Order: fires AFTER all per-tool PostToolUse / PostToolUseFailure hooks\n * for the same batch have completed, BEFORE the next model call. Pass an\n * `additionalContext` to inject context for that next model turn, or\n * `injectedMessages` to inject standalone per-message user speech (e.g.\n * mid-run steering) that must not be consolidated with hook context.\n */\nexport interface PostToolBatchHookInput extends BaseHookInput {\n hook_event_name: 'PostToolBatch';\n /** All tool calls (and their outcomes) from this batch, in batch order. */\n entries: PostToolBatchEntry[];\n}\n\n/**\n * Fires when a cooperative preemption seals the model stream mid-generation\n * — the second injection boundary, and the only one that exists during a\n * long text answer with no tool calls in it.\n *\n * Order: fires after the model stream is sealed and BEFORE the next model\n * call. The `injectedMessages` a hook returns here are appended verbatim and\n * the agent node self-loops. Returning nothing is a valid outcome (the host's\n * queue was cancelled or already drained): the run stops honestly rather than\n * self-looping into an empty turn.\n *\n * The sealed turn is NOT yet observable from graph state when this fires.\n * Dispatch happens inside the agent node, and only the outer graph's reducer\n * writes `StandardGraph.messages` — which cannot run until the node returns.\n * A hook calling `Run.getRunMessages()` here sees the state as of the last\n * completed superstep, so the sealed text is absent. `PostToolBatch` behaves\n * the same way: a tool-boundary hook cannot see the assistant turn that\n * requested the tool. This is a property of the single-node outer graph, not\n * of preemption, and committing first would mean returning from the node and\n * re-entering — precisely what the self-loop exists to avoid.\n *\n * A hook that needs the sealed text should therefore not go looking for it in\n * graph state. Decide from the host's own queue, or read it after the run.\n *\n * Requires `RunConfig.preemption`. This event is deliberately NOT\n * result-altering, so registering it never disables eager tool execution.\n */\nexport interface PreemptBoundaryHookInput extends BaseHookInput {\n hook_event_name: 'PreemptBoundary';\n /** 1-based index of this seal within the run. */\n sealCount: number;\n}\n\nexport interface PermissionDeniedHookInput extends BaseHookInput {\n hook_event_name: 'PermissionDenied';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n reason: string;\n}\n\nexport interface SubagentStartHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStart';\n parentAgentId?: string;\n agentId: string;\n agentType: string;\n inputs: BaseMessage[];\n}\n\nexport interface SubagentStopHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStop';\n agentId: string;\n agentType: string;\n messages: BaseMessage[];\n}\n\nexport interface StopHookInput extends BaseHookInput {\n hook_event_name: 'Stop';\n messages: BaseMessage[];\n stopReason?: string;\n stopHookActive: boolean;\n}\n\nexport interface StopFailureHookInput extends BaseHookInput {\n hook_event_name: 'StopFailure';\n error: string;\n lastAssistantMessage?: BaseMessage;\n}\n\nexport interface PreCompactHookInput extends BaseHookInput {\n hook_event_name: 'PreCompact';\n messagesBeforeCount: number;\n /**\n * What triggered compaction. Matches `SummarizationTrigger.type` from the\n * agent's summarization config. `'default'` means no trigger was\n * configured and compaction fired because messages were pruned.\n */\n trigger:\n | 'token_ratio'\n | 'remaining_tokens'\n | 'messages_to_refine'\n | 'default'\n | (string & {});\n}\n\nexport interface PostCompactHookInput extends BaseHookInput {\n hook_event_name: 'PostCompact';\n summary: string;\n /**\n * Number of messages remaining after compaction. The summarize node\n * returns a `removeAll` signal that clears all messages from state;\n * the summary itself is injected into the system prompt, not as a\n * message. This is `0` at the point of hook dispatch.\n */\n messagesAfterCount: number;\n}\n\n/** Discriminated union of every hook input shape. */\nexport type HookInput =\n | RunStartHookInput\n | UserPromptSubmitHookInput\n | PreToolUseHookInput\n | PostToolUseHookInput\n | PostToolUseFailureHookInput\n | PostToolBatchHookInput\n | PreemptBoundaryHookInput\n | PermissionDeniedHookInput\n | SubagentStartHookInput\n | SubagentStopHookInput\n | StopHookInput\n | StopFailureHookInput\n | PreCompactHookInput\n | PostCompactHookInput;\n\n/** Compile-time map from event name to its input shape. */\nexport type HookInputByEvent = {\n RunStart: RunStartHookInput;\n UserPromptSubmit: UserPromptSubmitHookInput;\n PreToolUse: PreToolUseHookInput;\n PostToolUse: PostToolUseHookInput;\n PostToolUseFailure: PostToolUseFailureHookInput;\n PostToolBatch: PostToolBatchHookInput;\n PreemptBoundary: PreemptBoundaryHookInput;\n PermissionDenied: PermissionDeniedHookInput;\n SubagentStart: SubagentStartHookInput;\n SubagentStop: SubagentStopHookInput;\n Stop: StopHookInput;\n StopFailure: StopFailureHookInput;\n PreCompact: PreCompactHookInput;\n PostCompact: PostCompactHookInput;\n};\n\n/**\n * Fields common to every hook output. Hooks that have nothing to say simply\n * return `{}` (or omit the fields below).\n */\nexport interface BaseHookOutput {\n /** Context string to inject into the conversation. Accumulated across hooks. */\n additionalContext?: string;\n /**\n * Messages to inject into graph state, one `HumanMessage` per entry\n * (converted via `convertInjectedMessages`, which preserves\n * `role`/`source`/`isMeta` in `additional_kwargs`). Unlike\n * `additionalContext` — which is consolidated across hooks into a single\n * system-flavored message — each entry keeps its own identity and role,\n * making this the channel for injecting verbatim user speech (e.g. a\n * mid-run steering message). Accumulated across hooks in registration\n * order.\n *\n * Consumed at exactly two dispatch sites, both of which run the same\n * converter so the emitted shapes cannot drift: `PostToolBatch` (the tool\n * boundary) and `PreemptBoundary` (a cooperative mid-generation seal).\n * Every other event ignores the field.\n */\n injectedMessages?: InjectedMessage[];\n /** True to prevent the next model turn. Any hook can set this. */\n preventContinuation?: boolean;\n /** Reason reported alongside `preventContinuation`. */\n stopReason?: string;\n /**\n * Marks this hook output as fire-and-forget for INFLUENCE only.\n * When `true`, the SDK skips every other field on this output —\n * `decision`, `additionalContext`, `updatedInput`,\n * `preventContinuation`, `allowedDecisions`, `updatedOutput` are\n * all ignored. The hook's return value cannot block, modify, or\n * inject context, so it's safe to use for pure side effects\n * (logging, metrics, webhooks).\n *\n * Important caveat: the hook's CALLBACK promise is still awaited\n * by `executeHooks` (subject to the matcher's timeout and the\n * default `DEFAULT_HOOK_TIMEOUT_MS`). The SDK does not\n * speculatively detach hooks based on output shape, because the\n * shape is only known after the promise resolves. For TRUE\n * fire-and-forget where the agent doesn't wait at all, the hook\n * body should detach its side effect itself and return\n * immediately:\n *\n * @example\n * ```ts\n * async (input) => {\n * // Detach the slow work — the SDK awaits this hook's\n * // returned promise, which resolves immediately because we\n * // don't `await` the side effect.\n * void sendToLoggingService(input).catch(console.error);\n * return { async: true };\n * };\n * ```\n *\n * @example WRONG — the agent will block on the webhook\n * ```ts\n * async (input) => {\n * await sendToLoggingService(input); // ← awaited, blocks\n * return { async: true }; // returning async:true doesn't undo the await\n * };\n * ```\n *\n * Mirrors Claude Code Agent SDK's `async` output, with the same\n * \"detach inside the hook body\" pattern.\n */\n async?: boolean;\n /**\n * Optional advisory timeout in milliseconds for the background work\n * a host has detached inside an `async: true` hook body. The SDK\n * does not enforce this (the hook's own AbortSignal handling does)\n * but the field is preserved on the wire so downstream\n * observability can surface long-running side effects. Ignored\n * unless `async` is true.\n */\n asyncTimeout?: number;\n}\n\nexport type RunStartHookOutput = BaseHookOutput;\n\nexport interface UserPromptSubmitHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport interface PreToolUseHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n /**\n * Replacement tool input. Merged into the pending tool call by the host.\n *\n * When multiple hooks set `updatedInput` within a single `executeHooks`\n * call, the last writer in registration order wins (outer loop: matcher\n * registration order; inner loop: hook position within the matcher). The\n * winner is deterministic — `Promise.all` preserves input-array order.\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput` to one hook per matcher to avoid confusing precedence.\n */\n updatedInput?: Record<string, unknown>;\n /**\n * Restricts which decisions the host UI is allowed to surface for this\n * tool call when the hook returns `decision: 'ask'`. Pass to lock a\n * tool down to a subset of `'approve' | 'reject' | 'edit' | 'respond'`\n * — for example, `['approve', 'reject']` to forbid the user from\n * editing the tool's args or substituting a custom response.\n *\n * The values flow into the resulting interrupt's\n * `review_configs[i].allowed_decisions`. Omitting the field keeps the\n * SDK default (all four decisions advertised). Last-writer-wins in\n * registration order, same precedence rules as `updatedInput`.\n */\n allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;\n}\n\nexport interface PostToolUseHookOutput extends BaseHookOutput {\n /**\n * Replacement tool output. Flows through the aggregated result so the\n * host can substitute it before appending the tool result message.\n * Ordering semantics match `PreToolUseHookOutput.updatedInput`:\n * last-writer-wins in registration order.\n */\n updatedOutput?: unknown;\n}\n\nexport type PostToolUseFailureHookOutput = BaseHookOutput;\n\nexport type PostToolBatchHookOutput = BaseHookOutput;\n\nexport type PreemptBoundaryHookOutput = BaseHookOutput;\n\nexport type PermissionDeniedHookOutput = BaseHookOutput;\n\nexport interface SubagentStartHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport type SubagentStopHookOutput = BaseHookOutput;\n\nexport interface StopHookOutput extends BaseHookOutput {\n decision?: StopDecision;\n reason?: string;\n}\n\nexport type StopFailureHookOutput = BaseHookOutput;\n\nexport type PreCompactHookOutput = BaseHookOutput;\n\nexport type PostCompactHookOutput = BaseHookOutput;\n\n/** Compile-time map from event name to its output shape. */\nexport type HookOutputByEvent = {\n RunStart: RunStartHookOutput;\n UserPromptSubmit: UserPromptSubmitHookOutput;\n PreToolUse: PreToolUseHookOutput;\n PostToolUse: PostToolUseHookOutput;\n PostToolUseFailure: PostToolUseFailureHookOutput;\n PostToolBatch: PostToolBatchHookOutput;\n PreemptBoundary: PreemptBoundaryHookOutput;\n PermissionDenied: PermissionDeniedHookOutput;\n SubagentStart: SubagentStartHookOutput;\n SubagentStop: SubagentStopHookOutput;\n Stop: StopHookOutput;\n StopFailure: StopFailureHookOutput;\n PreCompact: PreCompactHookOutput;\n PostCompact: PostCompactHookOutput;\n};\n\n/** Superset output shape used by the executor's fold loop. */\nexport type HookOutput =\n | RunStartHookOutput\n | UserPromptSubmitHookOutput\n | PreToolUseHookOutput\n | PostToolUseHookOutput\n | PostToolUseFailureHookOutput\n | PostToolBatchHookOutput\n | PreemptBoundaryHookOutput\n | PermissionDeniedHookOutput\n | SubagentStartHookOutput\n | SubagentStopHookOutput\n | StopHookOutput\n | StopFailureHookOutput\n | PreCompactHookOutput\n | PostCompactHookOutput;\n\n/**\n * A hook callback is a plain async function registered against a specific\n * event. The `signal` is always supplied by `executeHooks` and combines the\n * batch's parent signal with the per-hook timeout — callbacks that perform\n * long-running work should observe it.\n */\nexport type HookCallback<E extends HookEvent = HookEvent> = (\n input: HookInputByEvent[E],\n signal: AbortSignal\n) => HookOutputByEvent[E] | Promise<HookOutputByEvent[E]>;\n\n/**\n * A matcher groups one or more callbacks under a shared regex filter and\n * shared timeout/once/internal flags. The generic `E` ties the callback\n * types to the event the matcher is registered against.\n */\nexport interface HookMatcher<E extends HookEvent = HookEvent> {\n /**\n * Regex pattern matched against the event's primary query string (e.g.\n * the tool name for `PreToolUse`, the agent type for `SubagentStart`).\n *\n * Omitted or empty means \"always match\". For events that do not supply a\n * query string (`RunStart`, `Stop`, etc.), only wildcard matchers fire —\n * a non-empty pattern on such events will never match.\n *\n * Patterns are treated as trusted input: `executeHooks` compiles them\n * with `new RegExp(pattern)` without any sandbox, and a pathological\n * pattern can block the event loop. Host registration code is expected\n * to validate or length-bound patterns that originate from user input.\n */\n pattern?: string;\n /** Callbacks that fire when the matcher hits. Executed in parallel. */\n hooks: HookCallback<E>[];\n /** Per-matcher timeout in ms. Defaults to the executor's batch timeout. */\n timeout?: number;\n /**\n * Atomically remove the matcher before its first dispatch.\n *\n * `executeHooks` claims `once: true` matchers synchronously — between\n * `getMatchers` and its first `await` — so two concurrent calls cannot\n * both dispatch the same matcher. Whichever call runs its sync prefix\n * first wins the matcher; the other sees an empty bucket.\n *\n * Semantics are \"at most one dispatch, ever\" — if every hook in the\n * matcher throws, the matcher is still gone. Use `once` for\n * fire-and-forget bootstrapping (registration, telemetry, setup). Hosts\n * that need retry semantics should register a normal matcher and\n * self-unregister via the callback returned from `registry.register`.\n */\n once?: boolean;\n /** Internal hooks are excluded from telemetry and non-fatal error logging. */\n internal?: boolean;\n}\n\n/**\n * Storage shape for matchers keyed by event. Each event's matcher list is\n * a generic array parameterized by that event type, so lookup via\n * `HooksByEvent[E]` preserves type-safe callback signatures.\n */\nexport type HooksByEvent = {\n [E in HookEvent]?: HookMatcher<E>[];\n};\n\n/**\n * Aggregated result of a single `executeHooks` call. Fields are populated\n * according to the fold rules in `executeHooks.ts`.\n */\nexport interface AggregatedHookResult {\n /** Folded tool-gating decision; `deny > ask > allow`. */\n decision?: ToolDecision;\n /** Folded stop decision; any `block` wins. */\n stopDecision?: StopDecision;\n /** Reason from the hook that set the winning decision. */\n reason?: string;\n /**\n * Replacement tool input from a `PreToolUse` hook.\n *\n * Last-writer-wins in **registration order**: `executeHooks` uses\n * `Promise.all`, which preserves input-array order, so the fold iterates\n * outcomes in the same order they were pushed — outer loop over matchers\n * as they sit in the registry, inner loop over each matcher's `hooks`\n * array. The winner is therefore deterministic but may not match the\n * order in which hooks actually completed. Consumers that want a single\n * authoritative rewrite should still register one `updatedInput`-setting\n * hook per matcher to avoid subtle precedence bugs.\n */\n updatedInput?: Record<string, unknown>;\n /**\n * Restricted decision set from a `PreToolUse` hook. Same last-writer-wins\n * semantics as `updatedInput`. Surfaces to the interrupt payload's\n * `review_configs[i].allowed_decisions`.\n */\n allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;\n /**\n * Replacement tool output from a `PostToolUse` hook.\n *\n * Same last-writer-wins-in-registration-order semantics as\n * `updatedInput`. Present only when at least one hook set it; `undefined`\n * means \"use the original tool output\".\n */\n updatedOutput?: unknown;\n /** Accumulated `additionalContext` strings from every hook, in order. */\n additionalContexts: string[];\n /** Accumulated `injectedMessages` from every hook, in registration order. */\n injectedMessages: InjectedMessage[];\n /** True if any hook returned `preventContinuation`. */\n preventContinuation?: boolean;\n /**\n * Reason recorded alongside `preventContinuation`. First winner wins:\n * once a hook sets both flags, later hooks that also set\n * `preventContinuation` do not overwrite the reason.\n */\n stopReason?: string;\n /** Error messages from hooks that threw; always present (possibly empty). */\n errors: string[];\n}\n"],"mappings":";;;;;;;;;AAYA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
|
|
1
|
+
{"version":3,"file":"types.cjs","names":[],"sources":["../../../src/hooks/types.ts"],"sourcesContent":["// src/hooks/types.ts\nimport type { BaseMessage } from '@langchain/core/messages';\nimport type { InjectedMessage } from '@/types/tools';\n\n/**\n * Closed set of hook lifecycle events supported by the hooks system.\n *\n * These mirror the subset of Claude Code's event surface that makes sense\n * for a library context (no filesystem/CLI-specific events). See\n * `docs/hooks-design-report.md` §3.2 for the mapping to existing\n * `@librechat/agents` emission points.\n */\nexport const HOOK_EVENTS = [\n 'RunStart',\n 'UserPromptSubmit',\n 'PreToolUse',\n 'PostToolUse',\n 'PostToolUseFailure',\n 'PostToolBatch',\n 'PreemptBoundary',\n 'PermissionDenied',\n 'SubagentStart',\n 'SubagentStop',\n 'Stop',\n 'StopFailure',\n 'PreCompact',\n 'PostCompact',\n] as const;\n\nexport const TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY =\n '__librechat_tool_approval_execution_scope';\n\nexport type HookEvent = (typeof HOOK_EVENTS)[number];\n\n/** Tool-gating decision; executeHooks folds with `deny > ask > allow` precedence. */\nexport type ToolDecision = 'allow' | 'deny' | 'ask';\n\n/** Stop-loop decision; `block` means \"do not stop, run another turn\". Any `block` wins. */\nexport type StopDecision = 'continue' | 'block';\n\n/**\n * Fields shared by every `HookInput`. Discriminated by `hook_event_name`.\n *\n * - `runId` identifies the current agent run and is always present.\n * - `threadId` identifies the conversation thread when the host has one.\n * - `agentId` is only set when the hook fires inside a subagent scope.\n * - `executingAgentId` identifies the agent that owns the node emitting the hook,\n * whenever the graph knows it — including top-level agents in a multi-agent\n * graph (where `agentId` is intentionally undefined). Use this to attribute a\n * hook to a specific agent regardless of subagent scope.\n */\nexport interface BaseHookInput {\n runId: string;\n threadId?: string;\n agentId?: string;\n executingAgentId?: string;\n}\n\nexport interface RunStartHookInput extends BaseHookInput {\n hook_event_name: 'RunStart';\n messages: BaseMessage[];\n}\n\nexport interface UserPromptSubmitHookInput extends BaseHookInput {\n hook_event_name: 'UserPromptSubmit';\n prompt: string;\n attachments?: BaseMessage[];\n}\n\n/**\n * Fires before a tool is invoked. Hook may return `deny`/`ask`/`allow` and/or\n * an `updatedInput` that replaces the tool arguments before invocation.\n *\n * `toolInput` is intentionally typed as `Record<string, unknown>` because the\n * SDK is tool-agnostic — concrete tool argument shapes are only known at the\n * call site and are narrowed by the host. This is the one escape hatch in\n * the hook type system.\n */\nexport interface PreToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PreToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n stepId?: string;\n /**\n * Number of times this tool has been invoked in prior batches within the\n * current run. Within a single batch of parallel calls, all calls to the\n * same tool share the same turn value — per-call discrimination within a\n * batch is not supported in v1.\n */\n turn?: number;\n}\n\nexport interface PostToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolOutput: unknown;\n toolUseId: string;\n stepId?: string;\n turn?: number;\n}\n\nexport interface PostToolUseFailureHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUseFailure';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n error: string;\n stepId?: string;\n turn?: number;\n}\n\n/**\n * Per-tool result snapshot included in a `PostToolBatch` event. Mirrors\n * the data PostToolUse / PostToolUseFailure get individually, but the\n * batch view lets a single hook see the whole set so it can inject one\n * consolidated convention/audit message rather than N per-tool ones.\n */\nexport interface PostToolBatchEntry {\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n stepId?: string;\n turn?: number;\n /** Successful tool output, present only when `status === 'success'`. */\n toolOutput?: unknown;\n /** Error message, present only when `status === 'error'`. */\n error?: string;\n status: 'success' | 'error';\n}\n\n/**\n * Fires once after every tool call in a single batch finishes (including\n * any that were rejected via HITL). Lets a hook react to the batch as a\n * whole — useful for \"inject conventions once for the whole batch\", batch\n * audit logging, or coordinating cleanup that depends on knowing the full\n * result set rather than streaming each tool's result independently.\n *\n * Order: fires AFTER all per-tool PostToolUse / PostToolUseFailure hooks\n * for the same batch have completed, BEFORE the next model call. Pass an\n * `additionalContext` to inject context for that next model turn, or\n * `injectedMessages` to inject standalone per-message user speech (e.g.\n * mid-run steering) that must not be consolidated with hook context.\n */\nexport interface PostToolBatchHookInput extends BaseHookInput {\n hook_event_name: 'PostToolBatch';\n /** All tool calls (and their outcomes) from this batch, in batch order. */\n entries: PostToolBatchEntry[];\n}\n\n/**\n * Fires when a cooperative preemption seals the model stream mid-generation\n * — the second injection boundary, and the only one that exists during a\n * long text answer with no tool calls in it.\n *\n * Order: fires after the model stream is sealed and BEFORE the next model\n * call. The `injectedMessages` a hook returns here are appended verbatim and\n * the agent node self-loops. Returning nothing is a valid outcome (the host's\n * queue was cancelled or already drained): the run stops honestly rather than\n * self-looping into an empty turn.\n *\n * The sealed turn is NOT yet observable from graph state when this fires.\n * Dispatch happens inside the agent node, and only the outer graph's reducer\n * writes `StandardGraph.messages` — which cannot run until the node returns.\n * A hook calling `Run.getRunMessages()` here sees the state as of the last\n * completed superstep, so the sealed text is absent. `PostToolBatch` behaves\n * the same way: a tool-boundary hook cannot see the assistant turn that\n * requested the tool. This is a property of the single-node outer graph, not\n * of preemption, and committing first would mean returning from the node and\n * re-entering — precisely what the self-loop exists to avoid.\n *\n * A hook that needs the sealed text should therefore not go looking for it in\n * graph state. Decide from the host's own queue, or read it after the run.\n *\n * Requires `RunConfig.preemption`. This event is deliberately NOT\n * result-altering, so registering it never disables eager tool execution.\n */\nexport interface PreemptBoundaryHookInput extends BaseHookInput {\n hook_event_name: 'PreemptBoundary';\n /** 1-based index of this seal within the run. */\n sealCount: number;\n}\n\nexport interface PermissionDeniedHookInput extends BaseHookInput {\n hook_event_name: 'PermissionDenied';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n reason: string;\n}\n\nexport interface SubagentStartHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStart';\n parentAgentId?: string;\n agentId: string;\n agentType: string;\n inputs: BaseMessage[];\n}\n\nexport interface SubagentStopHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStop';\n agentId: string;\n agentType: string;\n messages: BaseMessage[];\n}\n\nexport interface StopHookInput extends BaseHookInput {\n hook_event_name: 'Stop';\n messages: BaseMessage[];\n stopReason?: string;\n stopHookActive: boolean;\n}\n\nexport interface StopFailureHookInput extends BaseHookInput {\n hook_event_name: 'StopFailure';\n error: string;\n lastAssistantMessage?: BaseMessage;\n}\n\nexport interface PreCompactHookInput extends BaseHookInput {\n hook_event_name: 'PreCompact';\n messagesBeforeCount: number;\n /**\n * What triggered compaction. Matches `SummarizationTrigger.type` from the\n * agent's summarization config. `'default'` means no trigger was\n * configured and compaction fired because messages were pruned.\n */\n trigger:\n | 'token_ratio'\n | 'remaining_tokens'\n | 'messages_to_refine'\n | 'default'\n | (string & {});\n}\n\nexport interface PostCompactHookInput extends BaseHookInput {\n hook_event_name: 'PostCompact';\n summary: string;\n /**\n * Number of messages remaining after compaction. The summarize node\n * returns a `removeAll` signal that clears all messages from state;\n * the summary itself is injected into the system prompt, not as a\n * message. This is `0` at the point of hook dispatch.\n */\n messagesAfterCount: number;\n}\n\n/** Discriminated union of every hook input shape. */\nexport type HookInput =\n | RunStartHookInput\n | UserPromptSubmitHookInput\n | PreToolUseHookInput\n | PostToolUseHookInput\n | PostToolUseFailureHookInput\n | PostToolBatchHookInput\n | PreemptBoundaryHookInput\n | PermissionDeniedHookInput\n | SubagentStartHookInput\n | SubagentStopHookInput\n | StopHookInput\n | StopFailureHookInput\n | PreCompactHookInput\n | PostCompactHookInput;\n\n/** Compile-time map from event name to its input shape. */\nexport type HookInputByEvent = {\n RunStart: RunStartHookInput;\n UserPromptSubmit: UserPromptSubmitHookInput;\n PreToolUse: PreToolUseHookInput;\n PostToolUse: PostToolUseHookInput;\n PostToolUseFailure: PostToolUseFailureHookInput;\n PostToolBatch: PostToolBatchHookInput;\n PreemptBoundary: PreemptBoundaryHookInput;\n PermissionDenied: PermissionDeniedHookInput;\n SubagentStart: SubagentStartHookInput;\n SubagentStop: SubagentStopHookInput;\n Stop: StopHookInput;\n StopFailure: StopFailureHookInput;\n PreCompact: PreCompactHookInput;\n PostCompact: PostCompactHookInput;\n};\n\n/**\n * Fields common to every hook output. Hooks that have nothing to say simply\n * return `{}` (or omit the fields below).\n */\nexport interface BaseHookOutput {\n /** Context string to inject into the conversation. Accumulated across hooks. */\n additionalContext?: string;\n /**\n * Messages to inject into graph state, one `HumanMessage` per entry\n * (converted via `convertInjectedMessages`, which preserves\n * `role`/`source`/`isMeta` in `additional_kwargs`). Unlike\n * `additionalContext` — which is consolidated across hooks into a single\n * system-flavored message — each entry keeps its own identity and role,\n * making this the channel for injecting verbatim user speech (e.g. a\n * mid-run steering message). Accumulated across hooks in registration\n * order.\n *\n * Consumed at exactly two dispatch sites, both of which run the same\n * converter so the emitted shapes cannot drift: `PostToolBatch` (the tool\n * boundary) and `PreemptBoundary` (a cooperative mid-generation seal).\n * Every other event ignores the field.\n */\n injectedMessages?: InjectedMessage[];\n /** True to prevent the next model turn. Any hook can set this. */\n preventContinuation?: boolean;\n /** Reason reported alongside `preventContinuation`. */\n stopReason?: string;\n /**\n * Marks this hook output as fire-and-forget for INFLUENCE only.\n * When `true`, the SDK skips every other field on this output —\n * `decision`, `additionalContext`, `updatedInput`,\n * `preventContinuation`, `allowedDecisions`, `updatedOutput` are\n * all ignored. The hook's return value cannot block, modify, or\n * inject context, so it's safe to use for pure side effects\n * (logging, metrics, webhooks).\n *\n * Important caveat: the hook's CALLBACK promise is still awaited\n * by `executeHooks` (subject to the matcher's timeout and the\n * default `DEFAULT_HOOK_TIMEOUT_MS`). The SDK does not\n * speculatively detach hooks based on output shape, because the\n * shape is only known after the promise resolves. For TRUE\n * fire-and-forget where the agent doesn't wait at all, the hook\n * body should detach its side effect itself and return\n * immediately:\n *\n * @example\n * ```ts\n * async (input) => {\n * // Detach the slow work — the SDK awaits this hook's\n * // returned promise, which resolves immediately because we\n * // don't `await` the side effect.\n * void sendToLoggingService(input).catch(console.error);\n * return { async: true };\n * };\n * ```\n *\n * @example WRONG — the agent will block on the webhook\n * ```ts\n * async (input) => {\n * await sendToLoggingService(input); // ← awaited, blocks\n * return { async: true }; // returning async:true doesn't undo the await\n * };\n * ```\n *\n * Mirrors Claude Code Agent SDK's `async` output, with the same\n * \"detach inside the hook body\" pattern.\n */\n async?: boolean;\n /**\n * Optional advisory timeout in milliseconds for the background work\n * a host has detached inside an `async: true` hook body. The SDK\n * does not enforce this (the hook's own AbortSignal handling does)\n * but the field is preserved on the wire so downstream\n * observability can surface long-running side effects. Ignored\n * unless `async` is true.\n */\n asyncTimeout?: number;\n}\n\nexport type RunStartHookOutput = BaseHookOutput;\n\nexport interface UserPromptSubmitHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport interface PreToolUseHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n /**\n * Replacement tool input. Merged into the pending tool call by the host.\n *\n * When multiple hooks set `updatedInput` within a single `executeHooks`\n * call, the last writer in registration order wins (outer loop: matcher\n * registration order; inner loop: hook position within the matcher). The\n * winner is deterministic — `Promise.all` preserves input-array order.\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput` to one hook per matcher to avoid confusing precedence.\n */\n updatedInput?: Record<string, unknown>;\n /**\n * Restricts which decisions the host UI is allowed to surface for this\n * tool call when the hook returns `decision: 'ask'`. Pass to lock a\n * tool down to a subset of `'approve' | 'reject' | 'edit' | 'respond'`\n * — for example, `['approve', 'reject']` to forbid the user from\n * editing the tool's args or substituting a custom response.\n *\n * The values flow into the resulting interrupt's\n * `review_configs[i].allowed_decisions`. Omitting the field keeps the\n * SDK default (all four decisions advertised). Last-writer-wins in\n * registration order, same precedence rules as `updatedInput`.\n */\n allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;\n}\n\n/** Stable identity for replaying a consumed one-shot tool approval. */\nexport interface ToolApprovalReplayKey {\n executionScope: string;\n agentId: string;\n toolUseId: string;\n}\n\n/** Checkpoint-safe copy of a consumed one-shot approval contribution. */\nexport interface ToolApprovalReplaySnapshot {\n key: ToolApprovalReplayKey;\n result: AggregatedHookResult;\n}\n\nexport interface PostToolUseHookOutput extends BaseHookOutput {\n /**\n * Replacement tool output. Flows through the aggregated result so the\n * host can substitute it before appending the tool result message.\n * Ordering semantics match `PreToolUseHookOutput.updatedInput`:\n * last-writer-wins in registration order.\n */\n updatedOutput?: unknown;\n}\n\nexport type PostToolUseFailureHookOutput = BaseHookOutput;\n\nexport type PostToolBatchHookOutput = BaseHookOutput;\n\nexport type PreemptBoundaryHookOutput = BaseHookOutput;\n\nexport type PermissionDeniedHookOutput = BaseHookOutput;\n\nexport interface SubagentStartHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport type SubagentStopHookOutput = BaseHookOutput;\n\nexport interface StopHookOutput extends BaseHookOutput {\n decision?: StopDecision;\n reason?: string;\n}\n\nexport type StopFailureHookOutput = BaseHookOutput;\n\nexport type PreCompactHookOutput = BaseHookOutput;\n\nexport type PostCompactHookOutput = BaseHookOutput;\n\n/** Compile-time map from event name to its output shape. */\nexport type HookOutputByEvent = {\n RunStart: RunStartHookOutput;\n UserPromptSubmit: UserPromptSubmitHookOutput;\n PreToolUse: PreToolUseHookOutput;\n PostToolUse: PostToolUseHookOutput;\n PostToolUseFailure: PostToolUseFailureHookOutput;\n PostToolBatch: PostToolBatchHookOutput;\n PreemptBoundary: PreemptBoundaryHookOutput;\n PermissionDenied: PermissionDeniedHookOutput;\n SubagentStart: SubagentStartHookOutput;\n SubagentStop: SubagentStopHookOutput;\n Stop: StopHookOutput;\n StopFailure: StopFailureHookOutput;\n PreCompact: PreCompactHookOutput;\n PostCompact: PostCompactHookOutput;\n};\n\n/** Superset output shape used by the executor's fold loop. */\nexport type HookOutput =\n | RunStartHookOutput\n | UserPromptSubmitHookOutput\n | PreToolUseHookOutput\n | PostToolUseHookOutput\n | PostToolUseFailureHookOutput\n | PostToolBatchHookOutput\n | PreemptBoundaryHookOutput\n | PermissionDeniedHookOutput\n | SubagentStartHookOutput\n | SubagentStopHookOutput\n | StopHookOutput\n | StopFailureHookOutput\n | PreCompactHookOutput\n | PostCompactHookOutput;\n\n/**\n * A hook callback is a plain async function registered against a specific\n * event. The `signal` is always supplied by `executeHooks` and combines the\n * batch's parent signal with the per-hook timeout — callbacks that perform\n * long-running work should observe it.\n */\nexport type HookCallback<E extends HookEvent = HookEvent> = (\n input: HookInputByEvent[E],\n signal: AbortSignal\n) => HookOutputByEvent[E] | Promise<HookOutputByEvent[E]>;\n\n/**\n * A matcher groups one or more callbacks under a shared regex filter and\n * shared timeout/once/internal flags. The generic `E` ties the callback\n * types to the event the matcher is registered against.\n */\nexport interface HookMatcher<E extends HookEvent = HookEvent> {\n /**\n * Regex pattern matched against the event's primary query string (e.g.\n * the tool name for `PreToolUse`, the agent type for `SubagentStart`).\n *\n * Omitted or empty means \"always match\". For events that do not supply a\n * query string (`RunStart`, `Stop`, etc.), only wildcard matchers fire —\n * a non-empty pattern on such events will never match.\n *\n * Patterns are treated as trusted input: `executeHooks` compiles them\n * with `new RegExp(pattern)` without any sandbox, and a pathological\n * pattern can block the event loop. Host registration code is expected\n * to validate or length-bound patterns that originate from user input.\n */\n pattern?: string;\n /** Callbacks that fire when the matcher hits. Executed in parallel. */\n hooks: HookCallback<E>[];\n /** Per-matcher timeout in ms. Defaults to the executor's batch timeout. */\n timeout?: number;\n /**\n * Atomically remove the matcher before its first dispatch.\n *\n * `executeHooks` claims `once: true` matchers synchronously — between\n * `getMatchers` and its first `await` — so two concurrent calls cannot\n * both dispatch the same matcher. Whichever call runs its sync prefix\n * first wins the matcher; the other sees an empty bucket.\n *\n * Semantics are \"at most one dispatch, ever\" — if every hook in the\n * matcher throws, the matcher is still gone. Use `once` for\n * fire-and-forget bootstrapping (registration, telemetry, setup). Hosts\n * that need retry semantics should register a normal matcher and\n * self-unregister via the callback returned from `registry.register`.\n */\n once?: boolean;\n /** Internal hooks are excluded from telemetry and non-fatal error logging. */\n internal?: boolean;\n}\n\n/**\n * Storage shape for matchers keyed by event. Each event's matcher list is\n * a generic array parameterized by that event type, so lookup via\n * `HooksByEvent[E]` preserves type-safe callback signatures.\n */\nexport type HooksByEvent = {\n [E in HookEvent]?: HookMatcher<E>[];\n};\n\n/**\n * Aggregated result of a single `executeHooks` call. Fields are populated\n * according to the fold rules in `executeHooks.ts`.\n */\nexport interface AggregatedHookResult {\n /** Folded tool-gating decision; `deny > ask > allow`. */\n decision?: ToolDecision;\n /** Folded stop decision; any `block` wins. */\n stopDecision?: StopDecision;\n /** Reason from the hook that set the winning decision. */\n reason?: string;\n /**\n * Replacement tool input from a `PreToolUse` hook.\n *\n * Last-writer-wins in **registration order**: `executeHooks` uses\n * `Promise.all`, which preserves input-array order, so the fold iterates\n * outcomes in the same order they were pushed — outer loop over matchers\n * as they sit in the registry, inner loop over each matcher's `hooks`\n * array. The winner is therefore deterministic but may not match the\n * order in which hooks actually completed. Consumers that want a single\n * authoritative rewrite should still register one `updatedInput`-setting\n * hook per matcher to avoid subtle precedence bugs.\n */\n updatedInput?: Record<string, unknown>;\n /**\n * Restricted decision set from a `PreToolUse` hook. Same last-writer-wins\n * semantics as `updatedInput`. Surfaces to the interrupt payload's\n * `review_configs[i].allowed_decisions`.\n */\n allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;\n /**\n * Replacement tool output from a `PostToolUse` hook.\n *\n * Same last-writer-wins-in-registration-order semantics as\n * `updatedInput`. Present only when at least one hook set it; `undefined`\n * means \"use the original tool output\".\n */\n updatedOutput?: unknown;\n /** Accumulated `additionalContext` strings from every hook, in order. */\n additionalContexts: string[];\n /** Accumulated `injectedMessages` from every hook, in registration order. */\n injectedMessages: InjectedMessage[];\n /** True if any hook returned `preventContinuation`. */\n preventContinuation?: boolean;\n /**\n * Reason recorded alongside `preventContinuation`. First winner wins:\n * once a hook sets both flags, later hooks that also set\n * `preventContinuation` do not overwrite the reason.\n */\n stopReason?: string;\n /** Error messages from hooks that threw; always present (possibly empty). */\n errors: string[];\n}\n"],"mappings":";;;;;;;;;AAYA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAa,2CACX"}
|
package/dist/cjs/llm/init.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
require("../common/enum.cjs");
|
|
2
2
|
require("../common/index.cjs");
|
|
3
3
|
const require_llm = require("../utils/llm.cjs");
|
|
4
|
-
const require_index$1 = require("./openai/index.cjs");
|
|
5
|
-
const require_providers = require("./providers.cjs");
|
|
6
4
|
require("../utils/index.cjs");
|
|
5
|
+
const require_index$2 = require("./openai/index.cjs");
|
|
6
|
+
const require_providers = require("./providers.cjs");
|
|
7
7
|
let _langchain_google_vertexai = require("@langchain/google-vertexai");
|
|
8
8
|
//#region src/llm/init.ts
|
|
9
9
|
/**
|
|
@@ -17,7 +17,7 @@ let _langchain_google_vertexai = require("@langchain/google-vertexai");
|
|
|
17
17
|
*/
|
|
18
18
|
function initializeModel({ provider, clientOptions, tools, override }) {
|
|
19
19
|
const model = override ?? new (require_providers.getChatModelClass(provider))(clientOptions ?? {});
|
|
20
|
-
if (require_llm.isOpenAILike(provider) && (model instanceof require_index$
|
|
20
|
+
if (require_llm.isOpenAILike(provider) && (model instanceof require_index$2.ChatOpenAI || model instanceof require_index$2.AzureChatOpenAI)) {
|
|
21
21
|
const opts = clientOptions;
|
|
22
22
|
if (opts) {
|
|
23
23
|
model.temperature = opts.temperature;
|
package/dist/cjs/llm/invoke.cjs
CHANGED
|
@@ -7,13 +7,13 @@ const require_events = require("../utils/events.cjs");
|
|
|
7
7
|
const require_alternation = require("../messages/alternation.cjs");
|
|
8
8
|
const require_handoffCue = require("../messages/handoffCue.cjs");
|
|
9
9
|
require("../messages/index.cjs");
|
|
10
|
+
const require_llm = require("../utils/llm.cjs");
|
|
10
11
|
const require_streamLimits = require("./streamLimits.cjs");
|
|
11
12
|
const require_toolOutputReferences = require("../tools/toolOutputReferences.cjs");
|
|
12
|
-
const require_llm = require("../utils/llm.cjs");
|
|
13
13
|
const require_stream = require("../stream.cjs");
|
|
14
|
+
const require_errors = require("../utils/errors.cjs");
|
|
14
15
|
const require_truncation = require("./truncation.cjs");
|
|
15
16
|
const require_providers = require("./providers.cjs");
|
|
16
|
-
const require_errors = require("../utils/errors.cjs");
|
|
17
17
|
const require_preempt = require("./preempt.cjs");
|
|
18
18
|
const require_init = require("./init.cjs");
|
|
19
19
|
let _langchain_core_runnables = require("@langchain/core/runnables");
|