Improve in-flight cancel latency via run abort signal propagation
This commit is contained in:
@@ -700,6 +700,7 @@ Cancel the current agent operation.
|
|||||||
|
|
||||||
Used by the web dashboard/chat stop button and channel-level `/stop` / `/cancel` command fast-paths.
|
Used by the web dashboard/chat stop button and channel-level `/stop` / `/cancel` command fast-paths.
|
||||||
Cancellation is best-effort and stops at the next agent/tool-loop safe point.
|
Cancellation is best-effort and stops at the next agent/tool-loop safe point.
|
||||||
|
Flynn now propagates a run-level abort signal into model/tool execution, so providers/tools that honor `AbortSignal` typically stop promptly instead of waiting for request/tool timeouts.
|
||||||
|
|
||||||
### Node Methods
|
### Node Methods
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export interface ToolExecutionContext {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`signal` is wired to run-level cancellation (`agent.cancel`, `/stop`, `/cancel`) and tool timeout. Tools should honor it for fast cooperative aborts.
|
||||||
|
|
||||||
### ToolResult
|
### ToolResult
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -3,6 +3,27 @@
|
|||||||
"updated_at": "2026-02-19",
|
"updated_at": "2026-02-19",
|
||||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||||
"plans": {
|
"plans": {
|
||||||
|
"faster-inflight-cancel-propagation": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-19",
|
||||||
|
"updated": "2026-02-19",
|
||||||
|
"summary": "Reduced cancellation latency by wiring a run-level AbortSignal from NativeAgent into model and tool execution. `agent.cancel`/`/stop` now abort in-flight model/tool awaits immediately for signal-aware providers/tools instead of waiting for timeout/safe-point boundaries.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/backends/native/agent.ts",
|
||||||
|
"src/tools/executor.ts",
|
||||||
|
"src/models/types.ts",
|
||||||
|
"src/models/openai.ts",
|
||||||
|
"src/models/github.ts",
|
||||||
|
"src/models/anthropic.ts",
|
||||||
|
"src/models/bedrock.ts",
|
||||||
|
"src/models/local/llamacpp.ts",
|
||||||
|
"src/backends/native/agent.test.ts",
|
||||||
|
"docs/api/PROTOCOL.md",
|
||||||
|
"docs/api/TOOLS.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/backends/native/agent.test.ts src/tools/executor.test.ts src/models/router.test.ts src/gateway/handlers/agent.test.ts + pnpm typecheck passing"
|
||||||
|
},
|
||||||
"makefile-skills-convenience-targets": {
|
"makefile-skills-convenience-targets": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-17",
|
"date": "2026-02-17",
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ describe('NativeAgent', () => {
|
|||||||
const response = await agent.process('Hi');
|
const response = await agent.process('Hi');
|
||||||
|
|
||||||
expect(response).toBe('Hello!');
|
expect(response).toBe('Hello!');
|
||||||
expect(mockClient.chat).toHaveBeenCalledWith({
|
expect(mockClient.chat).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
messages: [{ role: 'user', content: 'Hi' }],
|
messages: [{ role: 'user', content: 'Hi' }],
|
||||||
system: 'You are helpful.',
|
system: 'You are helpful.',
|
||||||
});
|
signal: expect.any(AbortSignal),
|
||||||
|
}));
|
||||||
|
|
||||||
const history = agent.getHistory();
|
const history = agent.getHistory();
|
||||||
expect(history).toHaveLength(2);
|
expect(history).toHaveLength(2);
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export class NativeAgent {
|
|||||||
private _lastToolFingerprint?: string;
|
private _lastToolFingerprint?: string;
|
||||||
private _cancelRequested = false;
|
private _cancelRequested = false;
|
||||||
private _runInProgress = false;
|
private _runInProgress = false;
|
||||||
|
private _runAbortController?: AbortController;
|
||||||
private modelTimeoutMs: number;
|
private modelTimeoutMs: number;
|
||||||
|
|
||||||
constructor(config: NativeAgentConfig) {
|
constructor(config: NativeAgentConfig) {
|
||||||
@@ -106,6 +107,7 @@ export class NativeAgent {
|
|||||||
|
|
||||||
async process(userMessage: string, attachments?: Attachment[]): Promise<string> {
|
async process(userMessage: string, attachments?: Attachment[]): Promise<string> {
|
||||||
this._cancelRequested = false;
|
this._cancelRequested = false;
|
||||||
|
this._runAbortController = new AbortController();
|
||||||
if ('clearAbort' in this.modelClient && typeof this.modelClient.clearAbort === 'function') {
|
if ('clearAbort' in this.modelClient && typeof this.modelClient.clearAbort === 'function') {
|
||||||
this.modelClient.clearAbort();
|
this.modelClient.clearAbort();
|
||||||
}
|
}
|
||||||
@@ -144,6 +146,7 @@ export class NativeAgent {
|
|||||||
} finally {
|
} finally {
|
||||||
this._runInProgress = false;
|
this._runInProgress = false;
|
||||||
this._cancelRequested = false;
|
this._cancelRequested = false;
|
||||||
|
this._runAbortController = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +356,9 @@ export class NativeAgent {
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const result = await toolExecutor.execute(internalName, tc.args, perCallContext);
|
const result = await toolExecutor.execute(internalName, tc.args, perCallContext, {
|
||||||
|
signal: this._runAbortController?.signal,
|
||||||
|
});
|
||||||
|
|
||||||
this.onToolUse?.({ type: 'end', tool: internalName, result });
|
this.onToolUse?.({ type: 'end', tool: internalName, result });
|
||||||
|
|
||||||
@@ -426,11 +431,22 @@ export class NativeAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async chatWithRouter(request: ChatRequest): Promise<ChatResponse> {
|
private async chatWithRouter(request: ChatRequest): Promise<ChatResponse> {
|
||||||
|
const runSignal = this._runAbortController?.signal;
|
||||||
|
const requestSignal = request.signal;
|
||||||
|
const signal = runSignal && requestSignal
|
||||||
|
? AbortSignal.any([runSignal, requestSignal])
|
||||||
|
: (runSignal ?? requestSignal);
|
||||||
|
|
||||||
|
const requestWithSignal = signal
|
||||||
|
? { ...request, signal }
|
||||||
|
: request;
|
||||||
|
|
||||||
const requestPromise = 'getClient' in this.modelClient
|
const requestPromise = 'getClient' in this.modelClient
|
||||||
? (this.modelClient as ModelRouter).chat(request, this.currentTier)
|
? (this.modelClient as ModelRouter).chat(requestWithSignal, this.currentTier)
|
||||||
: this.modelClient.chat(request);
|
: this.modelClient.chat(requestWithSignal);
|
||||||
|
|
||||||
let timer: NodeJS.Timeout | undefined;
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
let abortCleanup: (() => void) | undefined;
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
const error = new Error(`Model request timed out after ${this.modelTimeoutMs}ms`);
|
const error = new Error(`Model request timed out after ${this.modelTimeoutMs}ms`);
|
||||||
@@ -439,13 +455,31 @@ export class NativeAgent {
|
|||||||
}, this.modelTimeoutMs);
|
}, this.modelTimeoutMs);
|
||||||
timer.unref?.();
|
timer.unref?.();
|
||||||
});
|
});
|
||||||
|
const abortPromise = signal
|
||||||
|
? new Promise<never>((_, reject) => {
|
||||||
|
if (signal.aborted) {
|
||||||
|
const error = new Error('Operation cancelled by user.');
|
||||||
|
error.name = 'AbortError';
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onAbort = () => {
|
||||||
|
const error = new Error('Operation cancelled by user.');
|
||||||
|
error.name = 'AbortError';
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
signal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
abortCleanup = () => signal.removeEventListener('abort', onAbort);
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await Promise.race([requestPromise, timeoutPromise]);
|
return await Promise.race([requestPromise, timeoutPromise, ...(abortPromise ? [abortPromise] : [])]);
|
||||||
} finally {
|
} finally {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
abortCleanup?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +578,7 @@ export class NativeAgent {
|
|||||||
cancel(): void {
|
cancel(): void {
|
||||||
if (this._runInProgress) {
|
if (this._runInProgress) {
|
||||||
this._cancelRequested = true;
|
this._cancelRequested = true;
|
||||||
|
this._runAbortController?.abort();
|
||||||
if ('requestAbort' in this.modelClient && typeof this.modelClient.requestAbort === 'function') {
|
if ('requestAbort' in this.modelClient && typeof this.modelClient.requestAbort === 'function') {
|
||||||
this.modelClient.requestAbort();
|
this.modelClient.requestAbort();
|
||||||
}
|
}
|
||||||
@@ -555,7 +590,7 @@ export class NativeAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private throwIfCancelled(): void {
|
private throwIfCancelled(): void {
|
||||||
if (!this._cancelRequested) {
|
if (!this._cancelRequested && !this._runAbortController?.signal.aborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,10 @@ export class AnthropicClient implements ModelClient {
|
|||||||
params.thinking = { type: 'enabled', budget_tokens: 4096 };
|
params.thinking = { type: 'enabled', budget_tokens: 4096 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.client.messages.create(params) as AnthropicMessage;
|
const response = await this.client.messages.create(
|
||||||
|
params,
|
||||||
|
request.signal ? { signal: request.signal } : undefined,
|
||||||
|
) as AnthropicMessage;
|
||||||
|
|
||||||
const textContent = response.content.find((c) => c.type === 'text');
|
const textContent = response.content.find((c) => c.type === 'text');
|
||||||
const content = textContent?.type === 'text' ? textContent.text : '';
|
const content = textContent?.type === 'text' ? textContent.text : '';
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export class BedrockClient implements ModelClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const command = new ConverseCommand(params);
|
const command = new ConverseCommand(params);
|
||||||
const response = await this.client.send(command);
|
const response = await this.client.send(
|
||||||
|
command,
|
||||||
|
request.signal ? { abortSignal: request.signal } : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
// Extract text and tool_use content from the response
|
// Extract text and tool_use content from the response
|
||||||
const outputContent = response.output?.message?.content ?? [];
|
const outputContent = response.output?.message?.content ?? [];
|
||||||
@@ -126,7 +129,10 @@ export class BedrockClient implements ModelClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const command = new ConverseStreamCommand(params);
|
const command = new ConverseStreamCommand(params);
|
||||||
const response = await this.client.send(command);
|
const response = await this.client.send(
|
||||||
|
command,
|
||||||
|
request.signal ? { abortSignal: request.signal } : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
let inputTokens = 0;
|
let inputTokens = 0;
|
||||||
let outputTokens = 0;
|
let outputTokens = 0;
|
||||||
|
|||||||
@@ -163,7 +163,10 @@ export class GitHubModelsClient implements ModelClient {
|
|||||||
(params as OpenAI.ChatCompletionCreateParamsNonStreaming & { reasoning_effort?: 'low' | 'medium' | 'high' }).reasoning_effort = 'medium';
|
(params as OpenAI.ChatCompletionCreateParamsNonStreaming & { reasoning_effort?: 'low' | 'medium' | 'high' }).reasoning_effort = 'medium';
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.client.chat.completions.create(params);
|
const response = await this.client.chat.completions.create(
|
||||||
|
params,
|
||||||
|
request.signal ? { signal: request.signal } : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const choice = response.choices[0];
|
const choice = response.choices[0];
|
||||||
const content = choice?.message?.content ?? '';
|
const content = choice?.message?.content ?? '';
|
||||||
@@ -237,7 +240,10 @@ export class GitHubModelsClient implements ModelClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await this.client.chat.completions.create(params);
|
const stream = await this.client.chat.completions.create(
|
||||||
|
params,
|
||||||
|
request.signal ? { signal: request.signal } : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
let totalInputTokens = 0;
|
let totalInputTokens = 0;
|
||||||
let totalOutputTokens = 0;
|
let totalOutputTokens = 0;
|
||||||
|
|||||||
@@ -247,13 +247,16 @@ export class LlamaCppClient implements ModelClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
const signal = request.signal
|
||||||
|
? AbortSignal.any([request.signal, controller.signal])
|
||||||
|
: controller.signal;
|
||||||
const timer = setTimeout(() => controller.abort(), this.requestTimeout);
|
const timer = setTimeout(() => controller.abort(), this.requestTimeout);
|
||||||
try {
|
try {
|
||||||
response = await fetch(`${this.endpoint}/v1/chat/completions`, {
|
response = await fetch(`${this.endpoint}/v1/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
signal: controller.signal,
|
signal,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
@@ -331,6 +334,7 @@ export class LlamaCppClient implements ModelClient {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
signal: request.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export class OpenAIClient implements ModelClient {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
signal: request.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -277,7 +278,10 @@ export class OpenAIClient implements ModelClient {
|
|||||||
|
|
||||||
let response: OpenAI.ChatCompletion;
|
let response: OpenAI.ChatCompletion;
|
||||||
try {
|
try {
|
||||||
response = await this.client.chat.completions.create(params);
|
response = await this.client.chat.completions.create(
|
||||||
|
params,
|
||||||
|
request.signal ? { signal: request.signal } : undefined,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const status = typeof (error as { status?: unknown })?.status === 'number'
|
const status = typeof (error as { status?: unknown })?.status === 'number'
|
||||||
? (error as { status: number }).status
|
? (error as { status: number }).status
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export interface ChatRequest {
|
|||||||
tools?: ToolDefinition[];
|
tools?: ToolDefinition[];
|
||||||
/** Enable extended thinking/reasoning mode for this request. */
|
/** Enable extended thinking/reasoning mode for this request. */
|
||||||
thinking?: boolean;
|
thinking?: boolean;
|
||||||
|
/** Optional abort signal for cancelling in-flight provider requests. */
|
||||||
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatResponse {
|
export interface ChatResponse {
|
||||||
|
|||||||
+49
-6
@@ -24,6 +24,10 @@ export interface ToolExecutionObserverEvent {
|
|||||||
timestampSeconds: number;
|
timestampSeconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolExecuteOptions {
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
export class ToolExecutor {
|
export class ToolExecutor {
|
||||||
private registry: ToolRegistry;
|
private registry: ToolRegistry;
|
||||||
private hooks: HookEngine;
|
private hooks: HookEngine;
|
||||||
@@ -64,7 +68,12 @@ export class ToolExecutor {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(toolName: string, args: unknown, context?: ToolPolicyContext): Promise<ToolResult> {
|
async execute(
|
||||||
|
toolName: string,
|
||||||
|
args: unknown,
|
||||||
|
context?: ToolPolicyContext,
|
||||||
|
options?: ToolExecuteOptions,
|
||||||
|
): Promise<ToolResult> {
|
||||||
const executionId = randomUUID();
|
const executionId = randomUUID();
|
||||||
const executionEnvironment = this.resolveEffectiveExecutionEnvironment(toolName, context);
|
const executionEnvironment = this.resolveEffectiveExecutionEnvironment(toolName, context);
|
||||||
const skillName = context?.skillName;
|
const skillName = context?.skillName;
|
||||||
@@ -279,31 +288,56 @@ export class ToolExecutor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let timeoutHandle: NodeJS.Timeout | undefined;
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
||||||
const abortController = new AbortController();
|
const timeoutAbortController = new AbortController();
|
||||||
|
const externalSignal = options?.signal;
|
||||||
|
const combinedSignal = externalSignal
|
||||||
|
? AbortSignal.any([externalSignal, timeoutAbortController.signal])
|
||||||
|
: timeoutAbortController.signal;
|
||||||
|
let externalAbortCleanup: (() => void) | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const externalAbortPromise = externalSignal
|
||||||
|
? new Promise<ToolResult>((_, reject) => {
|
||||||
|
if (externalSignal.aborted) {
|
||||||
|
const error = new Error('Operation cancelled by user.');
|
||||||
|
error.name = 'AbortError';
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onAbort = () => {
|
||||||
|
const error = new Error('Operation cancelled by user.');
|
||||||
|
error.name = 'AbortError';
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
externalSignal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
externalAbortCleanup = () => externalSignal.removeEventListener('abort', onAbort);
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
(async () => {
|
(async () => {
|
||||||
if (executionEnvironment === 'sandbox' && this.sandboxManager) {
|
if (executionEnvironment === 'sandbox' && this.sandboxManager) {
|
||||||
const sandboxSessionId = context?.sessionId ?? `${context?.channel ?? 'unknown'}:${context?.sender ?? 'unknown'}`;
|
const sandboxSessionId = context?.sessionId ?? `${context?.channel ?? 'unknown'}:${context?.sender ?? 'unknown'}`;
|
||||||
const sandbox = await this.sandboxManager.getOrCreate(sandboxSessionId);
|
const sandbox = await this.sandboxManager.getOrCreate(sandboxSessionId);
|
||||||
if (toolName === 'shell.exec') {
|
if (toolName === 'shell.exec') {
|
||||||
return createSandboxedShellTool(sandbox).execute(args, { signal: abortController.signal });
|
return createSandboxedShellTool(sandbox).execute(args, { signal: combinedSignal });
|
||||||
}
|
}
|
||||||
if (toolName === 'process.start') {
|
if (toolName === 'process.start') {
|
||||||
return createSandboxedProcessStartTool(sandbox).execute(args, { signal: abortController.signal });
|
return createSandboxedProcessStartTool(sandbox).execute(args, { signal: combinedSignal });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tool.execute(args, { signal: abortController.signal });
|
return tool.execute(args, { signal: combinedSignal });
|
||||||
})(),
|
})(),
|
||||||
new Promise<ToolResult>((_, reject) => {
|
new Promise<ToolResult>((_, reject) => {
|
||||||
timeoutHandle = setTimeout(
|
timeoutHandle = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
abortController.abort();
|
timeoutAbortController.abort();
|
||||||
reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`));
|
reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`));
|
||||||
},
|
},
|
||||||
this.defaultTimeoutMs,
|
this.defaultTimeoutMs,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
...(externalAbortPromise ? [externalAbortPromise] : []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
@@ -357,6 +391,10 @@ export class ToolExecutor {
|
|||||||
timestampSeconds: Math.floor(Date.now() / 1000),
|
timestampSeconds: Math.floor(Date.now() / 1000),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (externalSignal?.aborted && this.isAbortError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
output: '',
|
output: '',
|
||||||
@@ -366,9 +404,14 @@ export class ToolExecutor {
|
|||||||
if (timeoutHandle) {
|
if (timeoutHandle) {
|
||||||
clearTimeout(timeoutHandle);
|
clearTimeout(timeoutHandle);
|
||||||
}
|
}
|
||||||
|
externalAbortCleanup?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isAbortError(error: unknown): boolean {
|
||||||
|
return error instanceof Error && error.name === 'AbortError';
|
||||||
|
}
|
||||||
|
|
||||||
private notifyExecutionObserver(event: ToolExecutionObserverEvent): void {
|
private notifyExecutionObserver(event: ToolExecutionObserverEvent): void {
|
||||||
if (!this.executionObserver) {
|
if (!this.executionObserver) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user