@mrclrchtr/supi-antigravity 6.4.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/CLAUDE.md +21 -0
- package/CONTEXT.md +53 -0
- package/README.md +75 -0
- package/docs/adr/0001-use-an-isolated-antigravity-home.md +5 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +118 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +232 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +363 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +12 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +308 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +57 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +34 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +201 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +71 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +105 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +453 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +79 -0
- package/scripts/live-probe.ts +161 -0
- package/src/activity.ts +22 -0
- package/src/availability.ts +231 -0
- package/src/catalogue.ts +21 -0
- package/src/config.ts +26 -0
- package/src/conversation/handles.ts +183 -0
- package/src/extension.ts +22 -0
- package/src/isolated-home.ts +346 -0
- package/src/process/environment.ts +33 -0
- package/src/process/event-values.ts +279 -0
- package/src/process/events.ts +365 -0
- package/src/process/hooks.ts +219 -0
- package/src/process/ndjson.ts +120 -0
- package/src/process/protocol.ts +69 -0
- package/src/process/runner.ts +147 -0
- package/src/process/subprocess.ts +278 -0
- package/src/process/usage.ts +50 -0
- package/src/runtime.ts +118 -0
- package/src/settings.ts +38 -0
- package/src/structured-output.ts +58 -0
- package/src/tool/antigravity_run/evidence.ts +169 -0
- package/src/tool/antigravity_run/execute.ts +225 -0
- package/src/tool/antigravity_run/guidance.ts +3 -0
- package/src/tool/antigravity_run/input.ts +106 -0
- package/src/tool/antigravity_run/register.ts +36 -0
- package/src/tool/antigravity_run/render.ts +236 -0
- package/src/tool/antigravity_run/result.ts +186 -0
- package/src/tool/antigravity_run/spec.ts +20 -0
- package/src/types.ts +107 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const MAX_SAFE_TEXT = 64 * 1024;
|
|
4
|
+
const MAX_TOOL_NAME = 80;
|
|
5
|
+
const MAX_ID_LENGTH = 512;
|
|
6
|
+
|
|
7
|
+
export const SUCCESS_STATES: ReadonlySet<string> = new Set([
|
|
8
|
+
"success",
|
|
9
|
+
"succeeded",
|
|
10
|
+
"done",
|
|
11
|
+
"completed",
|
|
12
|
+
"complete",
|
|
13
|
+
"ok",
|
|
14
|
+
]);
|
|
15
|
+
export const ERROR_STATES: ReadonlySet<string> = new Set([
|
|
16
|
+
"error",
|
|
17
|
+
"failed",
|
|
18
|
+
"failure",
|
|
19
|
+
"aborted",
|
|
20
|
+
"cancelled",
|
|
21
|
+
"canceled",
|
|
22
|
+
"timeout",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function safeString(value: unknown, maxLength = MAX_SAFE_TEXT): string | undefined {
|
|
30
|
+
if (typeof value !== "string") return undefined;
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed.length > 0 && trimmed.length <= maxLength ? trimmed : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function eventType(event: Record<string, unknown>): string {
|
|
36
|
+
return (safeString(event.type, 40) ?? safeString(event.event, 40) ?? "").toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function eventStatus(event: Record<string, unknown>): string | undefined {
|
|
40
|
+
if (
|
|
41
|
+
event.is_error === true ||
|
|
42
|
+
event.isError === true ||
|
|
43
|
+
(event.error !== undefined && event.error !== null)
|
|
44
|
+
) {
|
|
45
|
+
return "error";
|
|
46
|
+
}
|
|
47
|
+
const direct = safeString(event.status, 40) ?? safeString(event.subtype, 40);
|
|
48
|
+
if (direct) return direct.toLowerCase();
|
|
49
|
+
if (event.ok === true || event.success === true) return "success";
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isSuccessStatus(status: string | undefined): boolean {
|
|
54
|
+
return status === undefined || SUCCESS_STATES.has(status);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isErrorStatus(status: string | undefined): boolean {
|
|
58
|
+
return status !== undefined && ERROR_STATES.has(status);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function eventId(event: Record<string, unknown>): string | undefined {
|
|
62
|
+
const sources = [
|
|
63
|
+
event,
|
|
64
|
+
isRecord(event.tool_call) ? event.tool_call : undefined,
|
|
65
|
+
isRecord(event.toolCall) ? event.toolCall : undefined,
|
|
66
|
+
isRecord(event.tool_use) ? event.tool_use : undefined,
|
|
67
|
+
isRecord(event.toolUse) ? event.toolUse : undefined,
|
|
68
|
+
];
|
|
69
|
+
for (const source of sources) {
|
|
70
|
+
if (!source) continue;
|
|
71
|
+
for (const key of [
|
|
72
|
+
"tool_use_id",
|
|
73
|
+
"toolUseId",
|
|
74
|
+
"tool_call_id",
|
|
75
|
+
"toolCallId",
|
|
76
|
+
"step_index",
|
|
77
|
+
"stepIndex",
|
|
78
|
+
"id",
|
|
79
|
+
]) {
|
|
80
|
+
const value = safeIdentifier(source[key]);
|
|
81
|
+
if (value) return value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function conversationId(event: Record<string, unknown>): string | undefined {
|
|
88
|
+
for (const key of ["session_id", "sessionId", "conversation_id", "conversationId"]) {
|
|
89
|
+
const value = safeString(event[key], MAX_ID_LENGTH);
|
|
90
|
+
if (value) return value;
|
|
91
|
+
}
|
|
92
|
+
for (const nested of [event.session, event.conversation]) {
|
|
93
|
+
if (!isRecord(nested)) continue;
|
|
94
|
+
const value = conversationId(nested);
|
|
95
|
+
if (value) return value;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function toolName(event: Record<string, unknown>): string | undefined {
|
|
101
|
+
const sources = [
|
|
102
|
+
event,
|
|
103
|
+
isRecord(event.tool) ? event.tool : undefined,
|
|
104
|
+
isRecord(event.tool_call) ? event.tool_call : undefined,
|
|
105
|
+
isRecord(event.toolCall) ? event.toolCall : undefined,
|
|
106
|
+
isRecord(event.tool_use) ? event.tool_use : undefined,
|
|
107
|
+
isRecord(event.toolUse) ? event.toolUse : undefined,
|
|
108
|
+
isRecord(event.step_update) ? event.step_update : undefined,
|
|
109
|
+
];
|
|
110
|
+
for (const source of sources) {
|
|
111
|
+
const name = source ? nameFromSource(source) : undefined;
|
|
112
|
+
if (name) return name;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function nameFromSource(source: Record<string, unknown>): string | undefined {
|
|
118
|
+
for (const key of ["tool_name", "toolName", "name"]) {
|
|
119
|
+
const value = safeString(source[key], MAX_TOOL_NAME);
|
|
120
|
+
if (value) return normalizeToolName(value);
|
|
121
|
+
}
|
|
122
|
+
const functionValue = isRecord(source.function)
|
|
123
|
+
? safeString(source.function.name, MAX_TOOL_NAME)
|
|
124
|
+
: undefined;
|
|
125
|
+
return functionValue ? normalizeToolName(functionValue) : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function normalizeToolName(value: string): string {
|
|
129
|
+
return value
|
|
130
|
+
.toLowerCase()
|
|
131
|
+
.replace(/[-\s]+/g, "_")
|
|
132
|
+
.slice(0, MAX_TOOL_NAME);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function safeIdentifier(value: unknown): string | undefined {
|
|
136
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
|
|
137
|
+
return String(value);
|
|
138
|
+
}
|
|
139
|
+
return safeString(value, MAX_ID_LENGTH);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function toolInput(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
143
|
+
const nested = [
|
|
144
|
+
event.tool_call,
|
|
145
|
+
event.toolCall,
|
|
146
|
+
event.tool_use,
|
|
147
|
+
event.toolUse,
|
|
148
|
+
event.tool,
|
|
149
|
+
event.input,
|
|
150
|
+
event.arguments,
|
|
151
|
+
event.tool_info,
|
|
152
|
+
event.toolInfo,
|
|
153
|
+
];
|
|
154
|
+
for (const value of nested) {
|
|
155
|
+
const input = inputFromValue(value);
|
|
156
|
+
if (input) return input;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function inputFromValue(value: unknown): Record<string, unknown> | undefined {
|
|
162
|
+
if (!isRecord(value)) return undefined;
|
|
163
|
+
if (isRecord(value.input)) return value.input;
|
|
164
|
+
if (isRecord(value.arguments)) return value.arguments;
|
|
165
|
+
if (isRecord(value.args)) return value.args;
|
|
166
|
+
if (isRecord(value.parameters)) return value.parameters;
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function extractUrl(value: unknown): string | undefined {
|
|
171
|
+
return readBoundedField(value, ["url", "uri", "link"], (candidate) =>
|
|
172
|
+
/^https?:\/\//i.test(candidate),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function extractWorkspacePath(value: unknown): string | undefined {
|
|
177
|
+
return readBoundedField(value, [
|
|
178
|
+
"path",
|
|
179
|
+
"file",
|
|
180
|
+
"file_path",
|
|
181
|
+
"relative_path",
|
|
182
|
+
"directory_path",
|
|
183
|
+
"search_path",
|
|
184
|
+
"search_directory",
|
|
185
|
+
"absolute_path",
|
|
186
|
+
"directory",
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readBoundedField(
|
|
191
|
+
value: unknown,
|
|
192
|
+
names: readonly string[],
|
|
193
|
+
predicate: (candidate: string) => boolean = () => true,
|
|
194
|
+
): string | undefined {
|
|
195
|
+
if (!isRecord(value)) return undefined;
|
|
196
|
+
const fields = Object.entries(value);
|
|
197
|
+
for (const name of names) {
|
|
198
|
+
const normalizedName = normalizeFieldName(name);
|
|
199
|
+
for (const [key, field] of fields) {
|
|
200
|
+
if (normalizeFieldName(key) !== normalizedName) continue;
|
|
201
|
+
const candidate = safeString(field, 4_096);
|
|
202
|
+
if (candidate && predicate(candidate)) return candidate;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeFieldName(value: string): string {
|
|
209
|
+
return value.toLowerCase().replace(/[-_]/g, "");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function hashEvidence(value: string): string {
|
|
213
|
+
return createHash("sha256").update(value).digest("hex");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function answerCandidate(
|
|
217
|
+
event: Record<string, unknown>,
|
|
218
|
+
): Record<string, unknown> | undefined {
|
|
219
|
+
const candidates = [
|
|
220
|
+
event.structured_output,
|
|
221
|
+
event.structuredOutput,
|
|
222
|
+
event.output,
|
|
223
|
+
event.result,
|
|
224
|
+
event.response,
|
|
225
|
+
event.data,
|
|
226
|
+
];
|
|
227
|
+
for (const value of candidates) {
|
|
228
|
+
const candidate = decodeCandidate(value);
|
|
229
|
+
if (candidate) return selectAnswerFields(candidate);
|
|
230
|
+
}
|
|
231
|
+
return hasAnswerFields(event) ? selectAnswerFields(event) : undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function decodeCandidate(value: unknown): Record<string, unknown> | undefined {
|
|
235
|
+
if (isRecord(value)) return value;
|
|
236
|
+
const text = safeString(value, MAX_SAFE_TEXT);
|
|
237
|
+
if (!text) return undefined;
|
|
238
|
+
try {
|
|
239
|
+
const parsed: unknown = JSON.parse(text);
|
|
240
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
241
|
+
} catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hasAnswerFields(value: Record<string, unknown>): boolean {
|
|
247
|
+
return "answer" in value && "sources" in value && "workspaceEvidence" in value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function selectAnswerFields(value: Record<string, unknown>): Record<string, unknown> {
|
|
251
|
+
return {
|
|
252
|
+
answer: value.answer,
|
|
253
|
+
sources: value.sources,
|
|
254
|
+
workspaceEvidence: value.workspaceEvidence,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function isTerminalEvent(event: Record<string, unknown>): boolean {
|
|
259
|
+
const type = eventType(event);
|
|
260
|
+
return (
|
|
261
|
+
["result", "terminal", "done", "final", "completion", "response"].includes(type) ||
|
|
262
|
+
event.final === true
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function isToolResultEvent(event: Record<string, unknown>): boolean {
|
|
267
|
+
const type = eventType(event);
|
|
268
|
+
return (
|
|
269
|
+
type === "tool_result" ||
|
|
270
|
+
type === "toolresult" ||
|
|
271
|
+
type === "toolresponse" ||
|
|
272
|
+
type === "tool_response"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function isToolStartEvent(event: Record<string, unknown>): boolean {
|
|
277
|
+
const type = eventType(event);
|
|
278
|
+
return type === "tool_use" || type === "tooluse" || type === "tool_call" || type === "toolcall";
|
|
279
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { isAbsolute, normalize as normalizePath, relative } from "node:path";
|
|
2
|
+
import { isWebToolName, isWorkspaceToolName } from "../activity.ts";
|
|
3
|
+
import { validateAntigravityAnswer } from "../structured-output.ts";
|
|
4
|
+
import type { AntigravityExecutionFacts, AntigravityUsage } from "../types.ts";
|
|
5
|
+
import {
|
|
6
|
+
answerCandidate,
|
|
7
|
+
conversationId,
|
|
8
|
+
eventId,
|
|
9
|
+
eventStatus,
|
|
10
|
+
eventType,
|
|
11
|
+
extractUrl,
|
|
12
|
+
extractWorkspacePath,
|
|
13
|
+
hashEvidence,
|
|
14
|
+
isErrorStatus,
|
|
15
|
+
isRecord,
|
|
16
|
+
isSuccessStatus,
|
|
17
|
+
isTerminalEvent,
|
|
18
|
+
isToolResultEvent,
|
|
19
|
+
isToolStartEvent,
|
|
20
|
+
safeString,
|
|
21
|
+
toolInput,
|
|
22
|
+
toolName,
|
|
23
|
+
} from "./event-values.ts";
|
|
24
|
+
import { normalizeProtocolEvent } from "./protocol.ts";
|
|
25
|
+
import { mergeUsage, readUsage } from "./usage.ts";
|
|
26
|
+
|
|
27
|
+
interface PendingTool {
|
|
28
|
+
name: string;
|
|
29
|
+
urlHash?: string;
|
|
30
|
+
pathHash?: string;
|
|
31
|
+
observed?: boolean;
|
|
32
|
+
denied?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ToolResultClassification {
|
|
36
|
+
kind: "pending" | "failure" | "success";
|
|
37
|
+
denied: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MAX_COMPLETED_TOOL_IDS = 4_096;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reduces parsed events to bounded execution facts. Raw events and tool payloads
|
|
44
|
+
* are not retained after each event is processed.
|
|
45
|
+
*/
|
|
46
|
+
export class AntigravityEventAccumulator {
|
|
47
|
+
readonly #workspaceDirectory: string | undefined;
|
|
48
|
+
#pendingTools = new Map<string, PendingTool>();
|
|
49
|
+
#completedToolIds = new Set<string>();
|
|
50
|
+
#conversationId: string | undefined;
|
|
51
|
+
#candidate: Record<string, unknown> | undefined;
|
|
52
|
+
#terminalSeen = false;
|
|
53
|
+
#terminalSucceeded = false;
|
|
54
|
+
#usage: AntigravityUsage | undefined;
|
|
55
|
+
#toolNames: string[] = [];
|
|
56
|
+
#toolCounts = new Map<string, number>();
|
|
57
|
+
#successfulToolNames = new Set<string>();
|
|
58
|
+
#permissionDenials = 0;
|
|
59
|
+
#unattributedPermissionDenialSeen = false;
|
|
60
|
+
#sourceHashes = new Set<string>();
|
|
61
|
+
#workspacePathHashes = new Set<string>();
|
|
62
|
+
|
|
63
|
+
constructor(options: { workspaceDirectory?: string } = {}) {
|
|
64
|
+
this.#workspaceDirectory = options.workspaceDirectory;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Consume one already-parsed event. */
|
|
68
|
+
consume(event: Record<string, unknown>): void {
|
|
69
|
+
if (this.#terminalSeen)
|
|
70
|
+
throw new Error("Antigravity returned events after its terminal event.");
|
|
71
|
+
const normalizedEvent = normalizeProtocolEvent(event);
|
|
72
|
+
this.#conversationId =
|
|
73
|
+
conversationId(event) ?? conversationId(normalizedEvent) ?? this.#conversationId;
|
|
74
|
+
this.#usage = mergeUsage(this.#usage, mergeUsage(readUsage(event), readUsage(normalizedEvent)));
|
|
75
|
+
const terminal = isTerminalEvent(normalizedEvent);
|
|
76
|
+
const nestedTools = consumeNestedToolBlocks(this, normalizedEvent);
|
|
77
|
+
if (nestedTools && !terminal) return;
|
|
78
|
+
|
|
79
|
+
if (isToolStartEvent(normalizedEvent)) {
|
|
80
|
+
this.#rememberToolStart(normalizedEvent);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (isToolResultEvent(normalizedEvent)) {
|
|
84
|
+
this.#consumeToolResult(normalizedEvent);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (terminal) {
|
|
88
|
+
this.#consumeTerminal(normalizedEvent);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const directName = toolName(normalizedEvent);
|
|
93
|
+
if (directName && isSuccessfulActivity(normalizedEvent)) {
|
|
94
|
+
this.#recordSuccessfulTool(directName, normalizedEvent);
|
|
95
|
+
}
|
|
96
|
+
const denied = isPermissionDenial(normalizedEvent);
|
|
97
|
+
if (denied && !isNonToolStepUpdate(normalizedEvent)) {
|
|
98
|
+
const counted = directName
|
|
99
|
+
? this.#recordPermissionDenial(directName, normalizedEvent)
|
|
100
|
+
: this.#recordUnattributedPermissionDenial();
|
|
101
|
+
if (counted) this.#permissionDenials += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Finish the reduction and validate terminal structured output. */
|
|
106
|
+
finish(): AntigravityExecutionFacts {
|
|
107
|
+
if (!this.#terminalSeen || !this.#terminalSucceeded) {
|
|
108
|
+
throw new Error("Antigravity did not return a successful terminal event.");
|
|
109
|
+
}
|
|
110
|
+
if (!this.#candidate)
|
|
111
|
+
throw new Error("Antigravity terminal output was missing structured data.");
|
|
112
|
+
if (!this.#conversationId) throw new Error("Antigravity did not return a conversation handle.");
|
|
113
|
+
return {
|
|
114
|
+
answer: validateAntigravityAnswer(this.#candidate),
|
|
115
|
+
conversationId: this.#conversationId,
|
|
116
|
+
...(this.#usage ? { usage: this.#usage } : {}),
|
|
117
|
+
observedToolNames: [...this.#toolNames],
|
|
118
|
+
observedToolCounts: Object.fromEntries(this.#toolCounts),
|
|
119
|
+
successfulToolNames: [...this.#successfulToolNames],
|
|
120
|
+
permissionDenials: this.#permissionDenials,
|
|
121
|
+
observedSourceHashes: [...this.#sourceHashes],
|
|
122
|
+
observedWorkspacePathHashes: [...this.#workspacePathHashes],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#rememberToolStart(event: Record<string, unknown>): void {
|
|
127
|
+
const name = toolName(event);
|
|
128
|
+
if (!name) return;
|
|
129
|
+
const input = toolInput(event) ?? event;
|
|
130
|
+
const denied = isPermissionDenial(event);
|
|
131
|
+
const urlHash = hashValue(extractUrl(input));
|
|
132
|
+
const path = extractWorkspacePath(input);
|
|
133
|
+
const pathHash = path ? this.#hashWorkspacePath(path) : undefined;
|
|
134
|
+
const id = eventId(event);
|
|
135
|
+
if (id) this.#completedToolIds.delete(id);
|
|
136
|
+
const previous = id ? this.#pendingTools.get(id) : undefined;
|
|
137
|
+
const pending = mergePendingTool(
|
|
138
|
+
{
|
|
139
|
+
name,
|
|
140
|
+
observed: true,
|
|
141
|
+
...(urlHash ? { urlHash } : {}),
|
|
142
|
+
...(pathHash ? { pathHash } : {}),
|
|
143
|
+
...(denied ? { denied: true } : {}),
|
|
144
|
+
},
|
|
145
|
+
previous,
|
|
146
|
+
);
|
|
147
|
+
if (!previous) this.#recordObservedTool(name);
|
|
148
|
+
if (id) this.#pendingTools.set(id, pending);
|
|
149
|
+
if (isSuccessfulActivity(event)) this.#recordSuccessfulTool(name, event, pending);
|
|
150
|
+
if (denied && !previous?.denied) this.#permissionDenials += 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#consumeToolResult(event: Record<string, unknown>): void {
|
|
154
|
+
const id = eventId(event);
|
|
155
|
+
// Without an ID, each result is a distinct observation because duplicates are unknowable.
|
|
156
|
+
if (id && this.#completedToolIds.has(id)) return;
|
|
157
|
+
const pending = id ? this.#pendingTools.get(id) : undefined;
|
|
158
|
+
const name = toolName(event) ?? pending?.name;
|
|
159
|
+
if (!name) return;
|
|
160
|
+
const classification = classifyToolResult(event);
|
|
161
|
+
if (classification.kind === "pending") {
|
|
162
|
+
this.#recordPendingToolResult(id, name, pending);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (id) this.#completeToolResult(id);
|
|
166
|
+
if (classification.kind === "failure") {
|
|
167
|
+
this.#recordFailedToolResult(name, pending, classification.denied);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.#recordSuccessfulTool(name, event, pending);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#recordPendingToolResult(
|
|
174
|
+
id: string | undefined,
|
|
175
|
+
name: string,
|
|
176
|
+
pending: PendingTool | undefined,
|
|
177
|
+
): void {
|
|
178
|
+
if (!pending?.observed) this.#recordObservedTool(name);
|
|
179
|
+
if (id && !pending) this.#pendingTools.set(id, { name, observed: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#completeToolResult(id: string): void {
|
|
183
|
+
this.#pendingTools.delete(id);
|
|
184
|
+
this.#rememberCompletedToolId(id);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#recordFailedToolResult(name: string, pending: PendingTool | undefined, denied: boolean): void {
|
|
188
|
+
if (denied && !pending?.denied) this.#permissionDenials += 1;
|
|
189
|
+
if (!pending?.observed) this.#recordObservedTool(name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#rememberCompletedToolId(id: string): void {
|
|
193
|
+
this.#completedToolIds.add(id);
|
|
194
|
+
if (this.#completedToolIds.size <= MAX_COMPLETED_TOOL_IDS) return;
|
|
195
|
+
const oldest = this.#completedToolIds.values().next().value;
|
|
196
|
+
if (typeof oldest === "string") this.#completedToolIds.delete(oldest);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#recordObservedTool(name: string): void {
|
|
200
|
+
this.#toolNames = this.#toolNames.includes(name) ? this.#toolNames : [...this.#toolNames, name];
|
|
201
|
+
this.#toolCounts.set(name, (this.#toolCounts.get(name) ?? 0) + 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#recordUnattributedPermissionDenial(): boolean {
|
|
205
|
+
if (this.#unattributedPermissionDenialSeen) return false;
|
|
206
|
+
this.#unattributedPermissionDenialSeen = true;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#recordPermissionDenial(name: string, event: Record<string, unknown>): boolean {
|
|
211
|
+
const id = eventId(event);
|
|
212
|
+
if (id && this.#completedToolIds.has(id)) return false;
|
|
213
|
+
const pending = id ? this.#pendingTools.get(id) : undefined;
|
|
214
|
+
if (pending?.denied) return false;
|
|
215
|
+
if (!pending?.observed) this.#recordObservedTool(name);
|
|
216
|
+
if (id) {
|
|
217
|
+
if (pending) pending.denied = true;
|
|
218
|
+
else this.#pendingTools.set(id, { name, observed: true, denied: true });
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#recordSuccessfulTool(name: string, event: Record<string, unknown>, pending?: PendingTool): void {
|
|
224
|
+
if (!pending?.observed) this.#recordObservedTool(name);
|
|
225
|
+
this.#successfulToolNames.add(name);
|
|
226
|
+
const input = toolInput(event) ?? event;
|
|
227
|
+
const sourceHash = pending?.urlHash ?? hashValue(extractUrl(input));
|
|
228
|
+
const pathHash = pending?.pathHash ?? this.#hashWorkspacePathValue(extractWorkspacePath(input));
|
|
229
|
+
if (sourceHash && isWebToolName(name)) this.#sourceHashes.add(sourceHash);
|
|
230
|
+
if (pathHash && isWorkspaceToolName(name)) this.#workspacePathHashes.add(pathHash);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#hashWorkspacePath(value: string): string {
|
|
234
|
+
return hashEvidence(this.#normalizeWorkspacePath(value));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#hashWorkspacePathValue(value: string | undefined): string | undefined {
|
|
238
|
+
return value ? this.#hashWorkspacePath(value) : undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#normalizeWorkspacePath(value: string): string {
|
|
242
|
+
const normalized = value.replaceAll("\\", "/");
|
|
243
|
+
if (this.#workspaceDirectory && isAbsolute(normalized)) {
|
|
244
|
+
const distance = relative(this.#workspaceDirectory, normalized);
|
|
245
|
+
if (distance && distance !== ".." && !distance.startsWith("../")) {
|
|
246
|
+
return normalizeWorkspacePath(distance);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return normalizeWorkspacePath(normalized);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#consumeTerminal(event: Record<string, unknown>): void {
|
|
253
|
+
this.#terminalSeen = true;
|
|
254
|
+
const status = eventStatus(event);
|
|
255
|
+
if (isErrorStatus(status) || !hasExplicitSuccess(event, status)) {
|
|
256
|
+
throw new Error("Antigravity returned a non-success terminal status.");
|
|
257
|
+
}
|
|
258
|
+
this.#terminalSucceeded = true;
|
|
259
|
+
this.#candidate = answerCandidate(event);
|
|
260
|
+
this.#conversationId = conversationId(event) ?? this.#conversationId;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function consumeNestedToolBlocks(
|
|
265
|
+
accumulator: AntigravityEventAccumulator,
|
|
266
|
+
event: Record<string, unknown>,
|
|
267
|
+
): boolean {
|
|
268
|
+
const message = isRecord(event.message) ? event.message : undefined;
|
|
269
|
+
const content = message?.content;
|
|
270
|
+
if (!Array.isArray(content)) return false;
|
|
271
|
+
let consumed = false;
|
|
272
|
+
for (const block of content) {
|
|
273
|
+
if (!isRecord(block)) continue;
|
|
274
|
+
const normalizedBlock = normalizeProtocolEvent(block);
|
|
275
|
+
if (!isToolActivityBlock(normalizedBlock)) continue;
|
|
276
|
+
accumulator.consume(normalizedBlock);
|
|
277
|
+
consumed = true;
|
|
278
|
+
}
|
|
279
|
+
return consumed;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isToolActivityBlock(event: Record<string, unknown>): boolean {
|
|
283
|
+
if (isToolStartEvent(event) || isToolResultEvent(event)) return true;
|
|
284
|
+
return eventType(event) === "step_update" && !isNonToolStepUpdate(event);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasExplicitSuccess(event: Record<string, unknown>, status: string | undefined): boolean {
|
|
288
|
+
return status !== undefined
|
|
289
|
+
? isSuccessStatus(status)
|
|
290
|
+
: event.is_error === false || event.isError === false || event.success === true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isSuccessfulActivity(event: Record<string, unknown>): boolean {
|
|
294
|
+
const status = eventStatus(event);
|
|
295
|
+
return (
|
|
296
|
+
!isErrorStatus(status) &&
|
|
297
|
+
isSuccessStatus(status) &&
|
|
298
|
+
(status !== undefined || event.success === true || event.ok === true)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isNonToolStepUpdate(event: Record<string, unknown>): boolean {
|
|
303
|
+
const type = eventType(event);
|
|
304
|
+
if (!["assistant", "step_update"].includes(type)) return false;
|
|
305
|
+
const declaredType = safeString(event.step_type ?? event.stepType, 40)?.toLowerCase();
|
|
306
|
+
if (declaredType) return declaredType !== "tool";
|
|
307
|
+
return toolName(event) === undefined && !isRecord(event.tool_info) && !isRecord(event.toolInfo);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function classifyToolResult(event: Record<string, unknown>): ToolResultClassification {
|
|
311
|
+
const status = eventStatus(event);
|
|
312
|
+
const denied = isPermissionDenial(event);
|
|
313
|
+
if (denied || isErrorStatus(status)) return { kind: "failure", denied };
|
|
314
|
+
return { kind: isSuccessStatus(status) ? "success" : "pending", denied };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isPermissionDenial(event: Record<string, unknown>): boolean {
|
|
318
|
+
const status = eventStatus(event);
|
|
319
|
+
if (["denied", "permission_denied", "forbidden", "blocked"].includes(status ?? "")) return true;
|
|
320
|
+
const type = eventType(event);
|
|
321
|
+
if (type.includes("permission") || type.includes("denied") || type.includes("blocked"))
|
|
322
|
+
return true;
|
|
323
|
+
const hasExplicitFailure =
|
|
324
|
+
event.is_error === true ||
|
|
325
|
+
event.isError === true ||
|
|
326
|
+
isErrorStatus(status) ||
|
|
327
|
+
event.error !== undefined ||
|
|
328
|
+
event.reason !== undefined;
|
|
329
|
+
if (!hasExplicitFailure) return false;
|
|
330
|
+
const message = [event.error, event.reason, event.message, event.content]
|
|
331
|
+
.map((value) => safeDenialText(value))
|
|
332
|
+
.join(" ")
|
|
333
|
+
.toLowerCase();
|
|
334
|
+
return message.includes("permission") || message.includes("denied");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function safeDenialText(value: unknown, depth = 0): string {
|
|
338
|
+
if (depth > 3) return "";
|
|
339
|
+
if (typeof value === "string") return value.slice(0, 4_000);
|
|
340
|
+
if (Array.isArray(value)) {
|
|
341
|
+
return value
|
|
342
|
+
.slice(0, 20)
|
|
343
|
+
.map((item) => safeDenialText(item, depth + 1))
|
|
344
|
+
.join(" ");
|
|
345
|
+
}
|
|
346
|
+
if (!isRecord(value)) return "";
|
|
347
|
+
return ["text", "message", "content", "error", "reason"]
|
|
348
|
+
.map((key) => safeDenialText(value[key], depth + 1))
|
|
349
|
+
.join(" ");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function mergePendingTool(current: PendingTool, previous: PendingTool | undefined): PendingTool {
|
|
353
|
+
if (previous?.urlHash && !current.urlHash) current.urlHash = previous.urlHash;
|
|
354
|
+
if (previous?.pathHash && !current.pathHash) current.pathHash = previous.pathHash;
|
|
355
|
+
if (previous?.denied) current.denied = true;
|
|
356
|
+
return current;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function hashValue(value: string | undefined): string | undefined {
|
|
360
|
+
return value ? hashEvidence(value) : undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeWorkspacePath(value: string): string {
|
|
364
|
+
return normalizePath(value.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
365
|
+
}
|