@adecore/agents 0.31.12 → 0.33.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/dist/chat/acp-backend.d.ts +4 -2
- package/dist/chat/acp-backend.js +50 -11
- package/dist/chat/acp-protocol.d.ts +29 -4
- package/dist/chat/acp-protocol.js +258 -21
- package/dist/chat/backend.d.ts +3 -0
- package/dist/chat/chat-core.js +6 -3
- package/dist/chat/chat-session.js +5 -2
- package/dist/chat/claude-backend.js +4 -4
- package/dist/chat/claude-protocol.d.ts +4 -1
- package/dist/chat/claude-protocol.js +55 -3
- package/dist/chat/codex-backend.js +4 -4
- package/dist/chat/fixtures/antigravity-1.4.1-question.json +69 -0
- package/dist/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
- package/dist/chat/fixtures/grok-1.0.62-limits.json +66 -0
- package/dist/chat/fixtures/grok-1.0.62-question.json +99 -0
- package/dist/chat/json-rpc-transport.js +2 -2
- package/dist/chat/projector.js +22 -10
- package/dist/providers/accounts/service.js +4 -0
- package/dist/providers/claude-provider.js +2 -1
- package/dist/providers/detect.d.ts +1 -1
- package/dist/providers/detect.js +6 -3
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +64 -5
- package/dist/skills.d.ts +9 -1
- package/dist/skills.js +178 -20
- package/dist/testing/fake-acp-capture.d.ts +1 -0
- package/dist/testing/fake-acp-capture.js +4 -3
- package/dist/usage/accounts.js +3 -2
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +56 -11
- package/src/chat/acp-protocol.ts +303 -24
- package/src/chat/backend.ts +5 -1
- package/src/chat/chat-core.ts +6 -3
- package/src/chat/chat-session.ts +5 -2
- package/src/chat/claude-backend.ts +4 -4
- package/src/chat/claude-protocol.ts +57 -3
- package/src/chat/codex-backend.ts +4 -4
- package/src/chat/fixtures/antigravity-1.4.1-question.json +69 -0
- package/src/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
- package/src/chat/fixtures/grok-1.0.62-limits.json +66 -0
- package/src/chat/fixtures/grok-1.0.62-question.json +99 -0
- package/src/chat/json-rpc-transport.ts +2 -2
- package/src/chat/projector.ts +24 -12
- package/src/providers/accounts/service.ts +4 -0
- package/src/providers/claude-provider.ts +2 -1
- package/src/providers/detect.ts +7 -3
- package/src/providers/registry.ts +71 -5
- package/src/skills.ts +198 -19
- package/src/testing/fake-acp-capture.ts +8 -4
- package/src/usage/accounts.ts +3 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ModelSelection } from '@adecore/agent-contracts';
|
|
1
|
+
import { type AgentKind, type ModelSelection } from '@adecore/agent-contracts';
|
|
2
2
|
import { type AcpModeMap, type AcpPermissionRequest, type AcpSessionInfo, type AcpTurnUsage } from './acp-protocol.ts';
|
|
3
3
|
import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
4
4
|
export interface AcpClientInfo {
|
|
@@ -9,6 +9,7 @@ export interface AcpClientInfo {
|
|
|
9
9
|
export declare const DEFAULT_ACP_CLIENT: AcpClientInfo;
|
|
10
10
|
export interface AcpAgentSpec {
|
|
11
11
|
name: string;
|
|
12
|
+
kind?: AgentKind;
|
|
12
13
|
env?: Record<string, string>;
|
|
13
14
|
modes?: AcpModeMap;
|
|
14
15
|
authMethod?: string | null;
|
|
@@ -60,13 +61,14 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
60
61
|
compact(): void;
|
|
61
62
|
interrupt(): void;
|
|
62
63
|
respondApproval(requestId: string, decision: ApprovalDecision): boolean;
|
|
63
|
-
respondQuestion(): boolean;
|
|
64
|
+
respondQuestion(requestId: string, answers: Record<string, string>): boolean;
|
|
64
65
|
declineRequest(requestId: string): boolean;
|
|
65
66
|
stop(): void;
|
|
66
67
|
dispose(): Promise<void>;
|
|
67
68
|
private respond;
|
|
68
69
|
private fail;
|
|
69
70
|
private handleFrame;
|
|
71
|
+
private ask;
|
|
70
72
|
private holdEarly;
|
|
71
73
|
private hostAllows;
|
|
72
74
|
private handleExit;
|
package/dist/chat/acp-backend.js
CHANGED
|
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
5
5
|
import { errorText } from '../error-text.js';
|
|
6
6
|
import { MODE_ORDER } from '../modes.js';
|
|
7
|
-
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, AcpProtocol, acpStopWords, autoAllows, parseInitialize, parseSessionInfo, promptUsageOf, resolveAcpMode, runtimeModeOf, sessionIdOf, sessionUpdateOf, thoughtLevelOf } from './acp-protocol.js';
|
|
7
|
+
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, AcpProtocol, acpErrorLimit, acpStopWords, autoAllows, GROK_QUESTION_METHODS, parseInitialize, parseSessionInfo, promptUsageOf, resolveAcpMode, runtimeModeOf, sessionIdOf, sessionUpdateOf, thoughtLevelOf } from './acp-protocol.js';
|
|
8
8
|
import { attachmentNote } from './input.js';
|
|
9
9
|
import { JsonRpcError, JsonRpcTransport } from './json-rpc-transport.js';
|
|
10
10
|
export const DEFAULT_ACP_CLIENT = { name: 'agents', title: 'Agents', version: '0.1.0' };
|
|
@@ -49,7 +49,7 @@ export class AcpBackend {
|
|
|
49
49
|
this.host = host;
|
|
50
50
|
this.spec = spec;
|
|
51
51
|
this.client = spec.client ?? DEFAULT_ACP_CLIENT;
|
|
52
|
-
this.protocol = new AcpProtocol(launch.generation, spec.modes ?? {}, acpStopWords(spec.name), spec.adoptTitles === true);
|
|
52
|
+
this.protocol = new AcpProtocol(launch.generation, spec.modes ?? {}, acpStopWords(spec.name), spec.adoptTitles === true, launch.now);
|
|
53
53
|
}
|
|
54
54
|
get running() {
|
|
55
55
|
return this.transport !== null;
|
|
@@ -328,7 +328,7 @@ export class AcpBackend {
|
|
|
328
328
|
sendTurn(input) {
|
|
329
329
|
const transport = this.transport;
|
|
330
330
|
if (!transport) {
|
|
331
|
-
this.emit({ type: 'failed', message:
|
|
331
|
+
this.emit({ type: 'failed', message: `${this.spec.name} is not running` });
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
334
|
const prompt = this.promptBlocks(input);
|
|
@@ -347,7 +347,8 @@ export class AcpBackend {
|
|
|
347
347
|
// A process that went has its exit tell the thread; anything else ends this turn alone.
|
|
348
348
|
if (this.transport === transport && transport.alive && this.prompt === turn) {
|
|
349
349
|
this.prompt = null;
|
|
350
|
-
|
|
350
|
+
const limit = error instanceof JsonRpcError ? acpErrorLimit(error.code, error.data, this.spec.kind ?? null) : null;
|
|
351
|
+
this.emitAll(this.protocol.abort(turn.promptId, turn.cancelled ? null : errorText(error), limit));
|
|
351
352
|
}
|
|
352
353
|
});
|
|
353
354
|
}
|
|
@@ -357,7 +358,7 @@ export class AcpBackend {
|
|
|
357
358
|
this.sendTurn({ text: '/compact', preamble: null, attachments: [], mentions: [], skills: [] });
|
|
358
359
|
return;
|
|
359
360
|
}
|
|
360
|
-
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} cannot
|
|
361
|
+
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} cannot compact its context.` });
|
|
361
362
|
this.emit({ type: 'turn.done', state: 'done', costUsd: 0 });
|
|
362
363
|
}
|
|
363
364
|
/* Cancels the turn: every permission request still open is answered as cancelled, and the turn ends on the prompt's response. */
|
|
@@ -372,7 +373,7 @@ export class AcpBackend {
|
|
|
372
373
|
}
|
|
373
374
|
this.prompt.cancelled = true;
|
|
374
375
|
try {
|
|
375
|
-
for (const cancelled of this.protocol.
|
|
376
|
+
for (const cancelled of this.protocol.cancelRequests()) {
|
|
376
377
|
transport.respond(cancelled.rpcId, cancelled.result);
|
|
377
378
|
this.emit({ type: 'request.withdrawn', requestId: cancelled.requestId });
|
|
378
379
|
}
|
|
@@ -393,11 +394,22 @@ export class AcpBackend {
|
|
|
393
394
|
this.emitAll(answer.events);
|
|
394
395
|
return true;
|
|
395
396
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
respondQuestion(requestId, answers) {
|
|
398
|
+
const answer = this.protocol.answerQuestion(requestId, answers);
|
|
399
|
+
if (!answer) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
if (!this.respond(requestId, answer.rpcId, answer.result)) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
this.emitAll(answer.events);
|
|
406
|
+
return true;
|
|
399
407
|
}
|
|
400
408
|
declineRequest(requestId) {
|
|
409
|
+
if (this.protocol.hasQuestion(requestId)) {
|
|
410
|
+
const cancelled = this.protocol.cancelQuestion(requestId);
|
|
411
|
+
return cancelled !== null && this.respond(requestId, cancelled.rpcId, cancelled.result);
|
|
412
|
+
}
|
|
401
413
|
return this.protocol.hasPermission(requestId) && this.respondApproval(requestId, 'deny');
|
|
402
414
|
}
|
|
403
415
|
stop() {
|
|
@@ -441,10 +453,25 @@ export class AcpBackend {
|
|
|
441
453
|
if (id === null) {
|
|
442
454
|
return;
|
|
443
455
|
}
|
|
456
|
+
if (typeof frame.method === 'string' && GROK_QUESTION_METHODS.includes(frame.method)) {
|
|
457
|
+
const question = this.protocol.question(id, this.sessionId, frame.params);
|
|
458
|
+
if (question === null) {
|
|
459
|
+
transport.respondError(id, -32602, 'Not a question of this session');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.ask(transport, question);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
444
465
|
if (frame.method !== 'session/request_permission') {
|
|
445
466
|
transport.respondError(id, -32601, `${nameOf(this.client)} does not handle ${String(frame.method)}`);
|
|
446
467
|
return;
|
|
447
468
|
}
|
|
469
|
+
// Before any runtime mode answers it: a question is never the agent's to allow.
|
|
470
|
+
const interaction = this.protocol.interaction(id, this.sessionId, frame.params);
|
|
471
|
+
if (interaction !== null) {
|
|
472
|
+
this.ask(transport, interaction);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
448
475
|
const permission = this.protocol.permission(id, this.sessionId, frame.params);
|
|
449
476
|
if (permission === null) {
|
|
450
477
|
transport.respondError(id, -32602, 'Not a permission request of this session');
|
|
@@ -452,7 +479,7 @@ export class AcpBackend {
|
|
|
452
479
|
}
|
|
453
480
|
if (this.prompt?.cancelled) {
|
|
454
481
|
this.emitAll(permission.events.filter((event) => event.type !== 'approval.requested'));
|
|
455
|
-
const answer = this.protocol.
|
|
482
|
+
const answer = this.protocol.cancelRequests().find((cancelled) => cancelled.requestId === permission.requestId);
|
|
456
483
|
if (answer) {
|
|
457
484
|
transport.respond(answer.rpcId, answer.result);
|
|
458
485
|
}
|
|
@@ -468,6 +495,18 @@ export class AcpBackend {
|
|
|
468
495
|
}
|
|
469
496
|
this.emitAll(permission.events);
|
|
470
497
|
}
|
|
498
|
+
/* Puts a question to the person, or turns it down at once when the turn is being cancelled. */
|
|
499
|
+
ask(transport, question) {
|
|
500
|
+
if (this.prompt?.cancelled) {
|
|
501
|
+
this.emitAll(question.events.filter((event) => event.type !== 'question.requested'));
|
|
502
|
+
const cancelled = this.protocol.cancelQuestion(question.requestId);
|
|
503
|
+
if (cancelled) {
|
|
504
|
+
transport.respond(cancelled.rpcId, cancelled.result);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.emitAll(question.events);
|
|
509
|
+
}
|
|
471
510
|
/* Holds an update until the session is known; of a history a load replays, only the commands, since the thread has the rest. */
|
|
472
511
|
holdEarly(params) {
|
|
473
512
|
const update = sessionUpdateOf(params);
|
|
@@ -489,7 +528,7 @@ export class AcpBackend {
|
|
|
489
528
|
}
|
|
490
529
|
this.transport = null;
|
|
491
530
|
this.prompt = null;
|
|
492
|
-
this.protocol.
|
|
531
|
+
this.protocol.cancelRequests();
|
|
493
532
|
this.emit({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
|
|
494
533
|
}
|
|
495
534
|
/* A cancelled turn still spent what it reports. A host that fails to keep it costs the record, never the end of the turn. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ChatFileChange, type RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import { type AgentKind, type ChatFileChange, type ChatTurnLimit, type RuntimeMode } from '@adecore/agent-contracts';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { ApprovalDecision, BackendEvent } from './backend.ts';
|
|
4
4
|
export declare const ACP_PROTOCOL_VERSION = 1;
|
|
@@ -9,6 +9,7 @@ declare const DiffSchema: z.ZodObject<{
|
|
|
9
9
|
oldText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
10
10
|
newText: z.ZodString;
|
|
11
11
|
}, z.core.$loose>;
|
|
12
|
+
export declare const GROK_QUESTION_METHODS: readonly string[];
|
|
12
13
|
export interface AcpTurnUsage {
|
|
13
14
|
input: number;
|
|
14
15
|
cacheRead: number;
|
|
@@ -118,20 +119,31 @@ export interface AcpPermission {
|
|
|
118
119
|
request: AcpPermissionRequest;
|
|
119
120
|
events: BackendEvent[];
|
|
120
121
|
}
|
|
122
|
+
export interface AcpQuestion {
|
|
123
|
+
requestId: string;
|
|
124
|
+
rpcId: number | string;
|
|
125
|
+
events: BackendEvent[];
|
|
126
|
+
}
|
|
121
127
|
export interface AcpStopWords {
|
|
122
128
|
maxTokens: string;
|
|
123
129
|
maxTurnRequests: string;
|
|
124
130
|
refusal: string;
|
|
131
|
+
ownAnswer: string;
|
|
125
132
|
}
|
|
126
133
|
export declare function acpStopWords(agentName: string): AcpStopWords;
|
|
134
|
+
export declare function acpErrorLimit(code: number | null, data: unknown, kind: AgentKind | null): ChatTurnLimit | null;
|
|
135
|
+
export declare function quotaResetOf(text: string, now: number): number | null;
|
|
127
136
|
export declare class AcpProtocol {
|
|
128
137
|
private readonly generation;
|
|
129
138
|
private readonly modes;
|
|
130
139
|
private readonly words;
|
|
131
140
|
private readonly tools;
|
|
132
141
|
private readonly permissions;
|
|
142
|
+
private readonly questions;
|
|
133
143
|
private text;
|
|
134
144
|
private refusalLine;
|
|
145
|
+
private quota;
|
|
146
|
+
private readonly now;
|
|
135
147
|
private thought;
|
|
136
148
|
private counter;
|
|
137
149
|
private costUsd;
|
|
@@ -139,7 +151,7 @@ export declare class AcpProtocol {
|
|
|
139
151
|
private vendorTokens;
|
|
140
152
|
private readonly prompts;
|
|
141
153
|
private readonly titles;
|
|
142
|
-
constructor(generation: number, modes: AcpModeMap, words: AcpStopWords, titles?: boolean);
|
|
154
|
+
constructor(generation: number, modes: AcpModeMap, words: AcpStopWords, titles?: boolean, now?: () => number);
|
|
143
155
|
update(sessionId: string, params: unknown): BackendEvent[];
|
|
144
156
|
private mapUpdate;
|
|
145
157
|
private vendorUsage;
|
|
@@ -152,14 +164,27 @@ export declare class AcpProtocol {
|
|
|
152
164
|
result: unknown;
|
|
153
165
|
events: BackendEvent[];
|
|
154
166
|
} | null;
|
|
155
|
-
|
|
167
|
+
question(rpcId: number | string, sessionId: string, params: unknown): AcpQuestion | null;
|
|
168
|
+
interaction(rpcId: number | string, sessionId: string, params: unknown): AcpQuestion | null;
|
|
169
|
+
private ask;
|
|
170
|
+
answerQuestion(requestId: string, answers: Record<string, string>): {
|
|
171
|
+
rpcId: number | string;
|
|
172
|
+
result: unknown;
|
|
173
|
+
events: BackendEvent[];
|
|
174
|
+
} | null;
|
|
175
|
+
cancelQuestion(requestId: string): {
|
|
176
|
+
rpcId: number | string;
|
|
177
|
+
result: unknown;
|
|
178
|
+
} | null;
|
|
179
|
+
cancelRequests(): Array<{
|
|
156
180
|
requestId: string;
|
|
157
181
|
rpcId: number | string;
|
|
158
182
|
result: unknown;
|
|
159
183
|
}>;
|
|
160
184
|
hasPermission(requestId: string): boolean;
|
|
185
|
+
hasQuestion(requestId: string): boolean;
|
|
161
186
|
finish(result: unknown, promptId: string | undefined, cancelled?: boolean): BackendEvent[];
|
|
162
|
-
abort(promptId: string | undefined, error: string | null): BackendEvent[];
|
|
187
|
+
abort(promptId: string | undefined, error: string | null, limit?: ChatTurnLimit | null): BackendEvent[];
|
|
163
188
|
private messageChunk;
|
|
164
189
|
private thoughtChunk;
|
|
165
190
|
private closeText;
|
|
@@ -39,6 +39,26 @@ const PermissionRequestSchema = z.looseObject({
|
|
|
39
39
|
toolCall: ToolCallSchema,
|
|
40
40
|
options: z.array(PermissionOptionSchema)
|
|
41
41
|
});
|
|
42
|
+
/*
|
|
43
|
+
* Grok's questions to the person (`_x.ai/ask_user_question` in xai-org/grok-build, `ask_user_question/types.rs`). The
|
|
44
|
+
* answer goes back keyed by the question's text, each a list of the labels picked.
|
|
45
|
+
*/
|
|
46
|
+
export const GROK_QUESTION_METHODS = ['_x.ai/ask_user_question', 'x.ai/ask_user_question'];
|
|
47
|
+
const GrokQuestionSchema = z.looseObject({
|
|
48
|
+
question: z.string(),
|
|
49
|
+
header: z.string().nullish(),
|
|
50
|
+
options: z.array(z.unknown()).nullish(),
|
|
51
|
+
multiSelect: z.boolean().nullish()
|
|
52
|
+
});
|
|
53
|
+
const GrokChoiceSchema = z.looseObject({ label: z.string(), description: z.string().nullish() });
|
|
54
|
+
const GrokQuestionRequestSchema = z.looseObject({ sessionId: z.string(), questions: z.array(z.unknown()) });
|
|
55
|
+
// The label Grok takes for an answer in a person's own words, which ride along as the question's `notes`.
|
|
56
|
+
const GROK_OWN_ANSWER = 'Other';
|
|
57
|
+
/*
|
|
58
|
+
* The prefix Antigravity's ACP server 1.4.1 gives the tool call of a question it asks over `session/request_permission`,
|
|
59
|
+
* one request per question with an option per choice (`ExternalOnInteractionHook`).
|
|
60
|
+
*/
|
|
61
|
+
const INTERACTION_PREFIX = 'interaction_';
|
|
42
62
|
const SessionNotificationSchema = z.looseObject({ sessionId: z.string(), update: z.looseObject({ sessionUpdate: z.string() }) });
|
|
43
63
|
const PromptResponseSchema = z.looseObject({ stopReason: z.string() });
|
|
44
64
|
const tokenCount = z.number().int().nonnegative();
|
|
@@ -325,6 +345,15 @@ const KIND_NAMES = {
|
|
|
325
345
|
const KEPT_PROMPTS = 3;
|
|
326
346
|
// What Copilot CLI 1.0.101 writes as a message of its own after a call a person turned down; the thread shows the refusal already.
|
|
327
347
|
const REFUSAL_LINE = 'Info: Operation cancelled by user';
|
|
348
|
+
/*
|
|
349
|
+
* What Antigravity's ACP server 1.4.1 writes in place of an answer once the account's quota ran out (`quota_errors.py`).
|
|
350
|
+
* Its turn still ends with `end_turn`, so this text is all that tells the limit apart.
|
|
351
|
+
*/
|
|
352
|
+
const QUOTA_HEAD = 'Usage Limit Reached\n\nYou have reached your current quota for this period.';
|
|
353
|
+
/* Whether a message may still turn out to be one the thread does not show: the refusal line after a refusal, or a limit message. */
|
|
354
|
+
function withheld(text, afterRefusal) {
|
|
355
|
+
return (afterRefusal && REFUSAL_LINE.startsWith(text)) || QUOTA_HEAD.startsWith(text) || text.startsWith(QUOTA_HEAD);
|
|
356
|
+
}
|
|
328
357
|
function foldSpace(text) {
|
|
329
358
|
return text.replace(/\s+/g, ' ').trim();
|
|
330
359
|
}
|
|
@@ -410,11 +439,91 @@ function toolOutput(tool) {
|
|
|
410
439
|
}
|
|
411
440
|
export function acpStopWords(agentName) {
|
|
412
441
|
return {
|
|
413
|
-
maxTokens: `${agentName} stopped
|
|
414
|
-
maxTurnRequests: `${agentName} stopped
|
|
415
|
-
refusal: `${agentName} refused to continue this turn
|
|
442
|
+
maxTokens: `${agentName} stopped because the reply hit its length limit.`,
|
|
443
|
+
maxTurnRequests: `${agentName} stopped because this turn hit its step limit.`,
|
|
444
|
+
refusal: `${agentName} refused to continue this turn.`,
|
|
445
|
+
ownAnswer: `${agentName} accepts only the answers it offered, so it did not get yours.`
|
|
416
446
|
};
|
|
417
447
|
}
|
|
448
|
+
// Grok's code for a request its API turned down with a 429, after its own retry (`RATE_LIMITED_ERROR_CODE` in xai-org/grok-build).
|
|
449
|
+
const GROK_RATE_LIMITED = -32003;
|
|
450
|
+
// What that 429 says when an account on the free tier spent its usage; any other 429 is a rate limit.
|
|
451
|
+
const GROK_FREE_USAGE = 'subscription:free-usage-exhausted';
|
|
452
|
+
// The stream error types of an overloaded model, as Grok passes them on in its error text.
|
|
453
|
+
const OVERLOADED_TYPES = /\b(overloaded_error|service_unavailable_error)\b/;
|
|
454
|
+
/*
|
|
455
|
+
* The limit a failed `session/prompt` names in its JSON-RPC error; null for any other failure. ACP 1 has no code
|
|
456
|
+
* for a limit, so this reads Grok 1.0.62's: -32003 after a 429, which only Grok means that way, and -32603 with the
|
|
457
|
+
* HTTP status or the stream error of an overloaded model once its own retries ran out. Neither says when a limit lifts.
|
|
458
|
+
*/
|
|
459
|
+
export function acpErrorLimit(code, data, kind) {
|
|
460
|
+
const text = typeof data === 'string' ? data : isRecord(data) && typeof data.message === 'string' ? data.message : '';
|
|
461
|
+
if (code === GROK_RATE_LIMITED && kind === 'grok') {
|
|
462
|
+
return text.includes(GROK_FREE_USAGE) ? { kind: 'usage' } : { kind: 'overload' };
|
|
463
|
+
}
|
|
464
|
+
if (code !== -32603) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const status = isRecord(data) && typeof data.http_status === 'number' ? data.http_status : null;
|
|
468
|
+
const overloaded = status === 529 || OVERLOADED_TYPES.test(text) || (status !== null && status >= 500 && /overloaded/i.test(text));
|
|
469
|
+
return overloaded ? { kind: 'overload' } : null;
|
|
470
|
+
}
|
|
471
|
+
function quotaLimit(quota) {
|
|
472
|
+
return { kind: 'usage', ...(quota.resetsAt === null ? {} : { resetsAt: quota.resetsAt }) };
|
|
473
|
+
}
|
|
474
|
+
const UNIT_MS = { second: 1000, minute: 60_000, hour: 3_600_000, day: 86_400_000 };
|
|
475
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
476
|
+
/*
|
|
477
|
+
* When the quota in an Antigravity limit message lifts, rounded up: the server writes the two largest units of the
|
|
478
|
+
* delay and drops the rest (`will reset in 4 days, 23 hours.`), or the time to the minute (`will reset on Oct 21,
|
|
479
|
+
* 2026 14:05 UTC.`). Null when it names neither.
|
|
480
|
+
*/
|
|
481
|
+
export function quotaResetOf(text, now) {
|
|
482
|
+
const relative = /will reset in ([^.]+)\./.exec(text);
|
|
483
|
+
if (relative) {
|
|
484
|
+
let total = 0;
|
|
485
|
+
let smallest = Infinity;
|
|
486
|
+
for (const part of relative[1].split(',')) {
|
|
487
|
+
const amount = /^(\d+) (second|minute|hour|day)s?$/.exec(part.trim());
|
|
488
|
+
if (!amount) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const unit = UNIT_MS[amount[2]];
|
|
492
|
+
total += Number(amount[1]) * unit;
|
|
493
|
+
smallest = Math.min(smallest, unit);
|
|
494
|
+
}
|
|
495
|
+
return now + total + smallest;
|
|
496
|
+
}
|
|
497
|
+
const absolute = /will reset on ([A-Z][a-z]{2}) (\d{1,2}), (\d{4}) (\d{2}):(\d{2}) UTC\./.exec(text);
|
|
498
|
+
const month = absolute ? MONTHS.indexOf(absolute[1]) : -1;
|
|
499
|
+
if (!absolute || month === -1) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return Date.UTC(Number(absolute[3]), month, Number(absolute[2]), Number(absolute[4]), Number(absolute[5])) + UNIT_MS.minute;
|
|
503
|
+
}
|
|
504
|
+
/* The labels a person picked, out of the one answer a question card gives (choices joined with `, `); null for words of their own. */
|
|
505
|
+
function pickedLabels(answer, labels, multiSelect) {
|
|
506
|
+
if (labels.includes(answer)) {
|
|
507
|
+
return [answer];
|
|
508
|
+
}
|
|
509
|
+
if (!multiSelect) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
// A label may hold `, ` itself, so the pieces join up again until they make one.
|
|
513
|
+
const picked = [];
|
|
514
|
+
let part = '';
|
|
515
|
+
for (const piece of answer.split(', ')) {
|
|
516
|
+
part = part === '' ? piece : `${part}, ${piece}`;
|
|
517
|
+
if (labels.includes(part)) {
|
|
518
|
+
picked.push(part);
|
|
519
|
+
part = '';
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return part === '' && picked.length > 0 ? picked : null;
|
|
523
|
+
}
|
|
524
|
+
function cancelledAnswer(pending) {
|
|
525
|
+
return pending.shape === 'grok' ? { outcome: 'cancelled' } : { outcome: { outcome: 'cancelled' } };
|
|
526
|
+
}
|
|
418
527
|
/*
|
|
419
528
|
* One ACP session's `session/update` stream, mapped onto backend events. A message's chunks become
|
|
420
529
|
* one text item, cut where the agent switches to a thought or a tool; a tool call becomes a row that
|
|
@@ -426,10 +535,14 @@ export class AcpProtocol {
|
|
|
426
535
|
words;
|
|
427
536
|
tools = new Map();
|
|
428
537
|
permissions = new Map();
|
|
429
|
-
|
|
538
|
+
questions = new Map();
|
|
539
|
+
// `held` while the message may still be the refusal line or a limit message, whose chunks the thread has not heard yet.
|
|
430
540
|
text = null;
|
|
431
541
|
// Set by a refusal until the next message shows whether it is the refusal line.
|
|
432
542
|
refusalLine = false;
|
|
543
|
+
// The limit message of this turn, held back from the thread: it ends the turn as a limit, or shows as text when the turn ends otherwise.
|
|
544
|
+
quota = null;
|
|
545
|
+
now;
|
|
433
546
|
thought = null;
|
|
434
547
|
counter = 0;
|
|
435
548
|
costUsd = 0;
|
|
@@ -440,11 +553,12 @@ export class AcpProtocol {
|
|
|
440
553
|
prompts = [];
|
|
441
554
|
titles;
|
|
442
555
|
// `titles` takes the session titles the agent sends as the chat's suggested title.
|
|
443
|
-
constructor(generation, modes, words, titles = false) {
|
|
556
|
+
constructor(generation, modes, words, titles = false, now = Date.now) {
|
|
444
557
|
this.generation = generation;
|
|
445
558
|
this.modes = modes;
|
|
446
559
|
this.words = words;
|
|
447
560
|
this.titles = titles;
|
|
561
|
+
this.now = now;
|
|
448
562
|
}
|
|
449
563
|
/* One `session/update` notification; empty for another session, an unknown update or one that does not parse. */
|
|
450
564
|
update(sessionId, params) {
|
|
@@ -588,19 +702,121 @@ export class AcpProtocol {
|
|
|
588
702
|
this.refusalLine = true;
|
|
589
703
|
return { rpcId: pending.rpcId, result, events: this.toolCall({ toolCallId: pending.toolCallId }, false) };
|
|
590
704
|
}
|
|
591
|
-
/*
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
705
|
+
/* A Grok `ask_user_question` request; null when it does not parse or asks nothing, which the caller answers with an error. */
|
|
706
|
+
question(rpcId, sessionId, params) {
|
|
707
|
+
const request = GrokQuestionRequestSchema.safeParse(params);
|
|
708
|
+
if (!request.success || request.data.sessionId !== sessionId) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const questions = validItems(GrokQuestionSchema, request.data.questions).map((question, index) => ({
|
|
712
|
+
id: String(index),
|
|
713
|
+
header: question.header ?? '',
|
|
714
|
+
question: question.question,
|
|
715
|
+
choices: validItems(GrokChoiceSchema, question.options).map((choice) => ({ label: choice.label, description: choice.description ?? '' })),
|
|
716
|
+
multiSelect: question.multiSelect === true
|
|
597
717
|
}));
|
|
718
|
+
if (questions.length === 0) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
return this.ask(rpcId, { shape: 'grok', rpcId, questions });
|
|
722
|
+
}
|
|
723
|
+
/*
|
|
724
|
+
* A `session/request_permission` that is a question rather than a permission, as Antigravity asks one: one choice of
|
|
725
|
+
* several, never a call to run. Null for every other permission request.
|
|
726
|
+
*/
|
|
727
|
+
interaction(rpcId, sessionId, params) {
|
|
728
|
+
const request = PermissionRequestSchema.safeParse(params);
|
|
729
|
+
if (!request.success || request.data.sessionId !== sessionId) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const call = request.data.toolCall;
|
|
733
|
+
const raw = call.rawInput;
|
|
734
|
+
const bare = raw === undefined || raw === null || (isRecord(raw) && Object.keys(raw).length === 0);
|
|
735
|
+
if (!call.toolCallId.startsWith(INTERACTION_PREFIX) || !bare || call.kind || request.data.options.length === 0) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
const question = {
|
|
739
|
+
id: '0',
|
|
740
|
+
header: '',
|
|
741
|
+
question: call.title ?? '',
|
|
742
|
+
choices: request.data.options.map((option) => ({ label: option.name, description: '' })),
|
|
743
|
+
multiSelect: false
|
|
744
|
+
};
|
|
745
|
+
return this.ask(rpcId, { shape: 'interaction', rpcId, question, options: request.data.options });
|
|
746
|
+
}
|
|
747
|
+
ask(rpcId, pending) {
|
|
748
|
+
const requestId = `acp-${this.generation}-${String(rpcId)}`;
|
|
749
|
+
this.questions.set(requestId, pending);
|
|
750
|
+
const questions = pending.shape === 'grok' ? pending.questions : [pending.question];
|
|
751
|
+
return { requestId, rpcId, events: [...this.closeText(), ...this.closeThought(), { type: 'question.requested', requestId, questions }] };
|
|
752
|
+
}
|
|
753
|
+
/* The answer to a question in the agent's own shape, from the card's answers keyed by question id. */
|
|
754
|
+
answerQuestion(requestId, answers) {
|
|
755
|
+
const pending = this.questions.get(requestId);
|
|
756
|
+
if (!pending) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
this.questions.delete(requestId);
|
|
760
|
+
if (pending.shape === 'interaction') {
|
|
761
|
+
const given = (answers[pending.question.id] ?? '').trim();
|
|
762
|
+
const option = pending.options.find((candidate) => candidate.name === given) ?? pending.options.find((candidate) => candidate.optionId === given);
|
|
763
|
+
if (option === undefined) {
|
|
764
|
+
return {
|
|
765
|
+
rpcId: pending.rpcId,
|
|
766
|
+
result: { outcome: { outcome: 'cancelled' } },
|
|
767
|
+
events: [{ type: 'note', level: 'warning', text: this.words.ownAnswer }]
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return { rpcId: pending.rpcId, result: { outcome: { outcome: 'selected', optionId: option.optionId } }, events: [] };
|
|
771
|
+
}
|
|
772
|
+
const picked = {};
|
|
773
|
+
const notes = {};
|
|
774
|
+
for (const question of pending.questions) {
|
|
775
|
+
const given = answers[question.id];
|
|
776
|
+
if (given === undefined) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const labels = pickedLabels(given, question.choices.map((choice) => choice.label), question.multiSelect);
|
|
780
|
+
picked[question.question] = labels ?? [GROK_OWN_ANSWER];
|
|
781
|
+
if (labels === null) {
|
|
782
|
+
notes[question.question] = { notes: given };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
rpcId: pending.rpcId,
|
|
787
|
+
result: { outcome: 'accepted', answers: picked, ...(Object.keys(notes).length === 0 ? {} : { annotations: notes }) },
|
|
788
|
+
events: []
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/* Turns one question down, in the shape its agent takes a refusal in. */
|
|
792
|
+
cancelQuestion(requestId) {
|
|
793
|
+
const pending = this.questions.get(requestId);
|
|
794
|
+
if (!pending) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
this.questions.delete(requestId);
|
|
798
|
+
return { rpcId: pending.rpcId, result: cancelledAnswer(pending) };
|
|
799
|
+
}
|
|
800
|
+
/* Every permission request and question still open, answered as cancelled, which the protocol asks of a client that cancels a turn. */
|
|
801
|
+
cancelRequests() {
|
|
802
|
+
const cancelled = [
|
|
803
|
+
...[...this.permissions].map(([requestId, pending]) => ({
|
|
804
|
+
requestId,
|
|
805
|
+
rpcId: pending.rpcId,
|
|
806
|
+
result: { outcome: { outcome: 'cancelled' } }
|
|
807
|
+
})),
|
|
808
|
+
...[...this.questions].map(([requestId, pending]) => ({ requestId, rpcId: pending.rpcId, result: cancelledAnswer(pending) }))
|
|
809
|
+
];
|
|
598
810
|
this.permissions.clear();
|
|
811
|
+
this.questions.clear();
|
|
599
812
|
return cancelled;
|
|
600
813
|
}
|
|
601
814
|
hasPermission(requestId) {
|
|
602
815
|
return this.permissions.has(requestId);
|
|
603
816
|
}
|
|
817
|
+
hasQuestion(requestId) {
|
|
818
|
+
return this.questions.has(requestId);
|
|
819
|
+
}
|
|
604
820
|
/*
|
|
605
821
|
* The end of a turn, from the `stopReason` of its `session/prompt` response. Only a turn the client cancelled
|
|
606
822
|
* (`cancelled`) ends aborted: Grok 1.0.62 answers `cancelled` by itself when a person turns a call down, and
|
|
@@ -613,6 +829,15 @@ export class AcpProtocol {
|
|
|
613
829
|
this.refusalLine = false;
|
|
614
830
|
const events = [...this.closeText(), ...this.closeThought(), ...this.vendorUsage(isRecord(result) ? result._meta : undefined)];
|
|
615
831
|
const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
|
|
832
|
+
const quota = this.quota;
|
|
833
|
+
this.quota = null;
|
|
834
|
+
if (quota !== null && reason !== 'cancelled') {
|
|
835
|
+
events.push({ type: 'turn.done', ...base, state: 'error', error: quota.text, limit: quotaLimit(quota) });
|
|
836
|
+
return events;
|
|
837
|
+
}
|
|
838
|
+
if (quota !== null) {
|
|
839
|
+
events.push({ type: 'text.done', ref: quota.ref, text: quota.text });
|
|
840
|
+
}
|
|
616
841
|
switch (reason) {
|
|
617
842
|
case 'cancelled':
|
|
618
843
|
events.push({ type: 'turn.done', ...base, state: 'aborted' });
|
|
@@ -631,15 +856,22 @@ export class AcpProtocol {
|
|
|
631
856
|
}
|
|
632
857
|
return events;
|
|
633
858
|
}
|
|
634
|
-
/* A turn that ended without a `stopReason`: the request failed, or was cancelled before the agent said so. */
|
|
635
|
-
abort(promptId, error) {
|
|
859
|
+
/* A turn that ended without a `stopReason`: the request failed, on a limit its error names, or was cancelled before the agent said so. */
|
|
860
|
+
abort(promptId, error, limit = null) {
|
|
636
861
|
this.refusalLine = false;
|
|
637
862
|
const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
863
|
+
const events = [...this.closeText(), ...this.closeThought()];
|
|
864
|
+
const quota = this.quota;
|
|
865
|
+
this.quota = null;
|
|
866
|
+
if (quota !== null) {
|
|
867
|
+
events.push({ type: 'text.done', ref: quota.ref, text: quota.text });
|
|
868
|
+
}
|
|
869
|
+
// A failed request after the limit message still ran into that limit, so it pauses as one when the error names none.
|
|
870
|
+
const named = limit ?? (quota === null ? null : quotaLimit(quota));
|
|
871
|
+
events.push(error === null
|
|
872
|
+
? { type: 'turn.done', ...base, state: 'aborted' }
|
|
873
|
+
: { type: 'turn.done', ...base, state: 'error', error, ...(named === null ? {} : { limit: named }) });
|
|
874
|
+
return events;
|
|
643
875
|
}
|
|
644
876
|
messageChunk(update) {
|
|
645
877
|
const chunk = ContentChunkSchema.safeParse(update);
|
|
@@ -652,12 +884,13 @@ export class AcpProtocol {
|
|
|
652
884
|
events.push(...this.closeText());
|
|
653
885
|
}
|
|
654
886
|
if (this.text === null) {
|
|
655
|
-
|
|
887
|
+
const ref = messageId === null ? `message-${++this.counter}` : `message-${messageId}`;
|
|
888
|
+
this.text = { ref, messageId, text: '', held: true, afterRefusal: this.refusalLine };
|
|
656
889
|
this.refusalLine = false;
|
|
657
890
|
}
|
|
658
891
|
const text = this.text;
|
|
659
892
|
text.text += chunk.data.content.text;
|
|
660
|
-
if (text.held && !
|
|
893
|
+
if (text.held && !withheld(text.text, text.afterRefusal)) {
|
|
661
894
|
text.held = false;
|
|
662
895
|
events.push({ type: 'text.delta', ref: text.ref, text: text.text });
|
|
663
896
|
}
|
|
@@ -682,7 +915,11 @@ export class AcpProtocol {
|
|
|
682
915
|
closeText() {
|
|
683
916
|
const text = this.text;
|
|
684
917
|
this.text = null;
|
|
685
|
-
if (text === null || (text.held && text.text === REFUSAL_LINE)) {
|
|
918
|
+
if (text === null || (text.held && text.afterRefusal && text.text === REFUSAL_LINE)) {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
if (text.held && text.text.startsWith(QUOTA_HEAD)) {
|
|
922
|
+
this.quota = { ref: text.ref, text: text.text.trim(), resetsAt: quotaResetOf(text.text, this.now()) };
|
|
686
923
|
return [];
|
|
687
924
|
}
|
|
688
925
|
return [{ type: 'text.done', ref: text.ref, text: text.text }];
|
package/dist/chat/backend.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface BackendLaunch {
|
|
|
15
15
|
resumeNote: string | null;
|
|
16
16
|
resumeInstructions?: string | null;
|
|
17
17
|
goal?: ChatGoal;
|
|
18
|
+
cacheTtlMs?: number;
|
|
18
19
|
folders?: readonly string[];
|
|
19
20
|
projectConfig?: boolean;
|
|
20
21
|
spawn?: SpawnChatProcess;
|
|
@@ -167,6 +168,8 @@ export type BackendEvent = {
|
|
|
167
168
|
contextTokens?: number;
|
|
168
169
|
contextWindow?: number;
|
|
169
170
|
costUsd?: number;
|
|
171
|
+
cacheTtlMs?: number;
|
|
172
|
+
cacheExpiresAt?: number;
|
|
170
173
|
} | {
|
|
171
174
|
type: 'limits';
|
|
172
175
|
update: LimitsUpdate;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -255,10 +255,12 @@ export class ChatCore {
|
|
|
255
255
|
await this.hooks.admit?.(payload.chatId, cwd);
|
|
256
256
|
// A thread on disk keeps its account too. One it lost still opens, and says why on its next turn.
|
|
257
257
|
// A new one without an account takes the person's pick for this CLI, which is refused rather than replaced when it went.
|
|
258
|
-
|
|
258
|
+
// The pick is checked as named, before the default account loses its id, so one that is turned off is refused too.
|
|
259
|
+
const requested = payload.account ?? this.composerPreferences.for(kind).account;
|
|
259
260
|
if (!stored) {
|
|
260
|
-
this.requireAccount(kind,
|
|
261
|
+
this.requireAccount(kind, requested);
|
|
261
262
|
}
|
|
263
|
+
const account = stored ? stored.info.account : storedAccount(kind, requested);
|
|
262
264
|
const narrowed = (mode) => (this.hooks.runtimeModeFor ? this.hooks.runtimeModeFor(payload.chatId, mode) : mode);
|
|
263
265
|
// Asked again for a chat on disk, since the host may have listed it since.
|
|
264
266
|
const hidden = this.hooks.hiddenFor?.(payload.chatId) ? { hidden: true } : {};
|
|
@@ -369,7 +371,8 @@ export class ChatCore {
|
|
|
369
371
|
if (to === from) {
|
|
370
372
|
return;
|
|
371
373
|
}
|
|
372
|
-
|
|
374
|
+
// As named, so a switch to the default account is refused while it is turned off.
|
|
375
|
+
this.requireAccount(kind, requested);
|
|
373
376
|
const spoke = session.info.agentSessionId !== null || session.info.usage.turns > 0;
|
|
374
377
|
if (spoke && !this.canContinue(kind, from, to)) {
|
|
375
378
|
const label = this.accountLabel(kind, to);
|