@agentclientprotocol/claude-agent-acp 0.70.0 → 0.71.0
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/README.md +10 -9
- package/dist/acp-agent.d.ts +105 -38
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +1085 -660
- package/dist/acp-subagents.d.ts +82 -0
- package/dist/acp-subagents.d.ts.map +1 -0
- package/dist/acp-subagents.js +13 -0
- package/dist/air-extension.d.ts +23 -0
- package/dist/air-extension.d.ts.map +1 -0
- package/dist/air-extension.js +63 -0
- package/dist/async-tasks.d.ts +109 -0
- package/dist/async-tasks.d.ts.map +1 -0
- package/dist/async-tasks.js +613 -0
- package/dist/clear-context-coordinator.d.ts +60 -0
- package/dist/clear-context-coordinator.d.ts.map +1 -0
- package/dist/clear-context-coordinator.js +80 -0
- package/dist/exit-plan.d.ts +42 -0
- package/dist/exit-plan.d.ts.map +1 -0
- package/dist/exit-plan.js +154 -0
- package/dist/file-change-audit.d.ts.map +1 -1
- package/dist/file-change-audit.js +4 -33
- package/dist/fork-session.d.ts +12 -0
- package/dist/fork-session.d.ts.map +1 -0
- package/dist/fork-session.js +41 -0
- package/dist/native-subagents.d.ts +71 -0
- package/dist/native-subagents.d.ts.map +1 -0
- package/dist/native-subagents.js +422 -0
- package/dist/permissions/effects.d.ts +19 -0
- package/dist/permissions/effects.d.ts.map +1 -0
- package/dist/permissions/effects.js +166 -0
- package/dist/permissions/modes.d.ts +8 -0
- package/dist/permissions/modes.d.ts.map +1 -0
- package/dist/permissions/modes.js +41 -0
- package/dist/permissions/normalization.d.ts +6 -0
- package/dist/permissions/normalization.d.ts.map +1 -0
- package/dist/permissions/normalization.js +100 -0
- package/dist/permissions/options/filesystem.d.ts +9 -0
- package/dist/permissions/options/filesystem.d.ts.map +1 -0
- package/dist/permissions/options/filesystem.js +124 -0
- package/dist/permissions/options/shared.d.ts +39 -0
- package/dist/permissions/options/shared.d.ts.map +1 -0
- package/dist/permissions/options/shared.js +64 -0
- package/dist/permissions/options/shell.d.ts +5 -0
- package/dist/permissions/options/shell.d.ts.map +1 -0
- package/dist/permissions/options/shell.js +100 -0
- package/dist/permissions/options/tools.d.ts +11 -0
- package/dist/permissions/options/tools.d.ts.map +1 -0
- package/dist/permissions/options/tools.js +135 -0
- package/dist/permissions/options.d.ts +5 -0
- package/dist/permissions/options.d.ts.map +1 -0
- package/dist/permissions/options.js +60 -0
- package/dist/permissions/presentation.d.ts +15 -0
- package/dist/permissions/presentation.d.ts.map +1 -0
- package/dist/permissions/presentation.js +83 -0
- package/dist/permissions/response.d.ts +11 -0
- package/dist/permissions/response.d.ts.map +1 -0
- package/dist/permissions/response.js +19 -0
- package/dist/session-config-ids.d.ts +5 -0
- package/dist/session-config-ids.d.ts.map +1 -0
- package/dist/session-config-ids.js +4 -0
- package/dist/session-failure-extension.d.ts +2 -24
- package/dist/session-failure-extension.d.ts.map +1 -1
- package/dist/session-failure-extension.js +13 -40
- package/dist/session-mode.d.ts +66 -0
- package/dist/session-mode.d.ts.map +1 -0
- package/dist/session-mode.js +234 -0
- package/dist/session-titles.d.ts +77 -0
- package/dist/session-titles.d.ts.map +1 -0
- package/dist/session-titles.js +199 -0
- package/dist/tool-result-meta.d.ts +8 -0
- package/dist/tool-result-meta.d.ts.map +1 -0
- package/dist/tool-result-meta.js +19 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +39 -18
- package/package.json +6 -6
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { ALLOW_BYPASS } from "./permissions/modes.js";
|
|
3
|
+
export const MODE_CONFIG_ID = "mode";
|
|
4
|
+
export const AUTO_MODE_FALLBACK = "acceptEdits";
|
|
5
|
+
const AUTO_MODE_FALLBACK_NOTICE = "**Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.";
|
|
6
|
+
/** Owns session-mode policy and the ACP/SDK synchronization it requires. */
|
|
7
|
+
export class SessionModeManager {
|
|
8
|
+
options;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
async initialize({ query, requestedMode, currentModelInfo, currentModelId, }) {
|
|
13
|
+
const availableModes = this.buildAvailableModes();
|
|
14
|
+
let effectiveMode = requestedMode;
|
|
15
|
+
let autoModeFallbackWarningPending = false;
|
|
16
|
+
if (effectiveMode === "auto" &&
|
|
17
|
+
currentModelInfo !== undefined &&
|
|
18
|
+
currentModelInfo.supportsAutoMode !== true) {
|
|
19
|
+
this.options.logError(`permissions.defaultMode "auto" is not available for model ` +
|
|
20
|
+
`"${currentModelId}"; falling back to "${AUTO_MODE_FALLBACK}".`);
|
|
21
|
+
effectiveMode = AUTO_MODE_FALLBACK;
|
|
22
|
+
autoModeFallbackWarningPending = true;
|
|
23
|
+
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
|
|
24
|
+
}
|
|
25
|
+
else if (!this.isAvailable(availableModes, effectiveMode)) {
|
|
26
|
+
this.options.logError(`permissions.defaultMode "${effectiveMode}" is not available in ` +
|
|
27
|
+
`this session; falling back to "default".`);
|
|
28
|
+
effectiveMode = "default";
|
|
29
|
+
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
modes: { currentModeId: effectiveMode, availableModes },
|
|
33
|
+
autoModeFallbackWarningPending,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
static configOption(modes) {
|
|
37
|
+
return {
|
|
38
|
+
id: MODE_CONFIG_ID,
|
|
39
|
+
name: "Mode",
|
|
40
|
+
description: "Session permission mode",
|
|
41
|
+
category: "mode",
|
|
42
|
+
type: "select",
|
|
43
|
+
currentValue: modes.currentModeId,
|
|
44
|
+
options: modes.availableModes.map((mode) => ({
|
|
45
|
+
value: mode.id,
|
|
46
|
+
name: mode.name,
|
|
47
|
+
description: mode.description,
|
|
48
|
+
_meta: mode._meta,
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
syncConfig(session, mode) {
|
|
53
|
+
session.modes = { ...session.modes, currentModeId: mode };
|
|
54
|
+
session.configOptions = session.configOptions.map((option) => option.id === MODE_CONFIG_ID && typeof option.currentValue === "string"
|
|
55
|
+
? { ...option, currentValue: mode }
|
|
56
|
+
: option);
|
|
57
|
+
}
|
|
58
|
+
availableModeIds(modes) {
|
|
59
|
+
return modes.availableModes.map((mode) => mode.id);
|
|
60
|
+
}
|
|
61
|
+
effectiveMode(session, requestedMode) {
|
|
62
|
+
return requestedMode === "auto" && this.isAutoUnavailable(session)
|
|
63
|
+
? AUTO_MODE_FALLBACK
|
|
64
|
+
: requestedMode;
|
|
65
|
+
}
|
|
66
|
+
applyPermissionFallback(session, permissionResult) {
|
|
67
|
+
const fallbackApplied = this.isAutoUnavailable(session) &&
|
|
68
|
+
permissionResult.behavior === "allow" &&
|
|
69
|
+
permissionResult.updatedPermissions?.some((update) => update.type === "setMode" && update.mode === "auto") === true;
|
|
70
|
+
if (!fallbackApplied || permissionResult.behavior !== "allow") {
|
|
71
|
+
return { permissionResult, fallbackApplied: false };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
permissionResult: {
|
|
75
|
+
...permissionResult,
|
|
76
|
+
updatedPermissions: permissionResult.updatedPermissions?.map((update) => update.type === "setMode" && update.mode === "auto"
|
|
77
|
+
? { ...update, mode: AUTO_MODE_FALLBACK }
|
|
78
|
+
: update),
|
|
79
|
+
},
|
|
80
|
+
fallbackApplied: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Reconcile mode after a model switch. The caller rebuilds config options
|
|
84
|
+
* before publishing the returned state change. */
|
|
85
|
+
async reconcileForModel(session, model) {
|
|
86
|
+
if (session.modes.currentModeId !== "auto" ||
|
|
87
|
+
model === undefined ||
|
|
88
|
+
model.supportsAutoMode === true) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
session.modes = {
|
|
92
|
+
availableModes: session.modes.availableModes,
|
|
93
|
+
currentModeId: AUTO_MODE_FALLBACK,
|
|
94
|
+
};
|
|
95
|
+
await this.trySyncMode(session.query, AUTO_MODE_FALLBACK, `Failed to sync permissionMode to "${AUTO_MODE_FALLBACK}" after model switch invalidated "auto":`);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
async selectMode(sessionId, modeId) {
|
|
99
|
+
const session = this.requireOpenSession(sessionId);
|
|
100
|
+
const requestedMode = this.parseMode(modeId);
|
|
101
|
+
if (!this.isAvailable(session.modes.availableModes, requestedMode)) {
|
|
102
|
+
throw new Error(`Mode ${modeId} is not available in this session`);
|
|
103
|
+
}
|
|
104
|
+
const effectiveMode = this.effectiveMode(session, requestedMode);
|
|
105
|
+
try {
|
|
106
|
+
await session.query.setPermissionMode(effectiveMode);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof Error) {
|
|
110
|
+
if (!error.message)
|
|
111
|
+
error.message = "Invalid Mode";
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
// eslint-disable-next-line preserve-caught-error
|
|
115
|
+
throw new Error("Invalid Mode");
|
|
116
|
+
}
|
|
117
|
+
if (effectiveMode !== requestedMode) {
|
|
118
|
+
await this.publishFallbackWarning(sessionId, session);
|
|
119
|
+
}
|
|
120
|
+
return effectiveMode;
|
|
121
|
+
}
|
|
122
|
+
async setSessionMode(params) {
|
|
123
|
+
const effectiveMode = await this.selectMode(params.sessionId, params.modeId);
|
|
124
|
+
if (effectiveMode !== params.modeId) {
|
|
125
|
+
await this.publishCurrent(params.sessionId, effectiveMode);
|
|
126
|
+
}
|
|
127
|
+
await this.options.updateConfigOption(params.sessionId, MODE_CONFIG_ID, effectiveMode);
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
async publishCurrent(sessionId, mode) {
|
|
131
|
+
await this.options.sessionUpdate({
|
|
132
|
+
sessionId,
|
|
133
|
+
update: { sessionUpdate: "current_mode_update", currentModeId: mode },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async publishFallbackWarning(sessionId, session) {
|
|
137
|
+
session.autoModeFallbackWarningPending = false;
|
|
138
|
+
if (session.autoModeFallbackWarningShown)
|
|
139
|
+
return;
|
|
140
|
+
session.autoModeFallbackWarningShown = true;
|
|
141
|
+
try {
|
|
142
|
+
await this.options.sessionUpdate({
|
|
143
|
+
sessionId,
|
|
144
|
+
update: {
|
|
145
|
+
sessionUpdate: "agent_message_chunk",
|
|
146
|
+
content: { type: "text", text: AUTO_MODE_FALLBACK_NOTICE },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// The fallback has already been applied; a failed advisory must not turn
|
|
152
|
+
// the successful mode change into a failed request.
|
|
153
|
+
this.options.logError(`Failed to publish Auto mode fallback warning: ${error}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async publishFallbackState(sessionId, session) {
|
|
157
|
+
await this.publishCurrent(sessionId, AUTO_MODE_FALLBACK);
|
|
158
|
+
await this.publishFallbackWarning(sessionId, session);
|
|
159
|
+
}
|
|
160
|
+
requireOpenSession(sessionId) {
|
|
161
|
+
const session = this.options.getSession(sessionId);
|
|
162
|
+
if (!session)
|
|
163
|
+
throw new Error("Session not found");
|
|
164
|
+
if (session.queryClosed) {
|
|
165
|
+
throw RequestError.internalError(undefined, this.options.sessionEndedMessage);
|
|
166
|
+
}
|
|
167
|
+
return session;
|
|
168
|
+
}
|
|
169
|
+
isAutoUnavailable(session, modelId = session.models.currentModelId) {
|
|
170
|
+
const modelInfo = session.modelInfos.find((model) => model.value === modelId);
|
|
171
|
+
return modelInfo !== undefined && modelInfo.supportsAutoMode !== true;
|
|
172
|
+
}
|
|
173
|
+
isAvailable(availableModes, modeId) {
|
|
174
|
+
return availableModes.some((mode) => mode.id === modeId);
|
|
175
|
+
}
|
|
176
|
+
parseMode(modeId) {
|
|
177
|
+
switch (modeId) {
|
|
178
|
+
case "auto":
|
|
179
|
+
case "default":
|
|
180
|
+
case "acceptEdits":
|
|
181
|
+
case "bypassPermissions":
|
|
182
|
+
case "dontAsk":
|
|
183
|
+
case "plan":
|
|
184
|
+
return modeId;
|
|
185
|
+
default:
|
|
186
|
+
throw new Error("Invalid Mode");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
buildAvailableModes() {
|
|
190
|
+
const modes = [
|
|
191
|
+
{
|
|
192
|
+
id: "default",
|
|
193
|
+
name: "Manual",
|
|
194
|
+
description: "Always ask before making changes",
|
|
195
|
+
_meta: { kind: "standard" },
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "acceptEdits",
|
|
199
|
+
name: "Accept edits",
|
|
200
|
+
description: "Automatically accept all file edits",
|
|
201
|
+
_meta: { kind: "standard" },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "plan",
|
|
205
|
+
name: "Plan",
|
|
206
|
+
description: "Create a plan before making changes",
|
|
207
|
+
_meta: { kind: "plan" },
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "auto",
|
|
211
|
+
name: "Auto",
|
|
212
|
+
description: "Claude handles permission decisions",
|
|
213
|
+
_meta: { kind: "auto_review" },
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
if (ALLOW_BYPASS) {
|
|
217
|
+
modes.push({
|
|
218
|
+
id: "bypassPermissions",
|
|
219
|
+
name: "Bypass permissions",
|
|
220
|
+
description: "Accepts all permissions",
|
|
221
|
+
_meta: { kind: "full_access" },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return modes;
|
|
225
|
+
}
|
|
226
|
+
async trySyncMode(query, mode, errorMessage) {
|
|
227
|
+
try {
|
|
228
|
+
await query.setPermissionMode(mode);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
this.options.logError(errorMessage, error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-generated session titles.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's own auto-title generation never runs under the Agent SDK — the
|
|
5
|
+
* latch that arms it is pre-set in the headless path — so `SDKSessionInfo.summary`
|
|
6
|
+
* degrades to the raw first prompt. This module asks the CLI for a real title
|
|
7
|
+
* instead, via the `generate_session_title` control request, and publishes it
|
|
8
|
+
* over ACP as a `session_info_update`.
|
|
9
|
+
*
|
|
10
|
+
* {@link SessionTitles} holds the per-session state and is owned by `Session`.
|
|
11
|
+
* `acp-agent.ts` drives it from four places: {@link SessionTitles.onPrompt} and
|
|
12
|
+
* {@link SessionTitles.onAssistantText} to collect text,
|
|
13
|
+
* {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`, and
|
|
14
|
+
* {@link SessionTitles.reset} on `conversation_reset`.
|
|
15
|
+
*/
|
|
16
|
+
import type { ContentBlock, PromptRequest } from "@agentclientprotocol/sdk";
|
|
17
|
+
import type { ClaudeAcpAgent, Session } from "./acp-agent.js";
|
|
18
|
+
export declare function sanitizeTitle(text: string): string;
|
|
19
|
+
/** Rolling text a title is generated from: `previous` plus `text`, keeping only
|
|
20
|
+
* the trailing {@link MAX_TITLE_CONTEXT_LENGTH} characters. */
|
|
21
|
+
export declare function appendTitleContext(previous: string | undefined, text: string): string;
|
|
22
|
+
/** One session's title: the text a generated title is derived from, whether a
|
|
23
|
+
* title has been settled on, and the last one published. Created with the
|
|
24
|
+
* session and reachable as `session.titles`. */
|
|
25
|
+
export declare class SessionTitles {
|
|
26
|
+
private readonly agent;
|
|
27
|
+
private readonly sessionId;
|
|
28
|
+
/** Last title pushed to the client via `session_info_update`, so an unchanged
|
|
29
|
+
* title is never re-notified. Undefined until the first push. */
|
|
30
|
+
private lastTitle?;
|
|
31
|
+
/** Rolling tail of this session's own user + assistant text, capped at
|
|
32
|
+
* {@link MAX_TITLE_CONTEXT_LENGTH}. Dropped once a title exists. */
|
|
33
|
+
private context?;
|
|
34
|
+
/** Set once this session has a title or has asked for one. A title is
|
|
35
|
+
* generated at most once per session: the SDK reports a generated title in
|
|
36
|
+
* the same field as a user `/rename`, so re-titling could silently overwrite
|
|
37
|
+
* one. Released by {@link reset}, and when generation yields nothing. */
|
|
38
|
+
private settled;
|
|
39
|
+
constructor(agent: ClaudeAcpAgent, sessionId: string);
|
|
40
|
+
/** Collect a prompt's own text for the title, skipping openers not worth
|
|
41
|
+
* titling after. No-op once the title is settled. */
|
|
42
|
+
onPrompt(prompt: PromptRequest["prompt"]): void;
|
|
43
|
+
/** Collect the assistant's own answer for the title. Chunks are appended
|
|
44
|
+
* verbatim so streamed text reassembles. No-op once the title is settled. */
|
|
45
|
+
onAssistantText(content: ContentBlock): void;
|
|
46
|
+
/** Drop the title state so the next turn re-evaluates. Used on
|
|
47
|
+
* `conversation_reset`, which mounts a fresh transcript. */
|
|
48
|
+
reset(): void;
|
|
49
|
+
/** Turn-end title handling. `idle` is the SDK's turn-over signal, so it is
|
|
50
|
+
* when a title may have landed or become generatable.
|
|
51
|
+
*
|
|
52
|
+
* `info.customTitle` is non-empty only once the session file holds a real
|
|
53
|
+
* title — a user `/rename` or one generated earlier; the SDK folds both into
|
|
54
|
+
* that one field. Adopt it and latch, so neither is ever titled over.
|
|
55
|
+
*
|
|
56
|
+
* With no title yet, ask the SDK to generate one in the background: it is a
|
|
57
|
+
* ~2s small-model call and turn-end must not wait on it. Only when that is
|
|
58
|
+
* not possible do we fall back to `summary`, which for an SDK-driven session
|
|
59
|
+
* is just the raw first prompt. */
|
|
60
|
+
onTurnEnd(session: Session): Promise<void>;
|
|
61
|
+
/** Read the SDK's stored info for this session. A missing session file or read
|
|
62
|
+
* error is non-fatal: the title is best-effort and another turn will retry. */
|
|
63
|
+
private readSessionInfo;
|
|
64
|
+
/** Notify the client of a title, unless it is the one we last sent. */
|
|
65
|
+
private publish;
|
|
66
|
+
/** Whether to ask the SDK for a generated title now: once per session, on a
|
|
67
|
+
* live query, with enough collected text for the generator to work with. */
|
|
68
|
+
private canRequest;
|
|
69
|
+
/** Generate a title from the session's own text and publish it. `persist`
|
|
70
|
+
* writes it to the session file, which is what carries it into
|
|
71
|
+
* `session/list` and `session/load`. A null or failed generation releases the
|
|
72
|
+
* latch so a later turn retries, and publishes `fallback` (the stored
|
|
73
|
+
* summary) so a backend that can't generate titles is no worse off than
|
|
74
|
+
* before. */
|
|
75
|
+
private requestGenerateTitle;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=session-titles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-titles.d.ts","sourceRoot":"","sources":["../src/session-titles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE5E,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AA0B9D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUlD;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrF;AAOD;;iDAEiD;AACjD,qBAAa,aAAa;IAgBtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAhB5B;sEACkE;IAClE,OAAO,CAAC,SAAS,CAAC,CAAS;IAE3B;yEACqE;IACrE,OAAO,CAAC,OAAO,CAAC,CAAS;IAEzB;;;8EAG0E;IAC1E,OAAO,CAAC,OAAO,CAAS;gBAGL,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM;IAGpC;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,IAAI;IAW/C;kFAC8E;IAC9E,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAO5C;iEAC6D;IAC7D,KAAK,IAAI,IAAI;IAMb;;;;;;;;;;wCAUoC;IAC9B,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BhD;oFACgF;YAClE,eAAe;IAS7B,uEAAuE;YACzD,OAAO;IAgBrB;iFAC6E;IAC7E,OAAO,CAAC,UAAU;IAUlB;;;;;kBAKc;YACA,oBAAoB;CAoCnC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-generated session titles.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's own auto-title generation never runs under the Agent SDK — the
|
|
5
|
+
* latch that arms it is pre-set in the headless path — so `SDKSessionInfo.summary`
|
|
6
|
+
* degrades to the raw first prompt. This module asks the CLI for a real title
|
|
7
|
+
* instead, via the `generate_session_title` control request, and publishes it
|
|
8
|
+
* over ACP as a `session_info_update`.
|
|
9
|
+
*
|
|
10
|
+
* {@link SessionTitles} holds the per-session state and is owned by `Session`.
|
|
11
|
+
* `acp-agent.ts` drives it from four places: {@link SessionTitles.onPrompt} and
|
|
12
|
+
* {@link SessionTitles.onAssistantText} to collect text,
|
|
13
|
+
* {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`, and
|
|
14
|
+
* {@link SessionTitles.reset} on `conversation_reset`.
|
|
15
|
+
*/
|
|
16
|
+
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
17
|
+
const MAX_TITLE_LENGTH = 256;
|
|
18
|
+
/** How much session text the title is generated from. The CLI caps its own
|
|
19
|
+
* title input at the same 1000 trailing characters. */
|
|
20
|
+
const MAX_TITLE_CONTEXT_LENGTH = 1000;
|
|
21
|
+
/** Below this the generator returns null without calling the model, so there is
|
|
22
|
+
* nothing to gain by asking yet. */
|
|
23
|
+
const MIN_TITLE_CONTEXT_LENGTH = 10;
|
|
24
|
+
export function sanitizeTitle(text) {
|
|
25
|
+
// Replace newlines and collapse whitespace
|
|
26
|
+
const sanitized = text
|
|
27
|
+
.replace(/[\r\n]+/g, " ")
|
|
28
|
+
.replace(/\s+/g, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
if (sanitized.length <= MAX_TITLE_LENGTH) {
|
|
31
|
+
return sanitized;
|
|
32
|
+
}
|
|
33
|
+
return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + "…";
|
|
34
|
+
}
|
|
35
|
+
/** Rolling text a title is generated from: `previous` plus `text`, keeping only
|
|
36
|
+
* the trailing {@link MAX_TITLE_CONTEXT_LENGTH} characters. */
|
|
37
|
+
export function appendTitleContext(previous, text) {
|
|
38
|
+
const next = (previous ?? "") + text;
|
|
39
|
+
return next.length > MAX_TITLE_CONTEXT_LENGTH ? next.slice(-MAX_TITLE_CONTEXT_LENGTH) : next;
|
|
40
|
+
}
|
|
41
|
+
/** Whether this SDK still offers on-demand title generation. */
|
|
42
|
+
function supportsTitleGeneration(query) {
|
|
43
|
+
return typeof query.generateSessionTitle === "function";
|
|
44
|
+
}
|
|
45
|
+
/** One session's title: the text a generated title is derived from, whether a
|
|
46
|
+
* title has been settled on, and the last one published. Created with the
|
|
47
|
+
* session and reachable as `session.titles`. */
|
|
48
|
+
export class SessionTitles {
|
|
49
|
+
agent;
|
|
50
|
+
sessionId;
|
|
51
|
+
/** Last title pushed to the client via `session_info_update`, so an unchanged
|
|
52
|
+
* title is never re-notified. Undefined until the first push. */
|
|
53
|
+
lastTitle;
|
|
54
|
+
/** Rolling tail of this session's own user + assistant text, capped at
|
|
55
|
+
* {@link MAX_TITLE_CONTEXT_LENGTH}. Dropped once a title exists. */
|
|
56
|
+
context;
|
|
57
|
+
/** Set once this session has a title or has asked for one. A title is
|
|
58
|
+
* generated at most once per session: the SDK reports a generated title in
|
|
59
|
+
* the same field as a user `/rename`, so re-titling could silently overwrite
|
|
60
|
+
* one. Released by {@link reset}, and when generation yields nothing. */
|
|
61
|
+
settled = false;
|
|
62
|
+
constructor(agent, sessionId) {
|
|
63
|
+
this.agent = agent;
|
|
64
|
+
this.sessionId = sessionId;
|
|
65
|
+
}
|
|
66
|
+
/** Collect a prompt's own text for the title, skipping openers not worth
|
|
67
|
+
* titling after. No-op once the title is settled. */
|
|
68
|
+
onPrompt(prompt) {
|
|
69
|
+
if (this.settled) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const promptText = prompt
|
|
73
|
+
.flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : []))
|
|
74
|
+
.join("\n");
|
|
75
|
+
this.context = appendTitleContext(this.context, `\n${promptText}\n`);
|
|
76
|
+
}
|
|
77
|
+
/** Collect the assistant's own answer for the title. Chunks are appended
|
|
78
|
+
* verbatim so streamed text reassembles. No-op once the title is settled. */
|
|
79
|
+
onAssistantText(content) {
|
|
80
|
+
if (this.settled || content.type !== "text") {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.context = appendTitleContext(this.context, content.text);
|
|
84
|
+
}
|
|
85
|
+
/** Drop the title state so the next turn re-evaluates. Used on
|
|
86
|
+
* `conversation_reset`, which mounts a fresh transcript. */
|
|
87
|
+
reset() {
|
|
88
|
+
this.settled = false;
|
|
89
|
+
this.context = undefined;
|
|
90
|
+
this.lastTitle = undefined;
|
|
91
|
+
}
|
|
92
|
+
/** Turn-end title handling. `idle` is the SDK's turn-over signal, so it is
|
|
93
|
+
* when a title may have landed or become generatable.
|
|
94
|
+
*
|
|
95
|
+
* `info.customTitle` is non-empty only once the session file holds a real
|
|
96
|
+
* title — a user `/rename` or one generated earlier; the SDK folds both into
|
|
97
|
+
* that one field. Adopt it and latch, so neither is ever titled over.
|
|
98
|
+
*
|
|
99
|
+
* With no title yet, ask the SDK to generate one in the background: it is a
|
|
100
|
+
* ~2s small-model call and turn-end must not wait on it. Only when that is
|
|
101
|
+
* not possible do we fall back to `summary`, which for an SDK-driven session
|
|
102
|
+
* is just the raw first prompt. */
|
|
103
|
+
async onTurnEnd(session) {
|
|
104
|
+
const info = await this.readSessionInfo(session);
|
|
105
|
+
if (info?.customTitle) {
|
|
106
|
+
this.settled = true;
|
|
107
|
+
this.context = undefined;
|
|
108
|
+
await this.publish(info.customTitle, info.lastModified);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const fallback = info?.summary
|
|
112
|
+
? { title: info.summary, lastModified: info.lastModified }
|
|
113
|
+
: undefined;
|
|
114
|
+
if (this.canRequest(session)) {
|
|
115
|
+
this.settled = true;
|
|
116
|
+
void this.requestGenerateTitle(session, fallback).catch((error) => {
|
|
117
|
+
this.agent.logger.error(`Session ${this.sessionId}: session title update failed: ${error}`);
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Only while this session has no title of its own: `summary` degrades to the
|
|
122
|
+
// raw first prompt, which must never overwrite a generated title if the
|
|
123
|
+
// persisted one is slow to show up in `info`.
|
|
124
|
+
if (fallback && !this.settled) {
|
|
125
|
+
await this.publish(fallback.title, fallback.lastModified);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Read the SDK's stored info for this session. A missing session file or read
|
|
129
|
+
* error is non-fatal: the title is best-effort and another turn will retry. */
|
|
130
|
+
async readSessionInfo(session) {
|
|
131
|
+
try {
|
|
132
|
+
return await getSessionInfo(this.sessionId, { dir: session.cwd });
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
this.agent.logger.error(`Session ${this.sessionId}: failed to read session info: ${error}`);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Notify the client of a title, unless it is the one we last sent. */
|
|
140
|
+
async publish(rawTitle, lastModified) {
|
|
141
|
+
const title = sanitizeTitle(rawTitle);
|
|
142
|
+
if (title === this.lastTitle) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.lastTitle = title;
|
|
146
|
+
await this.agent.client.sessionUpdate({
|
|
147
|
+
sessionId: this.sessionId,
|
|
148
|
+
update: {
|
|
149
|
+
sessionUpdate: "session_info_update",
|
|
150
|
+
title,
|
|
151
|
+
updatedAt: new Date(lastModified).toISOString(),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Whether to ask the SDK for a generated title now: once per session, on a
|
|
156
|
+
* live query, with enough collected text for the generator to work with. */
|
|
157
|
+
canRequest(session) {
|
|
158
|
+
if (this.settled || session.queryClosed || session.cancelled) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!supportsTitleGeneration(session.query)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return (this.context?.trim().length ?? 0) >= MIN_TITLE_CONTEXT_LENGTH;
|
|
165
|
+
}
|
|
166
|
+
/** Generate a title from the session's own text and publish it. `persist`
|
|
167
|
+
* writes it to the session file, which is what carries it into
|
|
168
|
+
* `session/list` and `session/load`. A null or failed generation releases the
|
|
169
|
+
* latch so a later turn retries, and publishes `fallback` (the stored
|
|
170
|
+
* summary) so a backend that can't generate titles is no worse off than
|
|
171
|
+
* before. */
|
|
172
|
+
async requestGenerateTitle(session, fallback) {
|
|
173
|
+
const description = this.context?.trim() ?? "";
|
|
174
|
+
let title = null;
|
|
175
|
+
try {
|
|
176
|
+
title =
|
|
177
|
+
(await session.query.generateSessionTitle?.(description, {
|
|
178
|
+
persist: true,
|
|
179
|
+
})) ?? null;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.agent.logger.error(`Session ${this.sessionId}: failed to generate a session title: ${error}`);
|
|
183
|
+
}
|
|
184
|
+
// A session torn down or replaced while the title was in flight must not
|
|
185
|
+
// adopt it.
|
|
186
|
+
if (this.agent.sessions[this.sessionId] !== session) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!title) {
|
|
190
|
+
this.settled = false;
|
|
191
|
+
if (fallback) {
|
|
192
|
+
await this.publish(fallback.title, fallback.lastModified);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.context = undefined;
|
|
197
|
+
await this.publish(title, Date.now());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type ToolResultMeta = {
|
|
2
|
+
nonExecutionKind: string;
|
|
3
|
+
userFeedback?: string;
|
|
4
|
+
};
|
|
5
|
+
/** Validate the SDK's currently untyped tool_result_meta sidecar. Unknown
|
|
6
|
+
* non-execution kinds are preserved so newer CLIs remain forward-compatible. */
|
|
7
|
+
export declare function parseToolResultMeta(raw: unknown): Map<string, ToolResultMeta> | undefined;
|
|
8
|
+
//# sourceMappingURL=tool-result-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-result-meta.d.ts","sourceRoot":"","sources":["../src/tool-result-meta.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;gFACgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,SAAS,CAczF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Validate the SDK's currently untyped tool_result_meta sidecar. Unknown
|
|
2
|
+
* non-execution kinds are preserved so newer CLIs remain forward-compatible. */
|
|
3
|
+
export function parseToolResultMeta(raw) {
|
|
4
|
+
if (!Array.isArray(raw))
|
|
5
|
+
return undefined;
|
|
6
|
+
let byToolUseId;
|
|
7
|
+
for (const entry of raw) {
|
|
8
|
+
if (typeof entry !== "object" || entry === null)
|
|
9
|
+
continue;
|
|
10
|
+
const { id, non_execution_kind, user_feedback } = entry;
|
|
11
|
+
if (typeof id !== "string" || typeof non_execution_kind !== "string")
|
|
12
|
+
continue;
|
|
13
|
+
(byToolUseId ??= new Map()).set(id, {
|
|
14
|
+
nonExecutionKind: non_execution_kind,
|
|
15
|
+
...(typeof user_feedback === "string" ? { userFeedback: user_feedback } : {}),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return byToolUseId;
|
|
19
|
+
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -78,7 +78,12 @@ export declare function toolUpdateFromDiffToolResponse(toolResponse: unknown): {
|
|
|
78
78
|
};
|
|
79
79
|
export declare const registerHookCallback: (toolUseID: string, { onPostToolUseHook, }: {
|
|
80
80
|
onPostToolUseHook?: (toolUseID: string, toolInput: unknown, toolResponse: unknown) => Promise<void>;
|
|
81
|
-
}) => void;
|
|
81
|
+
}, ownerId?: string) => void;
|
|
82
|
+
export declare function unregisterHookCallback(toolUseID: string): void;
|
|
83
|
+
/** PostToolUse normally follows tool_result, so keep the callback for a short
|
|
84
|
+
* grace period while still bounding retention when the hook never arrives. */
|
|
85
|
+
export declare function completeHookCallback(toolUseID: string): void;
|
|
86
|
+
export declare function clearHookCallbacks(ownerId: string): void;
|
|
82
87
|
export declare const createPostToolUseHook: (options?: {
|
|
83
88
|
onEnterPlanMode?: () => Promise<void>;
|
|
84
89
|
}) => HookCallback;
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAaL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,gBAAgB,EAKjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAGL,oBAAoB,EAEpB,6BAA6B,EAE9B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAEL,yCAAyC,EAGzC,qCAAqC,EAGrC,kCAAkC,EAGlC,+CAA+C,EAI/C,wBAAwB,EACxB,kCAAkC,EAIlC,gCAAgC,EAEhC,iCAAiC,EAClC,MAAM,sCAAsC,CAAC;AA2B9C,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED,UAAU,UAAU;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC/B,KAAK,CAAC,EAAE;QACN,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC;QACF,eAAe,CAAC,EAAE;YAChB,WAAW,EAAE,MAAM,CAAC;YACpB,IAAI,EAAE,MAAM,CAAC;SACd,CAAC;QACF,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,MAAM,CAAC;YACpB,SAAS,EAAE,MAAM,CAAC;YAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;SACvB,CAAC;KACH,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAQpE;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,GAAG,EACZ,sBAAsB,GAAE,OAAe,EACvC,GAAG,CAAC,EAAE,MAAM,GACX,QAAQ,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -211,15 +211,8 @@ export function toolInfoFromToolUse(toolUse, supportsTerminalOutput = false, cwd
|
|
|
211
211
|
}
|
|
212
212
|
case "WebSearch": {
|
|
213
213
|
const input = toolUse.input;
|
|
214
|
-
let label = input?.query ? `"${input.query}"` : "Web search";
|
|
215
|
-
if (input?.allowed_domains && input.allowed_domains.length > 0) {
|
|
216
|
-
label += ` (allowed: ${input.allowed_domains.join(", ")})`;
|
|
217
|
-
}
|
|
218
|
-
if (input?.blocked_domains && input.blocked_domains.length > 0) {
|
|
219
|
-
label += ` (blocked: ${input.blocked_domains.join(", ")})`;
|
|
220
|
-
}
|
|
221
214
|
return {
|
|
222
|
-
title:
|
|
215
|
+
title: input?.query ? `Search "${input.query}"` : "Web search",
|
|
223
216
|
kind: "fetch",
|
|
224
217
|
content: [],
|
|
225
218
|
};
|
|
@@ -284,7 +277,7 @@ export function toolInfoFromToolUse(toolUse, supportsTerminalOutput = false, cwd
|
|
|
284
277
|
case "ExitPlanMode": {
|
|
285
278
|
const planInput = toolUse.input;
|
|
286
279
|
return {
|
|
287
|
-
title: "
|
|
280
|
+
title: "Approve Plan",
|
|
288
281
|
kind: "switch_mode",
|
|
289
282
|
content: planInput?.plan
|
|
290
283
|
? [{ type: "content", content: { type: "text", text: planInput.plan } }]
|
|
@@ -1047,14 +1040,38 @@ export function toolUpdateFromDiffToolResponse(toolResponse) {
|
|
|
1047
1040
|
result.locations = locations;
|
|
1048
1041
|
return result;
|
|
1049
1042
|
}
|
|
1050
|
-
/*
|
|
1051
|
-
|
|
1043
|
+
/* Callbacks are keyed globally because the SDK hook is process-wide, but each
|
|
1044
|
+
* entry retains its owning ACP session so cancellation/teardown can release it. */
|
|
1045
|
+
const toolUseCallbacks = new Map();
|
|
1052
1046
|
/* Setup callbacks that will be called when receiving hooks from Claude Code */
|
|
1053
|
-
export const registerHookCallback = (toolUseID, { onPostToolUseHook, }) => {
|
|
1054
|
-
|
|
1047
|
+
export const registerHookCallback = (toolUseID, { onPostToolUseHook, }, ownerId) => {
|
|
1048
|
+
unregisterHookCallback(toolUseID);
|
|
1049
|
+
toolUseCallbacks.set(toolUseID, {
|
|
1050
|
+
ownerId,
|
|
1055
1051
|
onPostToolUseHook,
|
|
1056
|
-
};
|
|
1052
|
+
});
|
|
1057
1053
|
};
|
|
1054
|
+
export function unregisterHookCallback(toolUseID) {
|
|
1055
|
+
const callback = toolUseCallbacks.get(toolUseID);
|
|
1056
|
+
if (callback?.cleanupTimer)
|
|
1057
|
+
clearTimeout(callback.cleanupTimer);
|
|
1058
|
+
toolUseCallbacks.delete(toolUseID);
|
|
1059
|
+
}
|
|
1060
|
+
/** PostToolUse normally follows tool_result, so keep the callback for a short
|
|
1061
|
+
* grace period while still bounding retention when the hook never arrives. */
|
|
1062
|
+
export function completeHookCallback(toolUseID) {
|
|
1063
|
+
const callback = toolUseCallbacks.get(toolUseID);
|
|
1064
|
+
if (!callback || callback.cleanupTimer)
|
|
1065
|
+
return;
|
|
1066
|
+
callback.cleanupTimer = setTimeout(() => unregisterHookCallback(toolUseID), 30_000);
|
|
1067
|
+
callback.cleanupTimer.unref?.();
|
|
1068
|
+
}
|
|
1069
|
+
export function clearHookCallbacks(ownerId) {
|
|
1070
|
+
for (const [toolUseID, callback] of toolUseCallbacks) {
|
|
1071
|
+
if (callback.ownerId === ownerId)
|
|
1072
|
+
unregisterHookCallback(toolUseID);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1058
1075
|
/* A callback for Claude Code that is called when receiving a PostToolUse hook */
|
|
1059
1076
|
export const createPostToolUseHook = (options) => async (input, toolUseID) => {
|
|
1060
1077
|
if (input.hook_event_name === "PostToolUse") {
|
|
@@ -1063,11 +1080,15 @@ export const createPostToolUseHook = (options) => async (input, toolUseID) => {
|
|
|
1063
1080
|
await options.onEnterPlanMode();
|
|
1064
1081
|
}
|
|
1065
1082
|
if (toolUseID) {
|
|
1066
|
-
const onPostToolUseHook = toolUseCallbacks
|
|
1067
|
-
|
|
1068
|
-
|
|
1083
|
+
const onPostToolUseHook = toolUseCallbacks.get(toolUseID)?.onPostToolUseHook;
|
|
1084
|
+
try {
|
|
1085
|
+
if (onPostToolUseHook) {
|
|
1086
|
+
await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
unregisterHookCallback(toolUseID);
|
|
1069
1091
|
}
|
|
1070
|
-
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
|
|
1071
1092
|
}
|
|
1072
1093
|
}
|
|
1073
1094
|
return { continue: true };
|