@agentclientprotocol/claude-agent-acp 0.67.0 → 0.69.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 +1 -0
- package/dist/acp-agent.d.ts +27 -21
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +359 -299
- package/dist/file-change-audit.d.ts +56 -0
- package/dist/file-change-audit.d.ts.map +1 -0
- package/dist/file-change-audit.js +379 -0
- package/dist/session-failure-extension.d.ts +104 -0
- package/dist/session-failure-extension.d.ts.map +1 -0
- package/dist/session-failure-extension.js +351 -0
- package/package.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { USAGE_LIMIT_ERROR_PREFIXES, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
const JETBRAINS_META_KEY = "jetbrains";
|
|
4
|
+
const AIR_META_KEY = "air";
|
|
5
|
+
const AIR_EXTENSION_VERSION_KEY = "version";
|
|
6
|
+
const AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
|
|
7
|
+
const AIR_SESSION_FAILURE_KEY = "sessionFailure";
|
|
8
|
+
const AIR_EXTENSION_VERSION = 1;
|
|
9
|
+
export function airSessionFailureCapabilityMeta(...additionalCapabilities) {
|
|
10
|
+
return {
|
|
11
|
+
[JETBRAINS_META_KEY]: {
|
|
12
|
+
[AIR_META_KEY]: {
|
|
13
|
+
[AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
|
|
14
|
+
[AIR_EXTENSION_CAPABILITIES_KEY]: [AIR_SESSION_FAILURE_KEY, ...additionalCapabilities],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function createSessionFailureState() {
|
|
20
|
+
return {
|
|
21
|
+
epoch: randomUUID(),
|
|
22
|
+
revisions: new Map(),
|
|
23
|
+
active: new Map(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const AIR_FAILURE_POLICY = {
|
|
27
|
+
advisory: {
|
|
28
|
+
category: "unknown",
|
|
29
|
+
actions: [],
|
|
30
|
+
fallbackTitle: "Claude reported a notice.",
|
|
31
|
+
},
|
|
32
|
+
auth_required: {
|
|
33
|
+
category: "access",
|
|
34
|
+
actions: ["login"],
|
|
35
|
+
fallbackTitle: "Sign in to continue using Claude.",
|
|
36
|
+
},
|
|
37
|
+
bad_request: {
|
|
38
|
+
category: "request",
|
|
39
|
+
actions: [],
|
|
40
|
+
fallbackTitle: "Claude could not process this request.",
|
|
41
|
+
},
|
|
42
|
+
budget_exhausted: {
|
|
43
|
+
category: "limit",
|
|
44
|
+
actions: ["new_session"],
|
|
45
|
+
fallbackTitle: "This Claude session reached its configured budget.",
|
|
46
|
+
},
|
|
47
|
+
context_exhausted: {
|
|
48
|
+
category: "limit",
|
|
49
|
+
actions: ["new_session"],
|
|
50
|
+
fallbackTitle: "This Claude turn reached its configured limit.",
|
|
51
|
+
},
|
|
52
|
+
internal_error: {
|
|
53
|
+
category: "service",
|
|
54
|
+
actions: ["retry", "new_session"],
|
|
55
|
+
fallbackTitle: "Claude Agent encountered an internal error.",
|
|
56
|
+
},
|
|
57
|
+
overloaded: {
|
|
58
|
+
category: "service",
|
|
59
|
+
actions: ["retry"],
|
|
60
|
+
fallbackTitle: "Claude is temporarily overloaded.",
|
|
61
|
+
},
|
|
62
|
+
provider_error: {
|
|
63
|
+
category: "service",
|
|
64
|
+
actions: ["retry"],
|
|
65
|
+
fallbackTitle: "The model provider reported an error.",
|
|
66
|
+
},
|
|
67
|
+
quota_exhausted: {
|
|
68
|
+
category: "limit",
|
|
69
|
+
actions: [],
|
|
70
|
+
fallbackTitle: "The Claude account has no available quota.",
|
|
71
|
+
},
|
|
72
|
+
rate_limited: {
|
|
73
|
+
category: "limit",
|
|
74
|
+
actions: ["retry"],
|
|
75
|
+
fallbackTitle: "Claude is temporarily rate limited.",
|
|
76
|
+
},
|
|
77
|
+
transport_lost: {
|
|
78
|
+
category: "connection",
|
|
79
|
+
actions: ["new_session"],
|
|
80
|
+
fallbackTitle: "The connection to Claude was lost.",
|
|
81
|
+
},
|
|
82
|
+
worker_shutdown: {
|
|
83
|
+
category: "connection",
|
|
84
|
+
actions: ["new_session"],
|
|
85
|
+
fallbackTitle: "The Claude worker is shutting down.",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
export function sessionFailureMeta(failure) {
|
|
89
|
+
return {
|
|
90
|
+
[JETBRAINS_META_KEY]: {
|
|
91
|
+
[AIR_META_KEY]: {
|
|
92
|
+
[AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
|
|
93
|
+
[AIR_SESSION_FAILURE_KEY]: {
|
|
94
|
+
id: failure.id,
|
|
95
|
+
revision: failure.revision,
|
|
96
|
+
category: failure.category,
|
|
97
|
+
severity: failure.severity,
|
|
98
|
+
title: failure.title,
|
|
99
|
+
...(failure.details ? { details: failure.details } : {}),
|
|
100
|
+
actions: failure.actions,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** `getSessionMessages` deliberately exposes only the API message and strips
|
|
107
|
+
* transcript-level `error` / `isApiErrorMessage` fields. The SDK exports the
|
|
108
|
+
* exact stable prefixes used by its synthetic usage-limit errors, so replay
|
|
109
|
+
* can recover this one typed failure without matching arbitrary model prose. */
|
|
110
|
+
export function assistantMessageText(apiMessage) {
|
|
111
|
+
if (!apiMessage || typeof apiMessage !== "object")
|
|
112
|
+
return undefined;
|
|
113
|
+
const { content } = apiMessage;
|
|
114
|
+
if (typeof content === "string")
|
|
115
|
+
return content || undefined;
|
|
116
|
+
if (!Array.isArray(content))
|
|
117
|
+
return undefined;
|
|
118
|
+
const text = content
|
|
119
|
+
.map((block) => block &&
|
|
120
|
+
typeof block === "object" &&
|
|
121
|
+
"type" in block &&
|
|
122
|
+
block.type === "text" &&
|
|
123
|
+
"text" in block &&
|
|
124
|
+
typeof block.text === "string"
|
|
125
|
+
? block.text
|
|
126
|
+
: "")
|
|
127
|
+
.join("");
|
|
128
|
+
return text || undefined;
|
|
129
|
+
}
|
|
130
|
+
export function isSyntheticUsageLimitMessage(apiMessage) {
|
|
131
|
+
if (!apiMessage || typeof apiMessage !== "object")
|
|
132
|
+
return false;
|
|
133
|
+
const { model } = apiMessage;
|
|
134
|
+
if (model !== "<synthetic>")
|
|
135
|
+
return false;
|
|
136
|
+
const text = assistantMessageText(apiMessage);
|
|
137
|
+
if (!text)
|
|
138
|
+
return false;
|
|
139
|
+
return USAGE_LIMIT_ERROR_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
140
|
+
}
|
|
141
|
+
export function activeUsageLimitMessage(messages) {
|
|
142
|
+
let active;
|
|
143
|
+
for (const message of messages) {
|
|
144
|
+
if (message.type !== "assistant" || message.parent_tool_use_id !== null)
|
|
145
|
+
continue;
|
|
146
|
+
if (isSyntheticUsageLimitMessage(message.message)) {
|
|
147
|
+
const title = assistantMessageText(message.message);
|
|
148
|
+
if (title)
|
|
149
|
+
active = { uuid: message.uuid, title };
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const model = message.message && typeof message.message === "object" && "model" in message.message
|
|
153
|
+
? message.message.model
|
|
154
|
+
: undefined;
|
|
155
|
+
// A later real model answer proves the account recovered. Synthetic
|
|
156
|
+
// local-command/interruption messages do not: they can be appended while
|
|
157
|
+
// the provider remains unavailable.
|
|
158
|
+
if (model !== undefined && model !== "<synthetic>")
|
|
159
|
+
active = undefined;
|
|
160
|
+
}
|
|
161
|
+
return active;
|
|
162
|
+
}
|
|
163
|
+
export function supportsAirSessionFailures(capabilities) {
|
|
164
|
+
const jetbrains = capabilities?._meta?.[JETBRAINS_META_KEY];
|
|
165
|
+
const air = jetbrains?.[AIR_META_KEY];
|
|
166
|
+
const version = air?.[AIR_EXTENSION_VERSION_KEY];
|
|
167
|
+
const advertised = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
|
|
168
|
+
return (typeof version === "number" &&
|
|
169
|
+
Number.isFinite(version) &&
|
|
170
|
+
Number.isInteger(version) &&
|
|
171
|
+
version >= AIR_EXTENSION_VERSION &&
|
|
172
|
+
Array.isArray(advertised) &&
|
|
173
|
+
advertised.some((capability) => capability === AIR_SESSION_FAILURE_KEY));
|
|
174
|
+
}
|
|
175
|
+
function sessionFailureRecoveryPolicy(kind, turnId) {
|
|
176
|
+
switch (kind) {
|
|
177
|
+
case "advisory":
|
|
178
|
+
return "never";
|
|
179
|
+
case "auth_required":
|
|
180
|
+
return "auth_status";
|
|
181
|
+
case "transport_lost":
|
|
182
|
+
case "worker_shutdown":
|
|
183
|
+
return "runtime_rebind";
|
|
184
|
+
case "quota_exhausted":
|
|
185
|
+
return "real_model_success";
|
|
186
|
+
default:
|
|
187
|
+
return turnId ? "next_attempt" : "real_model_success";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Owns the AIR failure lifecycle for both the live consumer and history replay.
|
|
191
|
+
* Published revisions become active only after delivery succeeds; recovery only
|
|
192
|
+
* removes internal active state because transcript records are historical. */
|
|
193
|
+
export class SessionFailureController {
|
|
194
|
+
sessionId;
|
|
195
|
+
state;
|
|
196
|
+
capabilities;
|
|
197
|
+
isCurrent;
|
|
198
|
+
sendUpdate;
|
|
199
|
+
logger;
|
|
200
|
+
constructor(options) {
|
|
201
|
+
this.sessionId = options.sessionId;
|
|
202
|
+
this.state = options.state;
|
|
203
|
+
this.capabilities = options.capabilities;
|
|
204
|
+
this.isCurrent = options.isCurrent;
|
|
205
|
+
this.sendUpdate = options.sendUpdate;
|
|
206
|
+
this.logger = options.logger;
|
|
207
|
+
}
|
|
208
|
+
isSupported() {
|
|
209
|
+
return supportsAirSessionFailures(this.capabilities);
|
|
210
|
+
}
|
|
211
|
+
async emit(failure) {
|
|
212
|
+
if (!this.isCurrent()) {
|
|
213
|
+
this.logger.error(`Session ${this.sessionId}: ignored AIR session failure from a stale consumer`);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await this.sendUpdate({
|
|
218
|
+
sessionId: this.sessionId,
|
|
219
|
+
update: {
|
|
220
|
+
sessionUpdate: "session_info_update",
|
|
221
|
+
_meta: sessionFailureMeta(failure),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.logger.error(`Session ${this.sessionId}: failed to publish AIR session failure: ${error}`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
recordActive(failure) {
|
|
232
|
+
this.state.revisions.set(failure.id, failure.revision);
|
|
233
|
+
this.state.active.set(failure.id, failure);
|
|
234
|
+
this.state.lastNotice =
|
|
235
|
+
failure.kind === "advisory" ? { title: failure.title, id: failure.id } : undefined;
|
|
236
|
+
}
|
|
237
|
+
async clear(shouldClear = () => true) {
|
|
238
|
+
if (!this.isSupported())
|
|
239
|
+
return true;
|
|
240
|
+
this.state.lastNotice = undefined;
|
|
241
|
+
for (const failure of [...this.state.active.values()]) {
|
|
242
|
+
if (!shouldClear(failure))
|
|
243
|
+
continue;
|
|
244
|
+
this.state.active.delete(failure.id);
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
async prepare(kind, failureOptions = {}) {
|
|
249
|
+
if (!this.isSupported() || !this.isCurrent())
|
|
250
|
+
return undefined;
|
|
251
|
+
const policy = AIR_FAILURE_POLICY[kind];
|
|
252
|
+
const title = failureOptions.title || policy.fallbackTitle;
|
|
253
|
+
if (kind === "advisory") {
|
|
254
|
+
const id = this.state.lastNotice?.title === title
|
|
255
|
+
? this.state.lastNotice.id
|
|
256
|
+
: `${this.sessionId}:notice:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
|
|
257
|
+
if (this.state.lastNotice?.title !== title) {
|
|
258
|
+
this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
id,
|
|
262
|
+
revision: (this.state.revisions.get(id) ?? 0) + 1,
|
|
263
|
+
kind,
|
|
264
|
+
category: policy.category,
|
|
265
|
+
severity: "warning",
|
|
266
|
+
title,
|
|
267
|
+
...(failureOptions.details ? { details: failureOptions.details } : {}),
|
|
268
|
+
actions: policy.actions,
|
|
269
|
+
recoveryPolicy: sessionFailureRecoveryPolicy(kind),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const id = failureOptions.turnId && !failureOptions.sessionScoped
|
|
273
|
+
? `${failureOptions.turnId}:error`
|
|
274
|
+
: `${this.sessionId}:session-error:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
|
|
275
|
+
if (!failureOptions.turnId || failureOptions.sessionScoped) {
|
|
276
|
+
this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
id,
|
|
280
|
+
revision: (this.state.revisions.get(id) ?? 0) + 1,
|
|
281
|
+
kind,
|
|
282
|
+
category: policy.category,
|
|
283
|
+
severity: failureOptions.severity ?? "error",
|
|
284
|
+
title,
|
|
285
|
+
...(failureOptions.details ? { details: failureOptions.details } : {}),
|
|
286
|
+
actions: failureOptions.severity === "warning" ? [] : policy.actions,
|
|
287
|
+
recoveryPolicy: sessionFailureRecoveryPolicy(kind, failureOptions.turnId && !failureOptions.sessionScoped ? failureOptions.turnId : undefined),
|
|
288
|
+
...(failureOptions.turnId && !failureOptions.sessionScoped
|
|
289
|
+
? { turnId: failureOptions.turnId }
|
|
290
|
+
: {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async publish(kind, failureOptions = {}) {
|
|
294
|
+
const failure = await this.prepare(kind, failureOptions);
|
|
295
|
+
if (!failure)
|
|
296
|
+
return;
|
|
297
|
+
if (await this.emit(failure))
|
|
298
|
+
this.recordActive(failure);
|
|
299
|
+
}
|
|
300
|
+
async restore(id, kind, title, active = true) {
|
|
301
|
+
if (!this.isSupported() || !this.isCurrent())
|
|
302
|
+
return false;
|
|
303
|
+
const policy = AIR_FAILURE_POLICY[kind];
|
|
304
|
+
const failure = {
|
|
305
|
+
id,
|
|
306
|
+
revision: (this.state.revisions.get(id) ?? 0) + 1,
|
|
307
|
+
kind,
|
|
308
|
+
category: policy.category,
|
|
309
|
+
severity: "error",
|
|
310
|
+
title,
|
|
311
|
+
actions: policy.actions,
|
|
312
|
+
recoveryPolicy: sessionFailureRecoveryPolicy(kind),
|
|
313
|
+
};
|
|
314
|
+
if (!(await this.emit(failure)))
|
|
315
|
+
return false;
|
|
316
|
+
this.state.revisions.set(failure.id, failure.revision);
|
|
317
|
+
if (active) {
|
|
318
|
+
this.state.active.set(failure.id, failure);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
this.state.active.delete(failure.id);
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export function providerFailureCategory(errorKind, isUsageLimit = false) {
|
|
327
|
+
if (isUsageLimit)
|
|
328
|
+
return "quota_exhausted";
|
|
329
|
+
switch (errorKind) {
|
|
330
|
+
case "authentication_failed":
|
|
331
|
+
case "oauth_org_not_allowed":
|
|
332
|
+
return "auth_required";
|
|
333
|
+
case "billing_error":
|
|
334
|
+
return "quota_exhausted";
|
|
335
|
+
case "rate_limit":
|
|
336
|
+
return "rate_limited";
|
|
337
|
+
case "overloaded":
|
|
338
|
+
return "overloaded";
|
|
339
|
+
case "invalid_request":
|
|
340
|
+
case "model_not_found":
|
|
341
|
+
return "bad_request";
|
|
342
|
+
case "max_output_tokens":
|
|
343
|
+
return "context_exhausted";
|
|
344
|
+
case "server_error":
|
|
345
|
+
case "unknown":
|
|
346
|
+
case undefined:
|
|
347
|
+
return "provider_error";
|
|
348
|
+
default:
|
|
349
|
+
return "provider_error";
|
|
350
|
+
}
|
|
351
|
+
}
|
package/package.json
CHANGED