@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,219 @@
|
|
|
1
|
+
import type { IsolatedAntigravityPaths } from "../isolated-home.ts";
|
|
2
|
+
import { PROJECT_HOOK_PROBE_ARGUMENTS, runAntigravityProbe } from "./runner.ts";
|
|
3
|
+
|
|
4
|
+
/** Reduced state of project-local Antigravity hooks. */
|
|
5
|
+
export type ProjectHookState = "active" | "inactive" | "unknown";
|
|
6
|
+
|
|
7
|
+
/** Result of the bounded `/hooks` inspection. */
|
|
8
|
+
export interface ProjectHookProbeResult {
|
|
9
|
+
state: ProjectHookState;
|
|
10
|
+
warning?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Inspect project-local hooks without retaining commands or hook configuration. */
|
|
14
|
+
export async function probeProjectHooks(options: {
|
|
15
|
+
paths: IsolatedAntigravityPaths;
|
|
16
|
+
cwd: string;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
onProcessStart?: () => void;
|
|
19
|
+
}): Promise<ProjectHookProbeResult> {
|
|
20
|
+
try {
|
|
21
|
+
const result = await runAntigravityProbe({
|
|
22
|
+
paths: options.paths,
|
|
23
|
+
cwd: options.cwd,
|
|
24
|
+
args: [...PROJECT_HOOK_PROBE_ARGUMENTS],
|
|
25
|
+
signal: options.signal,
|
|
26
|
+
timeoutMs: 15_000,
|
|
27
|
+
onProcessStart: options.onProcessStart,
|
|
28
|
+
});
|
|
29
|
+
return classifyHookOutput(result.stdout, result.exitCode);
|
|
30
|
+
} catch {
|
|
31
|
+
return unknownHookResult();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Classify bounded JSON output from the Antigravity hook command. */
|
|
36
|
+
export function classifyHookOutput(
|
|
37
|
+
output: string,
|
|
38
|
+
exitCode: number | null,
|
|
39
|
+
): ProjectHookProbeResult {
|
|
40
|
+
let parsed: unknown;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(output);
|
|
43
|
+
if (typeof parsed === "string") parsed = JSON.parse(parsed);
|
|
44
|
+
} catch {
|
|
45
|
+
return unknownHookResult();
|
|
46
|
+
}
|
|
47
|
+
const observation = inspectHookValue(parsed, { scope: "unknown", insideHookValue: false }, 0);
|
|
48
|
+
if (observation.active) return activeHookResult();
|
|
49
|
+
if (observation.inactive && exitCode === 0) return { state: "inactive" };
|
|
50
|
+
return unknownHookResult();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type HookScope = "project" | "global" | "unknown";
|
|
54
|
+
|
|
55
|
+
interface HookContext {
|
|
56
|
+
scope: HookScope;
|
|
57
|
+
insideHookValue: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inspectHookValue(
|
|
61
|
+
value: unknown,
|
|
62
|
+
context: HookContext,
|
|
63
|
+
depth: number,
|
|
64
|
+
): { active: boolean; inactive: boolean } {
|
|
65
|
+
if (depth > 8 || value === null || typeof value !== "object") {
|
|
66
|
+
return { active: false, inactive: false };
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(value)) return inspectHookArray(value, context, depth);
|
|
69
|
+
if (!isRecord(value)) return { active: false, inactive: false };
|
|
70
|
+
return inspectHookObject(value, context, depth);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function inspectHookArray(
|
|
74
|
+
value: unknown[],
|
|
75
|
+
context: HookContext,
|
|
76
|
+
depth: number,
|
|
77
|
+
): { active: boolean; inactive: boolean } {
|
|
78
|
+
const itemScopes = value.map(scopeOfValue);
|
|
79
|
+
if (
|
|
80
|
+
context.insideHookValue &&
|
|
81
|
+
value.length > 0 &&
|
|
82
|
+
itemScopes.every((scope) => scope === "global")
|
|
83
|
+
) {
|
|
84
|
+
return { active: false, inactive: true };
|
|
85
|
+
}
|
|
86
|
+
const result = value.reduce<{ active: boolean; inactive: boolean }>(
|
|
87
|
+
(current, item) => mergeHookObservation(current, inspectHookValue(item, context, depth + 1)),
|
|
88
|
+
{ active: false, inactive: false },
|
|
89
|
+
);
|
|
90
|
+
if (result.active || result.inactive || !context.insideHookValue) return result;
|
|
91
|
+
if (value.length === 0 || context.scope === "global") {
|
|
92
|
+
return { active: false, inactive: true };
|
|
93
|
+
}
|
|
94
|
+
return { active: true, inactive: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function inspectHookObject(
|
|
98
|
+
value: Record<string, unknown>,
|
|
99
|
+
context: HookContext,
|
|
100
|
+
depth: number,
|
|
101
|
+
): { active: boolean; inactive: boolean } {
|
|
102
|
+
const objectScope = scopeFromObject(value, context.scope);
|
|
103
|
+
const objectContext = { ...context, scope: objectScope };
|
|
104
|
+
return Object.entries(value).reduce<{ active: boolean; inactive: boolean }>(
|
|
105
|
+
(result, [key, child]) =>
|
|
106
|
+
mergeHookObservation(result, inspectHookProperty(key, child, objectContext, depth)),
|
|
107
|
+
{ active: false, inactive: false },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function inspectHookProperty(
|
|
112
|
+
key: string,
|
|
113
|
+
child: unknown,
|
|
114
|
+
context: HookContext,
|
|
115
|
+
depth: number,
|
|
116
|
+
): { active: boolean; inactive: boolean } {
|
|
117
|
+
const normalizedKey = key.toLowerCase().replace(/[-_]/g, "");
|
|
118
|
+
const childContext: HookContext = {
|
|
119
|
+
scope: scopeFromKey(normalizedKey, context.scope),
|
|
120
|
+
insideHookValue:
|
|
121
|
+
context.insideHookValue || normalizedKey.includes("hook") || normalizedKey === "projecthooks",
|
|
122
|
+
};
|
|
123
|
+
if (childContext.insideHookValue && typeof child === "boolean") {
|
|
124
|
+
return booleanHookState(normalizedKey, child, childContext.scope);
|
|
125
|
+
}
|
|
126
|
+
if (childContext.insideHookValue && typeof child === "string") {
|
|
127
|
+
return stringHookState(child, childContext.scope);
|
|
128
|
+
}
|
|
129
|
+
if (typeof child === "string" && ["result", "output", "data", "value"].includes(normalizedKey)) {
|
|
130
|
+
return inspectNestedJson(child, childContext, depth);
|
|
131
|
+
}
|
|
132
|
+
return inspectHookValue(child, childContext, depth + 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function inspectNestedJson(
|
|
136
|
+
value: string,
|
|
137
|
+
context: HookContext,
|
|
138
|
+
depth: number,
|
|
139
|
+
): { active: boolean; inactive: boolean } {
|
|
140
|
+
try {
|
|
141
|
+
return inspectHookValue(JSON.parse(value), context, depth + 1);
|
|
142
|
+
} catch {
|
|
143
|
+
return { active: false, inactive: false };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function scopeOfValue(value: unknown): HookScope | undefined {
|
|
148
|
+
if (!isRecord(value)) return scopeFromValue(value);
|
|
149
|
+
return scopeFromObject(value, "unknown");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function scopeFromObject(value: Record<string, unknown>, fallback: HookScope): HookScope {
|
|
153
|
+
for (const key of ["scope", "source", "location"]) {
|
|
154
|
+
const scope = scopeFromValue(value[key]);
|
|
155
|
+
if (scope) return scope;
|
|
156
|
+
}
|
|
157
|
+
return fallback;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function scopeFromKey(key: string, fallback: HookScope): HookScope {
|
|
161
|
+
if (key.includes("global")) return "global";
|
|
162
|
+
if (key.includes("project") || key.includes("local")) return "project";
|
|
163
|
+
return fallback;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function scopeFromValue(value: unknown): HookScope | undefined {
|
|
167
|
+
if (typeof value !== "string") return undefined;
|
|
168
|
+
const normalized = value.toLowerCase();
|
|
169
|
+
if (normalized.includes("global")) return "global";
|
|
170
|
+
if (normalized.includes("project") || normalized.includes("local")) return "project";
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function booleanHookState(
|
|
175
|
+
key: string,
|
|
176
|
+
value: boolean,
|
|
177
|
+
scope: HookScope,
|
|
178
|
+
): { active: boolean; inactive: boolean } {
|
|
179
|
+
if (!["active", "enabled", "loaded", "running"].includes(key)) {
|
|
180
|
+
return { active: false, inactive: false };
|
|
181
|
+
}
|
|
182
|
+
return scope === "global"
|
|
183
|
+
? { active: false, inactive: !value }
|
|
184
|
+
: { active: value, inactive: !value };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stringHookState(value: string, scope: HookScope): { active: boolean; inactive: boolean } {
|
|
188
|
+
const normalized = value.toLowerCase();
|
|
189
|
+
const active = ["active", "enabled", "running", "loaded"].includes(normalized);
|
|
190
|
+
const inactive = ["inactive", "disabled", "none", "notfound"].includes(normalized);
|
|
191
|
+
return scope === "global" ? { active: false, inactive } : { active, inactive };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
195
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function mergeHookObservation(
|
|
199
|
+
left: { active: boolean; inactive: boolean },
|
|
200
|
+
right: { active: boolean; inactive: boolean },
|
|
201
|
+
): { active: boolean; inactive: boolean } {
|
|
202
|
+
return { active: left.active || right.active, inactive: left.inactive || right.inactive };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function activeHookResult(): ProjectHookProbeResult {
|
|
206
|
+
return {
|
|
207
|
+
state: "active",
|
|
208
|
+
warning:
|
|
209
|
+
"Project-local Antigravity hooks are active. They can run commands, read the Antigravity transcript, and cause side effects outside the Inspection Permission Set.",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function unknownHookResult(): ProjectHookProbeResult {
|
|
214
|
+
return {
|
|
215
|
+
state: "unknown",
|
|
216
|
+
warning:
|
|
217
|
+
"Project-local Antigravity hook state could not be determined. Hooks may run commands, read the Antigravity transcript, and cause side effects outside the Inspection Permission Set.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Limits for streamed Antigravity output. */
|
|
2
|
+
export const MAX_STDOUT_LINE_BYTES = 128 * 1024;
|
|
3
|
+
export const MAX_STDOUT_BYTES = 5 * 1024 * 1024;
|
|
4
|
+
export const MAX_STREAM_EVENTS = 512;
|
|
5
|
+
export const MAX_RETAINED_STDERR_BYTES = 8 * 1024;
|
|
6
|
+
|
|
7
|
+
/** Error raised when a process stream exceeds a package-owned bound. */
|
|
8
|
+
export class StreamLimitError extends Error {
|
|
9
|
+
constructor(message: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "StreamLimitError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Incrementally parses newline-delimited text without buffering an oversized line.
|
|
17
|
+
* The callback runs as soon as each complete line arrives.
|
|
18
|
+
*/
|
|
19
|
+
export class BoundedLineParser {
|
|
20
|
+
readonly #maxLineBytes: number;
|
|
21
|
+
readonly #maxTotalBytes: number;
|
|
22
|
+
#lineParts: Buffer[] = [];
|
|
23
|
+
#lineBytes = 0;
|
|
24
|
+
#totalBytes = 0;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
options: {
|
|
28
|
+
maxLineBytes?: number;
|
|
29
|
+
maxTotalBytes?: number;
|
|
30
|
+
} = {},
|
|
31
|
+
) {
|
|
32
|
+
this.#maxLineBytes = options.maxLineBytes ?? MAX_STDOUT_LINE_BYTES;
|
|
33
|
+
this.#maxTotalBytes = options.maxTotalBytes ?? MAX_STDOUT_BYTES;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get totalBytes(): number {
|
|
37
|
+
return this.#totalBytes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
feed(chunk: Uint8Array, onLine: (line: string) => void): void {
|
|
41
|
+
this.#totalBytes += chunk.byteLength;
|
|
42
|
+
if (this.#totalBytes > this.#maxTotalBytes) {
|
|
43
|
+
throw new StreamLimitError("Antigravity stdout exceeded its byte limit.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let start = 0;
|
|
47
|
+
for (let index = 0; index < chunk.byteLength; index += 1) {
|
|
48
|
+
if (chunk[index] !== 10) continue;
|
|
49
|
+
this.#append(chunk.subarray(start, index));
|
|
50
|
+
onLine(this.#takeLine());
|
|
51
|
+
start = index + 1;
|
|
52
|
+
}
|
|
53
|
+
this.#append(chunk.subarray(start));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
finish(onLine: (line: string) => void): void {
|
|
57
|
+
if (this.#lineBytes === 0) return;
|
|
58
|
+
onLine(this.#takeLine());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#append(part: Uint8Array): void {
|
|
62
|
+
if (part.byteLength === 0) return;
|
|
63
|
+
this.#lineBytes += part.byteLength;
|
|
64
|
+
if (this.#lineBytes > this.#maxLineBytes) {
|
|
65
|
+
throw new StreamLimitError("An Antigravity stdout line exceeded its byte limit.");
|
|
66
|
+
}
|
|
67
|
+
this.#lineParts.push(Buffer.from(part));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#takeLine(): string {
|
|
71
|
+
const line = Buffer.concat(this.#lineParts, this.#lineBytes).toString("utf8");
|
|
72
|
+
this.#lineParts = [];
|
|
73
|
+
this.#lineBytes = 0;
|
|
74
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A bounded stderr collector used only to explain process failures. */
|
|
79
|
+
export class BoundedStderrCapture {
|
|
80
|
+
readonly #maxBytes: number;
|
|
81
|
+
#parts: Buffer[] = [];
|
|
82
|
+
#bytes = 0;
|
|
83
|
+
#truncated = false;
|
|
84
|
+
|
|
85
|
+
constructor(maxBytes = MAX_RETAINED_STDERR_BYTES) {
|
|
86
|
+
this.#maxBytes = maxBytes;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
feed(chunk: Uint8Array): void {
|
|
90
|
+
if (this.#bytes >= this.#maxBytes) {
|
|
91
|
+
this.#truncated = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const remaining = this.#maxBytes - this.#bytes;
|
|
95
|
+
const part = Buffer.from(chunk.subarray(0, remaining));
|
|
96
|
+
this.#parts.push(part);
|
|
97
|
+
this.#bytes += part.byteLength;
|
|
98
|
+
if (part.byteLength < chunk.byteLength) this.#truncated = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
text(): string {
|
|
102
|
+
const value = Buffer.concat(this.#parts).toString("utf8").trim();
|
|
103
|
+
return this.#truncated ? `${value}\n[stderr truncated]`.trim() : value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Parse one bounded NDJSON line and reject malformed JSON. */
|
|
108
|
+
export function parseNdjsonLine(line: string): Record<string, unknown> | undefined {
|
|
109
|
+
if (!line.trim()) return undefined;
|
|
110
|
+
let parsed: unknown;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(line);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new Error("Antigravity returned malformed NDJSON.", { cause: error });
|
|
115
|
+
}
|
|
116
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
117
|
+
throw new Error("Antigravity returned a non-object NDJSON event.");
|
|
118
|
+
}
|
|
119
|
+
return parsed as Record<string, unknown>;
|
|
120
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ERROR_STATES, eventType, isRecord, SUCCESS_STATES, safeString } from "./event-values.ts";
|
|
2
|
+
|
|
3
|
+
/** Adapt an agy event envelope to the small event vocabulary used by the reducer. */
|
|
4
|
+
export function normalizeProtocolEvent(event: Record<string, unknown>): Record<string, unknown> {
|
|
5
|
+
const envelopeType = eventType(event);
|
|
6
|
+
if (envelopeType === "result") return normalizeResultEnvelope(event);
|
|
7
|
+
if (envelopeType === "step_update") return normalizeStepUpdateEnvelope(event);
|
|
8
|
+
return event;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeResultEnvelope(event: Record<string, unknown>): Record<string, unknown> {
|
|
12
|
+
return isRecord(event.result)
|
|
13
|
+
? { ...event, ...event.result, type: "result" }
|
|
14
|
+
: { ...event, type: "result" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeStepUpdateEnvelope(event: Record<string, unknown>): Record<string, unknown> {
|
|
18
|
+
if (!isRecord(event.step_update)) return event;
|
|
19
|
+
const update = event.step_update;
|
|
20
|
+
const stepType = lowerBoundedText(update.step_type ?? update.stepType);
|
|
21
|
+
const toolShaped =
|
|
22
|
+
update.tool_name !== undefined ||
|
|
23
|
+
update.toolName !== undefined ||
|
|
24
|
+
isRecord(update.tool_info) ||
|
|
25
|
+
isRecord(update.toolInfo);
|
|
26
|
+
if (stepType !== "tool" && !(stepType === "" && toolShaped)) {
|
|
27
|
+
return { ...update, type: stepType === "agent_response" ? "assistant" : "step_update" };
|
|
28
|
+
}
|
|
29
|
+
return normalizeToolStep(update);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeToolStep(update: Record<string, unknown>): Record<string, unknown> {
|
|
33
|
+
const state = lowerBoundedText(update.state);
|
|
34
|
+
const toolInfo = getToolInfo(update);
|
|
35
|
+
const parameters = toolInfo?.parameters ?? toolInfo?.input;
|
|
36
|
+
const toolNameValue = update.tool_name ?? update.toolName ?? toolInfo?.name;
|
|
37
|
+
const stepIndex = update.step_index ?? update.stepIndex;
|
|
38
|
+
const active = state === "active" || state === "running";
|
|
39
|
+
const terminal = SUCCESS_STATES.has(state) || ERROR_STATES.has(state);
|
|
40
|
+
return {
|
|
41
|
+
...update,
|
|
42
|
+
type: active ? "tool_use" : terminal ? "tool_result" : "step_update",
|
|
43
|
+
...(stepIndex === undefined ? {} : { id: stepIndex }),
|
|
44
|
+
...(toolNameValue === undefined ? {} : { tool_name: toolNameValue }),
|
|
45
|
+
...(isRecord(parameters) ? { input: parameters } : {}),
|
|
46
|
+
...(terminal ? toolStateFields(state, toolInfo) : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getToolInfo(update: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
51
|
+
if (isRecord(update.tool_info)) return update.tool_info;
|
|
52
|
+
return isRecord(update.toolInfo) ? update.toolInfo : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toolStateFields(
|
|
56
|
+
state: string,
|
|
57
|
+
toolInfo: Record<string, unknown> | undefined,
|
|
58
|
+
): Record<string, unknown> {
|
|
59
|
+
if (SUCCESS_STATES.has(state)) return { status: "success" };
|
|
60
|
+
return {
|
|
61
|
+
status: "error",
|
|
62
|
+
is_error: true,
|
|
63
|
+
...(toolInfo?.error === undefined ? {} : { error: toolInfo.error }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function lowerBoundedText(value: unknown): string {
|
|
68
|
+
return (safeString(value, 40) ?? "").toLowerCase();
|
|
69
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { type IsolatedAntigravityPaths, prepareIsolatedAntigravityHome } from "../isolated-home.ts";
|
|
5
|
+
import type { AntigravityExecutionFacts, AntigravityProgressCallback } from "../types.ts";
|
|
6
|
+
import { eventType, toolName } from "./event-values.ts";
|
|
7
|
+
import { AntigravityEventAccumulator } from "./events.ts";
|
|
8
|
+
import { MAX_STREAM_EVENTS, parseNdjsonLine, StreamLimitError } from "./ndjson.ts";
|
|
9
|
+
import type { AntigravityProbeResult } from "./subprocess.ts";
|
|
10
|
+
import { AntigravityProcessError, runBoundedChildProcess } from "./subprocess.ts";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_RUN_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
13
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 15 * 1_000;
|
|
14
|
+
|
|
15
|
+
export type { AntigravityProbeResult } from "./subprocess.ts";
|
|
16
|
+
export { AntigravityProcessError } from "./subprocess.ts";
|
|
17
|
+
|
|
18
|
+
/** Build the fixed paid-run argument list. */
|
|
19
|
+
export function buildAntigravityRunArguments(
|
|
20
|
+
schemaPath: string,
|
|
21
|
+
model: string,
|
|
22
|
+
conversationId?: string,
|
|
23
|
+
workspaceDirectory?: string,
|
|
24
|
+
): string[] {
|
|
25
|
+
return [
|
|
26
|
+
"--input-format",
|
|
27
|
+
"stream-json",
|
|
28
|
+
"--output-format",
|
|
29
|
+
"stream-json",
|
|
30
|
+
"--json-schema",
|
|
31
|
+
schemaPath,
|
|
32
|
+
"--model",
|
|
33
|
+
model,
|
|
34
|
+
"--print-timeout",
|
|
35
|
+
"5m",
|
|
36
|
+
"--sandbox",
|
|
37
|
+
...(workspaceDirectory ? ["--add-dir", workspaceDirectory] : []),
|
|
38
|
+
"--disable-slash-commands",
|
|
39
|
+
...(conversationId ? ["--conversation", conversationId] : []),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Fixed arguments for the pre-run project hook probe. */
|
|
44
|
+
export const PROJECT_HOOK_PROBE_ARGUMENTS = Object.freeze([
|
|
45
|
+
"-p",
|
|
46
|
+
"/hooks",
|
|
47
|
+
"--output-format",
|
|
48
|
+
"json",
|
|
49
|
+
"--print-timeout",
|
|
50
|
+
"15s",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/** Run a bounded structured Antigravity conversation. */
|
|
54
|
+
export async function runAntigravityConversation(options: {
|
|
55
|
+
paths: IsolatedAntigravityPaths;
|
|
56
|
+
cwd: string;
|
|
57
|
+
prompt: string;
|
|
58
|
+
model: string;
|
|
59
|
+
conversationId?: string;
|
|
60
|
+
/** Add the selected project directory to agy's sandbox workspace. */
|
|
61
|
+
workspaceDirectory?: string;
|
|
62
|
+
schema: Record<string, unknown>;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
timeoutMs?: number;
|
|
65
|
+
/** Override the temporary schema parent for isolated integration tests. */
|
|
66
|
+
schemaDirectoryParent?: string;
|
|
67
|
+
onActivity?: AntigravityProgressCallback;
|
|
68
|
+
onProcessStart?: () => void;
|
|
69
|
+
}): Promise<AntigravityExecutionFacts> {
|
|
70
|
+
await prepareIsolatedAntigravityHome(options.paths);
|
|
71
|
+
const schemaDirectory = await mkdtemp(
|
|
72
|
+
join(options.schemaDirectoryParent ?? tmpdir(), "supi-antigravity-schema-"),
|
|
73
|
+
);
|
|
74
|
+
const schemaPath = join(schemaDirectory, "answer.json");
|
|
75
|
+
try {
|
|
76
|
+
await writeFile(schemaPath, `${JSON.stringify(options.schema)}\n`, { mode: 0o600 });
|
|
77
|
+
const args = buildAntigravityRunArguments(
|
|
78
|
+
schemaPath,
|
|
79
|
+
options.model,
|
|
80
|
+
options.conversationId,
|
|
81
|
+
options.workspaceDirectory,
|
|
82
|
+
);
|
|
83
|
+
const accumulator = new AntigravityEventAccumulator({ workspaceDirectory: options.cwd });
|
|
84
|
+
let eventCount = 0;
|
|
85
|
+
await runBoundedChildProcess({
|
|
86
|
+
args,
|
|
87
|
+
cwd: options.cwd,
|
|
88
|
+
homeDir: options.paths.homeDir,
|
|
89
|
+
prompt: options.prompt,
|
|
90
|
+
signal: options.signal,
|
|
91
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS,
|
|
92
|
+
onLine: (line) => {
|
|
93
|
+
const event = parseNdjsonLine(line);
|
|
94
|
+
if (!event) return;
|
|
95
|
+
eventCount += 1;
|
|
96
|
+
if (eventCount > MAX_STREAM_EVENTS) {
|
|
97
|
+
throw new StreamLimitError("Antigravity returned too many stream events.");
|
|
98
|
+
}
|
|
99
|
+
options.onActivity?.(safeActivityLabel(event));
|
|
100
|
+
accumulator.consume(event);
|
|
101
|
+
},
|
|
102
|
+
onProcessStart: options.onProcessStart,
|
|
103
|
+
});
|
|
104
|
+
return accumulator.finish();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof AntigravityProcessError) throw error;
|
|
107
|
+
// biome-ignore lint/style/useErrorCause: the custom process error preserves the protocol cause.
|
|
108
|
+
throw new AntigravityProcessError("Antigravity returned an invalid stream.", "protocol", {
|
|
109
|
+
cause: error,
|
|
110
|
+
});
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(schemaDirectory, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Run one bounded agy command and retain only bounded text for its caller. */
|
|
117
|
+
export async function runAntigravityProbe(options: {
|
|
118
|
+
paths: IsolatedAntigravityPaths;
|
|
119
|
+
cwd: string;
|
|
120
|
+
args: string[];
|
|
121
|
+
signal?: AbortSignal;
|
|
122
|
+
timeoutMs?: number;
|
|
123
|
+
maxStdoutBytes?: number;
|
|
124
|
+
onProcessStart?: () => void;
|
|
125
|
+
}): Promise<AntigravityProbeResult> {
|
|
126
|
+
await prepareIsolatedAntigravityHome(options.paths);
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
const result = await runBoundedChildProcess({
|
|
129
|
+
args: options.args,
|
|
130
|
+
cwd: options.cwd,
|
|
131
|
+
homeDir: options.paths.homeDir,
|
|
132
|
+
signal: options.signal,
|
|
133
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
134
|
+
maxStdoutBytes: options.maxStdoutBytes,
|
|
135
|
+
allowNonZero: true,
|
|
136
|
+
onLine: (line) => lines.push(line),
|
|
137
|
+
onProcessStart: options.onProcessStart,
|
|
138
|
+
});
|
|
139
|
+
return { ...result, stdout: lines.join("\n") };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function safeActivityLabel(event: Record<string, unknown>): string {
|
|
143
|
+
const name = toolName(event);
|
|
144
|
+
if (name) return `activity: ${name}`;
|
|
145
|
+
const type = eventType(event);
|
|
146
|
+
return type ? `activity: ${type}` : "activity: processing";
|
|
147
|
+
}
|