feat(tools): require cancellation signal on every invocation

This commit is contained in:
Tianyi Cui
2026-07-19 23:38:54 +08:00
parent a99750f341
commit e8b95c8754
77 changed files with 1129 additions and 446 deletions

View File

@@ -231,7 +231,7 @@ export function apply(ctx: Context, config: Config): void {
// --- PreToolUse → PreToolDecision. Matcher subject is the tool name. ---
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
const turn = lastTurn(exec.agent)
const merged = await runPoint('PreToolUse', exec.name, preToolPayload(ctx, exec), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
const merged = await runPoint('PreToolUse', exec.name, preToolPayload(ctx, exec), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal })
if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' }
if (merged.decision === 'ask') return { kind: 'ask', ...merged.reason !== undefined ? { reason: merged.reason } : {} }
return next()
@@ -240,7 +240,7 @@ export function apply(ctx: Context, config: Config): void {
// --- PostToolUse → PostToolDecision. Matcher subject is the tool name. ---
ctx.on('tools/post-execute', async (exec, result, next): Promise<PostToolDecision> => {
const turn = lastTurn(exec.agent)
const merged = await runPoint('PostToolUse', exec.name, postToolPayload(ctx, exec, result), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
const merged = await runPoint('PostToolUse', exec.name, postToolPayload(ctx, exec, result), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal })
const context = contextFrom(merged)
if (merged.decision === 'deny') {
return { kind: 'block', feedback: [{ type: 'text', text: merged.reason ?? 'blocked by PostToolUse hook' }], ...context ? { additionalContexts: [context] } : {} }

View File

@@ -14,6 +14,8 @@ import { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import * as HooksClaude from '@deepseek-ai/dsh-hooks-claude'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
const testToolSignal = new AbortController().signal
/** Targeted branch coverage for the CC bridge: option arms, warn paths, no-agent
* fallbacks, contextFrom-empty, and the detached-listener catch handlers. */
@@ -145,7 +147,7 @@ export function defineCoverageCases(group: CoverageGroup): void {
ctx.tools.register(defineTool({ name: 'echo', description: 'e', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'x' }] } }))
// Call execute() directly with NO agent — the bridge's no-agent/no-turn path.
const { CallId } = await import('@deepseek-ai/dsh-llm')
const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: {} })
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: {} })
expect(ran).toBe(false)
expect(result.isError).toBe(true)
})