@librechat/agents 3.1.75 → 3.1.77-dev.1
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/graphs/Graph.cjs +22 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestion.cjs +67 -0
- package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -0
- package/dist/cjs/hooks/HookRegistry.cjs +54 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/createToolPolicyHook.cjs +115 -0
- package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -0
- package/dist/cjs/hooks/executeHooks.cjs +40 -1
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langchain/google-common.cjs +3 -0
- package/dist/cjs/langchain/google-common.cjs.map +1 -0
- package/dist/cjs/langchain/index.cjs +86 -0
- package/dist/cjs/langchain/index.cjs.map +1 -0
- package/dist/cjs/langchain/language_models/chat_models.cjs +3 -0
- package/dist/cjs/langchain/language_models/chat_models.cjs.map +1 -0
- package/dist/cjs/langchain/messages/tool.cjs +3 -0
- package/dist/cjs/langchain/messages/tool.cjs.map +1 -0
- package/dist/cjs/langchain/messages.cjs +51 -0
- package/dist/cjs/langchain/messages.cjs.map +1 -0
- package/dist/cjs/langchain/openai.cjs +3 -0
- package/dist/cjs/langchain/openai.cjs.map +1 -0
- package/dist/cjs/langchain/prompts.cjs +11 -0
- package/dist/cjs/langchain/prompts.cjs.map +1 -0
- package/dist/cjs/langchain/runnables.cjs +19 -0
- package/dist/cjs/langchain/runnables.cjs.map +1 -0
- package/dist/cjs/langchain/tools.cjs +23 -0
- package/dist/cjs/langchain/tools.cjs.map +1 -0
- package/dist/cjs/langchain/utils/env.cjs +11 -0
- package/dist/cjs/langchain/utils/env.cjs.map +1 -0
- package/dist/cjs/llm/anthropic/index.cjs +145 -52
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +21 -14
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +1 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +5 -4
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +519 -655
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +20 -458
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +57 -175
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +5 -3
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +112 -3
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +2 -1
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +7 -6
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +73 -15
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/langchain.cjs +26 -0
- package/dist/cjs/messages/langchain.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +7 -6
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +400 -42
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +556 -56
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +55 -66
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs +189 -0
- package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tavily-search.cjs +372 -0
- package/dist/cjs/tools/search/tavily-search.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +26 -4
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/search/utils.cjs +10 -3
- package/dist/cjs/tools/search/utils.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +22 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestion.mjs +65 -0
- package/dist/esm/hitl/askUserQuestion.mjs.map +1 -0
- package/dist/esm/hooks/HookRegistry.mjs +54 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/createToolPolicyHook.mjs +113 -0
- package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -0
- package/dist/esm/hooks/executeHooks.mjs +40 -1
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langchain/google-common.mjs +2 -0
- package/dist/esm/langchain/google-common.mjs.map +1 -0
- package/dist/esm/langchain/index.mjs +5 -0
- package/dist/esm/langchain/index.mjs.map +1 -0
- package/dist/esm/langchain/language_models/chat_models.mjs +2 -0
- package/dist/esm/langchain/language_models/chat_models.mjs.map +1 -0
- package/dist/esm/langchain/messages/tool.mjs +2 -0
- package/dist/esm/langchain/messages/tool.mjs.map +1 -0
- package/dist/esm/langchain/messages.mjs +2 -0
- package/dist/esm/langchain/messages.mjs.map +1 -0
- package/dist/esm/langchain/openai.mjs +2 -0
- package/dist/esm/langchain/openai.mjs.map +1 -0
- package/dist/esm/langchain/prompts.mjs +2 -0
- package/dist/esm/langchain/prompts.mjs.map +1 -0
- package/dist/esm/langchain/runnables.mjs +2 -0
- package/dist/esm/langchain/runnables.mjs.map +1 -0
- package/dist/esm/langchain/tools.mjs +2 -0
- package/dist/esm/langchain/tools.mjs.map +1 -0
- package/dist/esm/langchain/utils/env.mjs +2 -0
- package/dist/esm/langchain/utils/env.mjs.map +1 -0
- package/dist/esm/llm/anthropic/index.mjs +146 -54
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +21 -14
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +1 -1
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +5 -4
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +520 -656
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +23 -459
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +57 -175
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +5 -3
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +7 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +2 -1
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +7 -6
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +73 -15
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/langchain.mjs +23 -0
- package/dist/esm/messages/langchain.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +7 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +400 -42
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +557 -57
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +55 -66
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-scraper.mjs +186 -0
- package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tavily-search.mjs +370 -0
- package/dist/esm/tools/search/tavily-search.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +26 -4
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/search/utils.mjs +10 -3
- package/dist/esm/tools/search/utils.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +7 -0
- package/dist/types/hitl/askUserQuestion.d.ts +55 -0
- package/dist/types/hitl/index.d.ts +6 -0
- package/dist/types/hooks/HookRegistry.d.ts +58 -0
- package/dist/types/hooks/createToolPolicyHook.d.ts +87 -0
- package/dist/types/hooks/index.d.ts +4 -1
- package/dist/types/hooks/types.d.ts +109 -3
- package/dist/types/index.d.ts +10 -0
- package/dist/types/langchain/google-common.d.ts +1 -0
- package/dist/types/langchain/index.d.ts +8 -0
- package/dist/types/langchain/language_models/chat_models.d.ts +1 -0
- package/dist/types/langchain/messages/tool.d.ts +1 -0
- package/dist/types/langchain/messages.d.ts +2 -0
- package/dist/types/langchain/openai.d.ts +1 -0
- package/dist/types/langchain/prompts.d.ts +1 -0
- package/dist/types/langchain/runnables.d.ts +2 -0
- package/dist/types/langchain/tools.d.ts +2 -0
- package/dist/types/langchain/utils/env.d.ts +1 -0
- package/dist/types/llm/anthropic/index.d.ts +22 -9
- package/dist/types/llm/anthropic/types.d.ts +5 -1
- package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
- package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
- package/dist/types/llm/openai/index.d.ts +21 -24
- package/dist/types/llm/openrouter/index.d.ts +11 -9
- package/dist/types/llm/vertexai/index.d.ts +1 -0
- package/dist/types/messages/cache.d.ts +4 -1
- package/dist/types/messages/format.d.ts +4 -1
- package/dist/types/messages/langchain.d.ts +27 -0
- package/dist/types/run.d.ts +117 -1
- package/dist/types/tools/ToolNode.d.ts +26 -1
- package/dist/types/tools/search/tavily-scraper.d.ts +19 -0
- package/dist/types/tools/search/tavily-search.d.ts +4 -0
- package/dist/types/tools/search/types.d.ts +99 -5
- package/dist/types/tools/search/utils.d.ts +2 -2
- package/dist/types/types/graph.d.ts +23 -37
- package/dist/types/types/hitl.d.ts +272 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/llm.d.ts +3 -3
- package/dist/types/types/run.d.ts +33 -0
- package/dist/types/types/stream.d.ts +1 -1
- package/dist/types/types/tools.d.ts +19 -0
- package/package.json +80 -17
- package/src/graphs/Graph.ts +33 -4
- package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
- package/src/hitl/askUserQuestion.ts +72 -0
- package/src/hitl/index.ts +7 -0
- package/src/hooks/HookRegistry.ts +71 -0
- package/src/hooks/__tests__/createToolPolicyHook.test.ts +259 -0
- package/src/hooks/createToolPolicyHook.ts +184 -0
- package/src/hooks/executeHooks.ts +50 -1
- package/src/hooks/index.ts +6 -0
- package/src/hooks/types.ts +112 -0
- package/src/index.ts +22 -0
- package/src/langchain/google-common.ts +1 -0
- package/src/langchain/index.ts +8 -0
- package/src/langchain/language_models/chat_models.ts +1 -0
- package/src/langchain/messages/tool.ts +5 -0
- package/src/langchain/messages.ts +21 -0
- package/src/langchain/openai.ts +1 -0
- package/src/langchain/prompts.ts +1 -0
- package/src/langchain/runnables.ts +7 -0
- package/src/langchain/tools.ts +8 -0
- package/src/langchain/utils/env.ts +1 -0
- package/src/llm/anthropic/index.ts +252 -84
- package/src/llm/anthropic/llm.spec.ts +751 -102
- package/src/llm/anthropic/types.ts +9 -1
- package/src/llm/anthropic/utils/message_inputs.ts +37 -19
- package/src/llm/anthropic/utils/message_outputs.ts +119 -101
- package/src/llm/bedrock/index.ts +2 -2
- package/src/llm/bedrock/llm.spec.ts +341 -0
- package/src/llm/bedrock/utils/message_inputs.ts +303 -4
- package/src/llm/bedrock/utils/message_outputs.ts +2 -1
- package/src/llm/custom-chat-models.smoke.test.ts +836 -0
- package/src/llm/google/llm.spec.ts +339 -57
- package/src/llm/google/utils/common.ts +53 -48
- package/src/llm/openai/contentBlocks.test.ts +346 -0
- package/src/llm/openai/index.ts +856 -833
- package/src/llm/openai/utils/index.ts +107 -78
- package/src/llm/openai/utils/messages.test.ts +159 -0
- package/src/llm/openrouter/index.ts +124 -247
- package/src/llm/openrouter/reasoning.test.ts +8 -1
- package/src/llm/vertexai/index.ts +11 -5
- package/src/llm/vertexai/llm.spec.ts +28 -1
- package/src/messages/cache.test.ts +4 -3
- package/src/messages/cache.ts +3 -2
- package/src/messages/core.ts +16 -9
- package/src/messages/format.ts +96 -16
- package/src/messages/formatAgentMessages.test.ts +166 -1
- package/src/messages/langchain.ts +39 -0
- package/src/messages/prune.ts +12 -8
- package/src/run.ts +456 -47
- package/src/scripts/caching.ts +2 -3
- package/src/specs/summarization.test.ts +51 -58
- package/src/tools/ToolNode.ts +706 -63
- package/src/tools/__tests__/hitl.test.ts +3593 -0
- package/src/tools/search/search.ts +83 -73
- package/src/tools/search/tavily-scraper.ts +235 -0
- package/src/tools/search/tavily-search.ts +424 -0
- package/src/tools/search/tavily.test.ts +965 -0
- package/src/tools/search/tool.ts +36 -26
- package/src/tools/search/types.ts +133 -8
- package/src/tools/search/utils.ts +13 -5
- package/src/types/graph.ts +32 -87
- package/src/types/hitl.ts +303 -0
- package/src/types/index.ts +1 -0
- package/src/types/llm.ts +3 -3
- package/src/types/run.ts +33 -0
- package/src/types/stream.ts +1 -1
- package/src/types/tools.ts +19 -0
- package/src/utils/llmConfig.ts +1 -6
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative `PreToolUse` hook factory. Lets hosts express common
|
|
3
|
+
* permission policies (allow / deny / ask lists + a global mode) without
|
|
4
|
+
* hand-rolling matching, precedence, and decision logic per-host.
|
|
5
|
+
*
|
|
6
|
+
* Maps directly to the Claude Code Agent SDK permission vocabulary
|
|
7
|
+
* (`allowed_tools` / `disallowed_tools` / `permissionMode`) so users of
|
|
8
|
+
* either SDK can think in the same terms. See the README's HITL section
|
|
9
|
+
* for the cross-walk and `docs/hooks-design-report.md` for the broader
|
|
10
|
+
* hook system context.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { HookCallback, PreToolUseHookOutput, ToolDecision } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Permission mode controlling how tool calls that match no rule are
|
|
17
|
+
* resolved. Mirrors Claude Code's `permissionMode`.
|
|
18
|
+
*
|
|
19
|
+
* - `default` — unmatched tools fall through to `'ask'` (interrupt).
|
|
20
|
+
* - `dontAsk` — unmatched tools are denied; the human is never
|
|
21
|
+
* prompted. Useful for headless / API agents where a
|
|
22
|
+
* silent denial is preferable to a hung interrupt.
|
|
23
|
+
* - `bypass` — every tool is approved, except those matching `deny`
|
|
24
|
+
* patterns. The kill switch you flip when you trust
|
|
25
|
+
* the agent and want to stop being asked. Equivalent to
|
|
26
|
+
* Claude Code's `bypassPermissions`.
|
|
27
|
+
*/
|
|
28
|
+
export type ToolPolicyMode = 'default' | 'dontAsk' | 'bypass';
|
|
29
|
+
|
|
30
|
+
export interface ToolPolicyConfig {
|
|
31
|
+
/**
|
|
32
|
+
* Global mode applied to tools that don't match any rule.
|
|
33
|
+
* Defaults to `'default'` (ask the human).
|
|
34
|
+
*/
|
|
35
|
+
mode?: ToolPolicyMode;
|
|
36
|
+
/**
|
|
37
|
+
* Tool name patterns that are auto-approved without a prompt.
|
|
38
|
+
* Patterns support glob `*` wildcards: `read_file`, `mcp:github:*`,
|
|
39
|
+
* `*search*`. Match is anchored (`^pattern$`).
|
|
40
|
+
*/
|
|
41
|
+
allow?: readonly string[];
|
|
42
|
+
/**
|
|
43
|
+
* Tool name patterns that are blocked outright. Wins over `allow`
|
|
44
|
+
* and `ask`, and overrides `mode: 'bypass'` — a deny rule always
|
|
45
|
+
* holds, matching Claude Code's "deny rules are checked first" guarantee.
|
|
46
|
+
*/
|
|
47
|
+
deny?: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* Tool name patterns that always trigger human approval, regardless
|
|
50
|
+
* of `mode: 'default'` vs `'dontAsk'`. In `mode: 'bypass'` these are
|
|
51
|
+
* still bypassed (because that's what bypass means).
|
|
52
|
+
*/
|
|
53
|
+
ask?: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Optional reason attached to the resulting `ask` / `deny` hook
|
|
56
|
+
* decision so the host UI can render why approval is required.
|
|
57
|
+
* The literal token `{tool}` is replaced with the tool name.
|
|
58
|
+
*/
|
|
59
|
+
reason?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compile a glob string with `*` wildcards into a single anchored
|
|
64
|
+
* `RegExp`. Other regex metacharacters are escaped, so `read_file.md`
|
|
65
|
+
* matches the literal dot. Patterns are short (tool names), so we do
|
|
66
|
+
* not cache here — the registry's `matchesQuery` already caches its own
|
|
67
|
+
* regex compilations and our patterns are evaluated once per ToolNode
|
|
68
|
+
* batch, not once per stream chunk.
|
|
69
|
+
*/
|
|
70
|
+
function globToRegex(pattern: string): RegExp {
|
|
71
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
72
|
+
return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Pre-compile a list of glob patterns into a single match function. */
|
|
76
|
+
function compileMatchers(
|
|
77
|
+
patterns: readonly string[] | undefined
|
|
78
|
+
): (toolName: string) => boolean {
|
|
79
|
+
if (patterns == null || patterns.length === 0) {
|
|
80
|
+
return () => false;
|
|
81
|
+
}
|
|
82
|
+
const regexes = patterns.map(globToRegex);
|
|
83
|
+
return (toolName: string): boolean => {
|
|
84
|
+
for (const regex of regexes) {
|
|
85
|
+
if (regex.test(toolName)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatReason(
|
|
94
|
+
template: string | undefined,
|
|
95
|
+
toolName: string
|
|
96
|
+
): string | undefined {
|
|
97
|
+
if (template == null) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return template.replace(/\{tool\}/g, toolName);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build a `PreToolUse` hook callback that applies a declarative tool
|
|
105
|
+
* permission policy. Register it with a `HookRegistry` and the SDK's
|
|
106
|
+
* `humanInTheLoop` machinery handles the rest:
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* const policyHook = createToolPolicyHook({
|
|
110
|
+
* mode: 'default',
|
|
111
|
+
* allow: ['read_*', 'grep', 'glob'],
|
|
112
|
+
* deny: ['delete_*'],
|
|
113
|
+
* ask: ['execute_*', 'mcp:*'],
|
|
114
|
+
* });
|
|
115
|
+
* registry.register('PreToolUse', { hooks: [policyHook] });
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* Evaluation order matches Claude Code's permission flow:
|
|
119
|
+
*
|
|
120
|
+
* 1. `deny` rule match → `'deny'` (always wins, even in `bypass`).
|
|
121
|
+
* 2. `mode === 'bypass'` → `'allow'`.
|
|
122
|
+
* 3. `allow` rule match → `'allow'`.
|
|
123
|
+
* 4. `ask` rule match → `'ask'`.
|
|
124
|
+
* 5. `mode === 'dontAsk'` → `'deny'`.
|
|
125
|
+
* 6. fallthrough → `'ask'`.
|
|
126
|
+
*
|
|
127
|
+
* The returned callback is a single `HookCallback`, not a `HookMatcher` —
|
|
128
|
+
* register it under the matcher with the pattern you want (omit the
|
|
129
|
+
* pattern to fire on every tool call, which is the typical case since
|
|
130
|
+
* the policy itself does the filtering).
|
|
131
|
+
*/
|
|
132
|
+
export function createToolPolicyHook(
|
|
133
|
+
config: ToolPolicyConfig
|
|
134
|
+
): HookCallback<'PreToolUse'> {
|
|
135
|
+
const denyMatcher = compileMatchers(config.deny);
|
|
136
|
+
const allowMatcher = compileMatchers(config.allow);
|
|
137
|
+
const askMatcher = compileMatchers(config.ask);
|
|
138
|
+
const mode: ToolPolicyMode = config.mode ?? 'default';
|
|
139
|
+
const reasonTemplate = config.reason;
|
|
140
|
+
|
|
141
|
+
return async (input): Promise<PreToolUseHookOutput> => {
|
|
142
|
+
const toolName = input.toolName;
|
|
143
|
+
const decision = decide(
|
|
144
|
+
toolName,
|
|
145
|
+
mode,
|
|
146
|
+
denyMatcher,
|
|
147
|
+
allowMatcher,
|
|
148
|
+
askMatcher
|
|
149
|
+
);
|
|
150
|
+
if (decision === 'allow') {
|
|
151
|
+
return { decision };
|
|
152
|
+
}
|
|
153
|
+
const reason = formatReason(reasonTemplate, toolName);
|
|
154
|
+
if (reason != null) {
|
|
155
|
+
return { decision, reason };
|
|
156
|
+
}
|
|
157
|
+
return { decision };
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function decide(
|
|
162
|
+
toolName: string,
|
|
163
|
+
mode: ToolPolicyMode,
|
|
164
|
+
denyMatch: (n: string) => boolean,
|
|
165
|
+
allowMatch: (n: string) => boolean,
|
|
166
|
+
askMatch: (n: string) => boolean
|
|
167
|
+
): ToolDecision {
|
|
168
|
+
if (denyMatch(toolName)) {
|
|
169
|
+
return 'deny';
|
|
170
|
+
}
|
|
171
|
+
if (mode === 'bypass') {
|
|
172
|
+
return 'allow';
|
|
173
|
+
}
|
|
174
|
+
if (allowMatch(toolName)) {
|
|
175
|
+
return 'allow';
|
|
176
|
+
}
|
|
177
|
+
if (askMatch(toolName)) {
|
|
178
|
+
return 'ask';
|
|
179
|
+
}
|
|
180
|
+
if (mode === 'dontAsk') {
|
|
181
|
+
return 'deny';
|
|
182
|
+
}
|
|
183
|
+
return 'ask';
|
|
184
|
+
}
|
|
@@ -255,6 +255,19 @@ function applyUpdatedOutput(
|
|
|
255
255
|
agg.updatedOutput = output.updatedOutput;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
function applyAllowedDecisions(
|
|
259
|
+
agg: AggregatedHookResult,
|
|
260
|
+
output: HookOutput
|
|
261
|
+
): void {
|
|
262
|
+
if (
|
|
263
|
+
!('allowedDecisions' in output) ||
|
|
264
|
+
output.allowedDecisions === undefined
|
|
265
|
+
) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
agg.allowedDecisions = output.allowedDecisions;
|
|
269
|
+
}
|
|
270
|
+
|
|
258
271
|
function fold(outcomes: readonly HookOutcome[]): AggregatedHookResult {
|
|
259
272
|
const agg = freshResult();
|
|
260
273
|
for (const outcome of outcomes) {
|
|
@@ -268,11 +281,21 @@ function fold(outcomes: readonly HookOutcome[]): AggregatedHookResult {
|
|
|
268
281
|
if (output === null) {
|
|
269
282
|
continue;
|
|
270
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Skip fire-and-forget outputs entirely: the agent has already
|
|
286
|
+
* moved on, so an async hook cannot influence the run. Background
|
|
287
|
+
* work inside the hook body still runs (we don't cancel it), it
|
|
288
|
+
* just doesn't fold into the aggregate result.
|
|
289
|
+
*/
|
|
290
|
+
if (output.async === true) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
271
293
|
applyContext(agg, output);
|
|
272
294
|
applyStopFlag(agg, output);
|
|
273
295
|
applyDecision(agg, output);
|
|
274
296
|
applyUpdatedInput(agg, output);
|
|
275
297
|
applyUpdatedOutput(agg, output);
|
|
298
|
+
applyAllowedDecisions(agg, output);
|
|
276
299
|
}
|
|
277
300
|
return agg;
|
|
278
301
|
}
|
|
@@ -371,5 +394,31 @@ export async function executeHooks(
|
|
|
371
394
|
|
|
372
395
|
const outcomes = await Promise.all(tasks);
|
|
373
396
|
reportErrors(outcomes, event, logger);
|
|
374
|
-
|
|
397
|
+
const aggregated = fold(outcomes);
|
|
398
|
+
/**
|
|
399
|
+
* Centralized `preventContinuation` propagation: when any hook (across
|
|
400
|
+
* any callsite — RunStart, PreToolUse, PostToolBatch, SubagentStop,
|
|
401
|
+
* etc.) returns `preventContinuation: true`, raise a halt signal on
|
|
402
|
+
* the registry scoped to the run's `sessionId`. `Run.processStream`
|
|
403
|
+
* polls the signal between stream events using its own id and exits
|
|
404
|
+
* cleanly, skipping the `Stop` hook (since the run is being halted,
|
|
405
|
+
* not naturally completing).
|
|
406
|
+
*
|
|
407
|
+
* First-write-wins per session inside the registry — a halt already
|
|
408
|
+
* raised by an earlier hook in the same run is preserved so the
|
|
409
|
+
* original `reason` / `source` are not clobbered. Hooks fired
|
|
410
|
+
* without a `sessionId` cannot raise a halt (there's no run for the
|
|
411
|
+
* loop to poll under), which is fine: every in-tree callsite passes
|
|
412
|
+
* `sessionId: runId`. Pre-stream callsites in `Run.processStream`
|
|
413
|
+
* still read `preventContinuation` directly off the result for an
|
|
414
|
+
* early return because they have not yet entered the stream loop.
|
|
415
|
+
*/
|
|
416
|
+
if (aggregated.preventContinuation === true && sessionId !== undefined) {
|
|
417
|
+
registry.haltRun(
|
|
418
|
+
sessionId,
|
|
419
|
+
aggregated.stopReason ?? 'preventContinuation',
|
|
420
|
+
event
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return aggregated;
|
|
375
424
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// `createSummarizeNode` (PreCompact, PostCompact), and
|
|
8
8
|
// `SubagentExecutor.execute` (SubagentStart, SubagentStop).
|
|
9
9
|
export { HookRegistry } from './HookRegistry';
|
|
10
|
+
export type { HookHaltSignal } from './HookRegistry';
|
|
10
11
|
export { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';
|
|
11
12
|
export {
|
|
12
13
|
matchesQuery,
|
|
@@ -14,6 +15,8 @@ export {
|
|
|
14
15
|
MAX_PATTERN_LENGTH,
|
|
15
16
|
MAX_CACHE_SIZE,
|
|
16
17
|
} from './matchers';
|
|
18
|
+
export { createToolPolicyHook } from './createToolPolicyHook';
|
|
19
|
+
export type { ToolPolicyMode, ToolPolicyConfig } from './createToolPolicyHook';
|
|
17
20
|
export { HOOK_EVENTS } from './types';
|
|
18
21
|
export type {
|
|
19
22
|
HookEvent,
|
|
@@ -34,6 +37,8 @@ export type {
|
|
|
34
37
|
PreToolUseHookInput,
|
|
35
38
|
PostToolUseHookInput,
|
|
36
39
|
PostToolUseFailureHookInput,
|
|
40
|
+
PostToolBatchHookInput,
|
|
41
|
+
PostToolBatchEntry,
|
|
37
42
|
PermissionDeniedHookInput,
|
|
38
43
|
SubagentStartHookInput,
|
|
39
44
|
SubagentStopHookInput,
|
|
@@ -46,6 +51,7 @@ export type {
|
|
|
46
51
|
PreToolUseHookOutput,
|
|
47
52
|
PostToolUseHookOutput,
|
|
48
53
|
PostToolUseFailureHookOutput,
|
|
54
|
+
PostToolBatchHookOutput,
|
|
49
55
|
PermissionDeniedHookOutput,
|
|
50
56
|
SubagentStartHookOutput,
|
|
51
57
|
SubagentStopHookOutput,
|
package/src/hooks/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ export const HOOK_EVENTS = [
|
|
|
15
15
|
'PreToolUse',
|
|
16
16
|
'PostToolUse',
|
|
17
17
|
'PostToolUseFailure',
|
|
18
|
+
'PostToolBatch',
|
|
18
19
|
'PermissionDenied',
|
|
19
20
|
'SubagentStart',
|
|
20
21
|
'SubagentStop',
|
|
@@ -100,6 +101,42 @@ export interface PostToolUseFailureHookInput extends BaseHookInput {
|
|
|
100
101
|
turn?: number;
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Per-tool result snapshot included in a `PostToolBatch` event. Mirrors
|
|
106
|
+
* the data PostToolUse / PostToolUseFailure get individually, but the
|
|
107
|
+
* batch view lets a single hook see the whole set so it can inject one
|
|
108
|
+
* consolidated convention/audit message rather than N per-tool ones.
|
|
109
|
+
*/
|
|
110
|
+
export interface PostToolBatchEntry {
|
|
111
|
+
toolName: string;
|
|
112
|
+
toolInput: Record<string, unknown>;
|
|
113
|
+
toolUseId: string;
|
|
114
|
+
stepId?: string;
|
|
115
|
+
turn?: number;
|
|
116
|
+
/** Successful tool output, present only when `status === 'success'`. */
|
|
117
|
+
toolOutput?: unknown;
|
|
118
|
+
/** Error message, present only when `status === 'error'`. */
|
|
119
|
+
error?: string;
|
|
120
|
+
status: 'success' | 'error';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fires once after every tool call in a single batch finishes (including
|
|
125
|
+
* any that were rejected via HITL). Lets a hook react to the batch as a
|
|
126
|
+
* whole — useful for "inject conventions once for the whole batch", batch
|
|
127
|
+
* audit logging, or coordinating cleanup that depends on knowing the full
|
|
128
|
+
* result set rather than streaming each tool's result independently.
|
|
129
|
+
*
|
|
130
|
+
* Order: fires AFTER all per-tool PostToolUse / PostToolUseFailure hooks
|
|
131
|
+
* for the same batch have completed, BEFORE the next model call. Pass an
|
|
132
|
+
* `additionalContext` to inject context for that next model turn.
|
|
133
|
+
*/
|
|
134
|
+
export interface PostToolBatchHookInput extends BaseHookInput {
|
|
135
|
+
hook_event_name: 'PostToolBatch';
|
|
136
|
+
/** All tool calls (and their outcomes) from this batch, in batch order. */
|
|
137
|
+
entries: PostToolBatchEntry[];
|
|
138
|
+
}
|
|
139
|
+
|
|
103
140
|
export interface PermissionDeniedHookInput extends BaseHookInput {
|
|
104
141
|
hook_event_name: 'PermissionDenied';
|
|
105
142
|
toolName: string;
|
|
@@ -171,6 +208,7 @@ export type HookInput =
|
|
|
171
208
|
| PreToolUseHookInput
|
|
172
209
|
| PostToolUseHookInput
|
|
173
210
|
| PostToolUseFailureHookInput
|
|
211
|
+
| PostToolBatchHookInput
|
|
174
212
|
| PermissionDeniedHookInput
|
|
175
213
|
| SubagentStartHookInput
|
|
176
214
|
| SubagentStopHookInput
|
|
@@ -186,6 +224,7 @@ export type HookInputByEvent = {
|
|
|
186
224
|
PreToolUse: PreToolUseHookInput;
|
|
187
225
|
PostToolUse: PostToolUseHookInput;
|
|
188
226
|
PostToolUseFailure: PostToolUseFailureHookInput;
|
|
227
|
+
PostToolBatch: PostToolBatchHookInput;
|
|
189
228
|
PermissionDenied: PermissionDeniedHookInput;
|
|
190
229
|
SubagentStart: SubagentStartHookInput;
|
|
191
230
|
SubagentStop: SubagentStopHookInput;
|
|
@@ -206,6 +245,56 @@ export interface BaseHookOutput {
|
|
|
206
245
|
preventContinuation?: boolean;
|
|
207
246
|
/** Reason reported alongside `preventContinuation`. */
|
|
208
247
|
stopReason?: string;
|
|
248
|
+
/**
|
|
249
|
+
* Marks this hook output as fire-and-forget for INFLUENCE only.
|
|
250
|
+
* When `true`, the SDK skips every other field on this output —
|
|
251
|
+
* `decision`, `additionalContext`, `updatedInput`,
|
|
252
|
+
* `preventContinuation`, `allowedDecisions`, `updatedOutput` are
|
|
253
|
+
* all ignored. The hook's return value cannot block, modify, or
|
|
254
|
+
* inject context, so it's safe to use for pure side effects
|
|
255
|
+
* (logging, metrics, webhooks).
|
|
256
|
+
*
|
|
257
|
+
* Important caveat: the hook's CALLBACK promise is still awaited
|
|
258
|
+
* by `executeHooks` (subject to the matcher's timeout and the
|
|
259
|
+
* default `DEFAULT_HOOK_TIMEOUT_MS`). The SDK does not
|
|
260
|
+
* speculatively detach hooks based on output shape, because the
|
|
261
|
+
* shape is only known after the promise resolves. For TRUE
|
|
262
|
+
* fire-and-forget where the agent doesn't wait at all, the hook
|
|
263
|
+
* body should detach its side effect itself and return
|
|
264
|
+
* immediately:
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```ts
|
|
268
|
+
* async (input) => {
|
|
269
|
+
* // Detach the slow work — the SDK awaits this hook's
|
|
270
|
+
* // returned promise, which resolves immediately because we
|
|
271
|
+
* // don't `await` the side effect.
|
|
272
|
+
* void sendToLoggingService(input).catch(console.error);
|
|
273
|
+
* return { async: true };
|
|
274
|
+
* };
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
277
|
+
* @example WRONG — the agent will block on the webhook
|
|
278
|
+
* ```ts
|
|
279
|
+
* async (input) => {
|
|
280
|
+
* await sendToLoggingService(input); // ← awaited, blocks
|
|
281
|
+
* return { async: true }; // returning async:true doesn't undo the await
|
|
282
|
+
* };
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* Mirrors Claude Code Agent SDK's `async` output, with the same
|
|
286
|
+
* "detach inside the hook body" pattern.
|
|
287
|
+
*/
|
|
288
|
+
async?: boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Optional advisory timeout in milliseconds for the background work
|
|
291
|
+
* a host has detached inside an `async: true` hook body. The SDK
|
|
292
|
+
* does not enforce this (the hook's own AbortSignal handling does)
|
|
293
|
+
* but the field is preserved on the wire so downstream
|
|
294
|
+
* observability can surface long-running side effects. Ignored
|
|
295
|
+
* unless `async` is true.
|
|
296
|
+
*/
|
|
297
|
+
asyncTimeout?: number;
|
|
209
298
|
}
|
|
210
299
|
|
|
211
300
|
export type RunStartHookOutput = BaseHookOutput;
|
|
@@ -229,6 +318,19 @@ export interface PreToolUseHookOutput extends BaseHookOutput {
|
|
|
229
318
|
* `updatedInput` to one hook per matcher to avoid confusing precedence.
|
|
230
319
|
*/
|
|
231
320
|
updatedInput?: Record<string, unknown>;
|
|
321
|
+
/**
|
|
322
|
+
* Restricts which decisions the host UI is allowed to surface for this
|
|
323
|
+
* tool call when the hook returns `decision: 'ask'`. Pass to lock a
|
|
324
|
+
* tool down to a subset of `'approve' | 'reject' | 'edit' | 'respond'`
|
|
325
|
+
* — for example, `['approve', 'reject']` to forbid the user from
|
|
326
|
+
* editing the tool's args or substituting a custom response.
|
|
327
|
+
*
|
|
328
|
+
* The values flow into the resulting interrupt's
|
|
329
|
+
* `review_configs[i].allowed_decisions`. Omitting the field keeps the
|
|
330
|
+
* SDK default (all four decisions advertised). Last-writer-wins in
|
|
331
|
+
* registration order, same precedence rules as `updatedInput`.
|
|
332
|
+
*/
|
|
333
|
+
allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;
|
|
232
334
|
}
|
|
233
335
|
|
|
234
336
|
export interface PostToolUseHookOutput extends BaseHookOutput {
|
|
@@ -243,6 +345,8 @@ export interface PostToolUseHookOutput extends BaseHookOutput {
|
|
|
243
345
|
|
|
244
346
|
export type PostToolUseFailureHookOutput = BaseHookOutput;
|
|
245
347
|
|
|
348
|
+
export type PostToolBatchHookOutput = BaseHookOutput;
|
|
349
|
+
|
|
246
350
|
export type PermissionDeniedHookOutput = BaseHookOutput;
|
|
247
351
|
|
|
248
352
|
export interface SubagentStartHookOutput extends BaseHookOutput {
|
|
@@ -270,6 +374,7 @@ export type HookOutputByEvent = {
|
|
|
270
374
|
PreToolUse: PreToolUseHookOutput;
|
|
271
375
|
PostToolUse: PostToolUseHookOutput;
|
|
272
376
|
PostToolUseFailure: PostToolUseFailureHookOutput;
|
|
377
|
+
PostToolBatch: PostToolBatchHookOutput;
|
|
273
378
|
PermissionDenied: PermissionDeniedHookOutput;
|
|
274
379
|
SubagentStart: SubagentStartHookOutput;
|
|
275
380
|
SubagentStop: SubagentStopHookOutput;
|
|
@@ -286,6 +391,7 @@ export type HookOutput =
|
|
|
286
391
|
| PreToolUseHookOutput
|
|
287
392
|
| PostToolUseHookOutput
|
|
288
393
|
| PostToolUseFailureHookOutput
|
|
394
|
+
| PostToolBatchHookOutput
|
|
289
395
|
| PermissionDeniedHookOutput
|
|
290
396
|
| SubagentStartHookOutput
|
|
291
397
|
| SubagentStopHookOutput
|
|
@@ -381,6 +487,12 @@ export interface AggregatedHookResult {
|
|
|
381
487
|
* hook per matcher to avoid subtle precedence bugs.
|
|
382
488
|
*/
|
|
383
489
|
updatedInput?: Record<string, unknown>;
|
|
490
|
+
/**
|
|
491
|
+
* Restricted decision set from a `PreToolUse` hook. Same last-writer-wins
|
|
492
|
+
* semantics as `updatedInput`. Surfaces to the interrupt payload's
|
|
493
|
+
* `review_configs[i].allowed_decisions`.
|
|
494
|
+
*/
|
|
495
|
+
allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;
|
|
384
496
|
/**
|
|
385
497
|
* Replacement tool output from a `PostToolUse` hook.
|
|
386
498
|
*
|
package/src/index.ts
CHANGED
|
@@ -35,9 +35,31 @@ export * from './utils';
|
|
|
35
35
|
/* Hooks */
|
|
36
36
|
export * from './hooks';
|
|
37
37
|
|
|
38
|
+
/* HITL helpers */
|
|
39
|
+
export * from './hitl';
|
|
40
|
+
|
|
38
41
|
/* Types */
|
|
39
42
|
export type * from './types';
|
|
40
43
|
|
|
44
|
+
/* LangChain compatibility facade */
|
|
45
|
+
export * from './langchain';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* HITL primitives re-exported from `@langchain/langgraph` so hosts that
|
|
49
|
+
* build durable checkpoint savers, dispatch `Command({ resume })`, or
|
|
50
|
+
* detect interrupts can do so against the same langgraph instance the
|
|
51
|
+
* SDK was compiled against — avoiding accidental dual-version drift.
|
|
52
|
+
*/
|
|
53
|
+
export {
|
|
54
|
+
Command,
|
|
55
|
+
INTERRUPT,
|
|
56
|
+
interrupt,
|
|
57
|
+
MemorySaver,
|
|
58
|
+
BaseCheckpointSaver,
|
|
59
|
+
isInterrupted,
|
|
60
|
+
} from '@langchain/langgraph';
|
|
61
|
+
export type { Interrupt } from '@langchain/langgraph';
|
|
62
|
+
|
|
41
63
|
/* LLM */
|
|
42
64
|
export { CustomOpenAIClient } from './llm/openai';
|
|
43
65
|
export { ChatOpenRouter } from './llm/openrouter';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { GoogleAIToolType } from '@langchain/google-common';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AIMessage,
|
|
3
|
+
AIMessageChunk,
|
|
4
|
+
BaseMessage,
|
|
5
|
+
BaseMessageChunk,
|
|
6
|
+
HumanMessage,
|
|
7
|
+
SystemMessage,
|
|
8
|
+
ToolMessage,
|
|
9
|
+
getBufferString,
|
|
10
|
+
isAIMessage,
|
|
11
|
+
isBaseMessage,
|
|
12
|
+
isToolMessage,
|
|
13
|
+
} from '@langchain/core/messages';
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
BaseMessageFields,
|
|
17
|
+
MessageContent,
|
|
18
|
+
MessageContentText,
|
|
19
|
+
MessageContentImageUrl,
|
|
20
|
+
UsageMetadata,
|
|
21
|
+
} from '@langchain/core/messages';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { AzureOpenAIInput } from '@langchain/openai';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PromptTemplate } from '@langchain/core/prompts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getEnvironmentVariable } from '@langchain/core/utils/env';
|