@adecore/agents 0.31.11 → 0.32.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 +262 -23
- 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 +46 -2
- 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 +308 -26
- 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 +48 -2
- 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
|
}
|
|
@@ -332,6 +361,8 @@ function str(value) {
|
|
|
332
361
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
333
362
|
}
|
|
334
363
|
/* A field of the agent's own input, which no schema promises; the first one that is a non-empty string. */
|
|
364
|
+
// Where agents put the command a call runs; Antigravity names it `CommandLine`.
|
|
365
|
+
const COMMAND_KEYS = ['command', 'cmd', 'CommandLine'];
|
|
335
366
|
function rawField(raw, ...keys) {
|
|
336
367
|
if (!isRecord(raw)) {
|
|
337
368
|
return null;
|
|
@@ -375,7 +406,7 @@ export function toolRow(tool) {
|
|
|
375
406
|
const raw = isRecord(tool.rawInput) ? tool.rawInput : {};
|
|
376
407
|
switch (tool.kind) {
|
|
377
408
|
case 'execute':
|
|
378
|
-
return { name: 'Bash', input: { ...raw, description, command: rawField(tool.rawInput,
|
|
409
|
+
return { name: 'Bash', input: { ...raw, description, command: rawField(tool.rawInput, ...COMMAND_KEYS) ?? description } };
|
|
379
410
|
case 'read':
|
|
380
411
|
return { name: 'Read', input: { ...raw, description, ...(path === null ? {} : { file_path: path }) } };
|
|
381
412
|
case 'search':
|
|
@@ -408,11 +439,91 @@ function toolOutput(tool) {
|
|
|
408
439
|
}
|
|
409
440
|
export function acpStopWords(agentName) {
|
|
410
441
|
return {
|
|
411
|
-
maxTokens: `${agentName} stopped
|
|
412
|
-
maxTurnRequests: `${agentName} stopped
|
|
413
|
-
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.`
|
|
414
446
|
};
|
|
415
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
|
+
}
|
|
416
527
|
/*
|
|
417
528
|
* One ACP session's `session/update` stream, mapped onto backend events. A message's chunks become
|
|
418
529
|
* one text item, cut where the agent switches to a thought or a tool; a tool call becomes a row that
|
|
@@ -424,10 +535,14 @@ export class AcpProtocol {
|
|
|
424
535
|
words;
|
|
425
536
|
tools = new Map();
|
|
426
537
|
permissions = new Map();
|
|
427
|
-
|
|
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.
|
|
428
540
|
text = null;
|
|
429
541
|
// Set by a refusal until the next message shows whether it is the refusal line.
|
|
430
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;
|
|
431
546
|
thought = null;
|
|
432
547
|
counter = 0;
|
|
433
548
|
costUsd = 0;
|
|
@@ -438,11 +553,12 @@ export class AcpProtocol {
|
|
|
438
553
|
prompts = [];
|
|
439
554
|
titles;
|
|
440
555
|
// `titles` takes the session titles the agent sends as the chat's suggested title.
|
|
441
|
-
constructor(generation, modes, words, titles = false) {
|
|
556
|
+
constructor(generation, modes, words, titles = false, now = Date.now) {
|
|
442
557
|
this.generation = generation;
|
|
443
558
|
this.modes = modes;
|
|
444
559
|
this.words = words;
|
|
445
560
|
this.titles = titles;
|
|
561
|
+
this.now = now;
|
|
446
562
|
}
|
|
447
563
|
/* One `session/update` notification; empty for another session, an unknown update or one that does not parse. */
|
|
448
564
|
update(sessionId, params) {
|
|
@@ -548,7 +664,7 @@ export class AcpProtocol {
|
|
|
548
664
|
description: tool.title === '' ? null : tool.title,
|
|
549
665
|
canAllowAlways: options.some((option) => option.kind === 'allow_always')
|
|
550
666
|
});
|
|
551
|
-
const command = rawField(tool.rawInput,
|
|
667
|
+
const command = rawField(tool.rawInput, ...COMMAND_KEYS);
|
|
552
668
|
const asked = { kind: tool.kind, title: tool.title === '' ? null : tool.title, commands: [...(command === null ? [] : [command]), ...tool.text] };
|
|
553
669
|
return { requestId, rpcId, kind: tool.kind, request: asked, events };
|
|
554
670
|
}
|
|
@@ -586,19 +702,121 @@ export class AcpProtocol {
|
|
|
586
702
|
this.refusalLine = true;
|
|
587
703
|
return { rpcId: pending.rpcId, result, events: this.toolCall({ toolCallId: pending.toolCallId }, false) };
|
|
588
704
|
}
|
|
589
|
-
/*
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
595
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
|
+
];
|
|
596
810
|
this.permissions.clear();
|
|
811
|
+
this.questions.clear();
|
|
597
812
|
return cancelled;
|
|
598
813
|
}
|
|
599
814
|
hasPermission(requestId) {
|
|
600
815
|
return this.permissions.has(requestId);
|
|
601
816
|
}
|
|
817
|
+
hasQuestion(requestId) {
|
|
818
|
+
return this.questions.has(requestId);
|
|
819
|
+
}
|
|
602
820
|
/*
|
|
603
821
|
* The end of a turn, from the `stopReason` of its `session/prompt` response. Only a turn the client cancelled
|
|
604
822
|
* (`cancelled`) ends aborted: Grok 1.0.62 answers `cancelled` by itself when a person turns a call down, and
|
|
@@ -611,6 +829,15 @@ export class AcpProtocol {
|
|
|
611
829
|
this.refusalLine = false;
|
|
612
830
|
const events = [...this.closeText(), ...this.closeThought(), ...this.vendorUsage(isRecord(result) ? result._meta : undefined)];
|
|
613
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
|
+
}
|
|
614
841
|
switch (reason) {
|
|
615
842
|
case 'cancelled':
|
|
616
843
|
events.push({ type: 'turn.done', ...base, state: 'aborted' });
|
|
@@ -629,15 +856,22 @@ export class AcpProtocol {
|
|
|
629
856
|
}
|
|
630
857
|
return events;
|
|
631
858
|
}
|
|
632
|
-
/* A turn that ended without a `stopReason`: the request failed, or was cancelled before the agent said so. */
|
|
633
|
-
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) {
|
|
634
861
|
this.refusalLine = false;
|
|
635
862
|
const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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;
|
|
641
875
|
}
|
|
642
876
|
messageChunk(update) {
|
|
643
877
|
const chunk = ContentChunkSchema.safeParse(update);
|
|
@@ -650,12 +884,13 @@ export class AcpProtocol {
|
|
|
650
884
|
events.push(...this.closeText());
|
|
651
885
|
}
|
|
652
886
|
if (this.text === null) {
|
|
653
|
-
|
|
887
|
+
const ref = messageId === null ? `message-${++this.counter}` : `message-${messageId}`;
|
|
888
|
+
this.text = { ref, messageId, text: '', held: true, afterRefusal: this.refusalLine };
|
|
654
889
|
this.refusalLine = false;
|
|
655
890
|
}
|
|
656
891
|
const text = this.text;
|
|
657
892
|
text.text += chunk.data.content.text;
|
|
658
|
-
if (text.held && !
|
|
893
|
+
if (text.held && !withheld(text.text, text.afterRefusal)) {
|
|
659
894
|
text.held = false;
|
|
660
895
|
events.push({ type: 'text.delta', ref: text.ref, text: text.text });
|
|
661
896
|
}
|
|
@@ -680,7 +915,11 @@ export class AcpProtocol {
|
|
|
680
915
|
closeText() {
|
|
681
916
|
const text = this.text;
|
|
682
917
|
this.text = null;
|
|
683
|
-
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()) };
|
|
684
923
|
return [];
|
|
685
924
|
}
|
|
686
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;
|