@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.110
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/CHANGELOG.md +520 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +388 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +71 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
2
|
+
import { ToolObservations, agyToolObservation } from "./tool-observations.mjs";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import net from "node:net";
|
|
7
|
+
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
import { ObservationClock } from "./observation-clock.mjs";
|
|
9
|
+
import { hookBudget } from "./native-hook-command.mjs";
|
|
10
|
+
import { agyWorkspaceHooks } from "./agy-hooks.mjs";
|
|
11
|
+
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { observeModel } from "./model-observations.mjs";
|
|
14
|
+
import { AgyError, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
15
|
+
import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
16
|
+
|
|
17
|
+
const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
|
|
18
|
+
class DaemonError extends AgyError {}
|
|
19
|
+
const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
|
|
20
|
+
function locations(stateDir) {
|
|
21
|
+
const dir = absolute(stateDir, "--state-dir"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 22), local = path.join(dir, "daemon.sock");
|
|
22
|
+
// `tsx` creates Unix-domain sockets below TMPDIR. An evaluation path is
|
|
23
|
+
// intentionally descriptive and can exceed the platform's socket-path cap,
|
|
24
|
+
// so provider temp files must not inherit that path.
|
|
25
|
+
return { dir, socket: Buffer.byteLength(local) < 100 ? local : `/tmp/dd-agy-${digest}.sock`, state: path.join(dir, "daemon.json"), journal: path.join(dir, "events.jsonl"), log: path.join(dir, "daemon.log"), gemini: path.join(dir, "gemini"), runtime: path.join(dir, "gemini", "runtime"), config: path.join(dir, "gemini", "config"), temporary: `/tmp/dd-agy-tmp-${digest}` };
|
|
26
|
+
}
|
|
27
|
+
async function writeJson(file, value) { const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`; await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(temporary, file); }
|
|
28
|
+
async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
29
|
+
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
30
|
+
const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
31
|
+
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"]) delete env[key]; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
|
|
32
|
+
function errorPayload(error) { return { code: error.code ?? "operation_failed", message: error.message ?? String(error), retryable: error.retryable === true, ...(error.details === undefined ? {} : { details: error.details }) }; }
|
|
33
|
+
async function forwardUsage(config, sessionId, result, tools, descendants) {
|
|
34
|
+
if (!config.ddFlowBin || !config.projectRoot) return;
|
|
35
|
+
const snapshot = usageSnapshot(result, tools), hasChildren = descendants.length > 0;
|
|
36
|
+
if (snapshot.total_tokens === null) return { status: "unavailable", reason: "provider_did_not_report_total_tokens" };
|
|
37
|
+
const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: hasChildren ? "unknown" : "physical_session", completeness: hasChildren ? "partial" : "complete", usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cache_read_tokens, totalTokens: snapshot.total_tokens }, tool_calls: snapshot.tool_calls };
|
|
38
|
+
const target = executable(config.ddFlowBin, ["agy", "usage", "ingest", "--project-root", config.projectRoot, "--json"]);
|
|
39
|
+
await new Promise((resolve, reject) => { const child = spawn(target.command, target.args, { env: { ...process.env, DD_FLOW_HOME: config.ddFlowHome }, stdio: ["pipe", "ignore", "pipe"] }); let stderr = ""; child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk); child.on("error", reject); child.on("close", code => code === 0 ? resolve() : reject(new DaemonError("agy_usage_ingest_failed", stderr.trim() || "dd-flow rejected Antigravity usage"))); child.stdin.end(`${JSON.stringify(payload)}\n`); });
|
|
40
|
+
return { status: "ingested" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function prepare(paths, config) {
|
|
44
|
+
config.stateDir = paths.dir;
|
|
45
|
+
config.hookBinding = "environment-v1";
|
|
46
|
+
const hooks = agyWorkspaceHooks();
|
|
47
|
+
for (const dir of [paths.dir, paths.gemini, paths.runtime, paths.config, paths.temporary]) { await mkdir(dir, { recursive: true, mode: 0o700 }); await chmod(dir, 0o700); }
|
|
48
|
+
if (config.noFlow) {
|
|
49
|
+
// Technical probes still need native settlement, but must not install
|
|
50
|
+
// workspace hooks or forward any lifecycle mutation to dd-flow.
|
|
51
|
+
await writeJson(path.join(paths.config, "hooks.json"), { "dd-flow-observer": { Stop: hooks["dd-flow"].Stop } });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Native discovery follows cwd (feature worktree/reviewer clone), while
|
|
55
|
+
// projectRoot remains the stable lifecycle identity returned to dd-flow.
|
|
56
|
+
Object.assign(config, await qualifyAgyHooks(config.cwd ?? config.projectRoot, { stateDir: paths.dir }));
|
|
57
|
+
// Workspace discovery covers root and children without global duplicates.
|
|
58
|
+
await writeJson(path.join(paths.config, "hooks.json"), {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function qualifyAgyHooks(projectRoot, { stateDir = null, install = false } = {}) {
|
|
62
|
+
const hooks = agyWorkspaceHooks();
|
|
63
|
+
// Register in workspace discovery before creating any Session: native root
|
|
64
|
+
// and children load these named hook definitions without a global duplicate.
|
|
65
|
+
// Replace our prior handlers, including the obsolete wildcard post hook,
|
|
66
|
+
// without dropping unrelated commands in a shared matcher group.
|
|
67
|
+
const workspaceConfig = path.join(absolute(projectRoot, "--project-root"), ".agents");
|
|
68
|
+
const workspaceHooks = path.join(workspaceConfig, "hooks.json");
|
|
69
|
+
let existing = {}, existingText = "";
|
|
70
|
+
try {
|
|
71
|
+
existingText = await readFile(workspaceHooks, "utf8");
|
|
72
|
+
const parsed = JSON.parse(existingText);
|
|
73
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error("not an object");
|
|
74
|
+
existing = parsed;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.code !== "ENOENT") throw new DaemonError("workspace_hooks_invalid", `cannot install Antigravity lifecycle hooks: ${error.message}`);
|
|
77
|
+
if (!install) throw new DaemonError("workspace_hooks_unqualified", "Antigravity hooks must be installed during qualification before daemon start", false, { hooks_path: workspaceHooks });
|
|
78
|
+
}
|
|
79
|
+
const isManagedHandler = handler => typeof handler?.command === "string"
|
|
80
|
+
&& ((handler.command.includes("DD_AGY_HOOK_ENTRY") && handler.command.includes(" hook handle --event ") && handler.command.includes("DD_AGY_STATE_DIR")) || /(?:^|\/)dd-agy\.mjs['"]?\s+hook handle/.test(handler.command));
|
|
81
|
+
const events = ["PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"];
|
|
82
|
+
const existingManaged = Object.entries(existing).flatMap(([name, definition]) => {
|
|
83
|
+
if (events.includes(name) && Array.isArray(definition)) return definition;
|
|
84
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition)) return [];
|
|
85
|
+
return events.flatMap(event => Array.isArray(definition[event]) ? definition[event] : []);
|
|
86
|
+
}).flatMap(item => Array.isArray(item?.hooks) ? item.hooks : [item]).filter(handler => isManagedHandler(handler) && handler.command.includes("--state-dir") && !handler.command.includes("DD_AGY_STATE_DIR"));
|
|
87
|
+
if (existingManaged.some(handler => !stateDir || !handler.command.includes(`--state-dir ${shell(stateDir)}`))) {
|
|
88
|
+
throw new DaemonError("workspace_hooks_drifted", "qualified Antigravity hook routes to another daemon state directory", false, { hooks_path: workspaceHooks, expected_state_dir: stateDir });
|
|
89
|
+
}
|
|
90
|
+
const retain = handlers => handlers.flatMap(item => {
|
|
91
|
+
if (!Array.isArray(item?.hooks)) return isManagedHandler(item) ? [] : [item];
|
|
92
|
+
const remaining = item.hooks.filter(handler => !isManagedHandler(handler));
|
|
93
|
+
return remaining.length ? [{ ...item, hooks: remaining }] : [];
|
|
94
|
+
});
|
|
95
|
+
const workspace = Object.create(null), legacy = Object.create(null);
|
|
96
|
+
for (const [name, definition] of Object.entries(existing)) {
|
|
97
|
+
// Migrate the old adapter's event-keyed workspace format.
|
|
98
|
+
if (events.includes(name) && Array.isArray(definition)) {
|
|
99
|
+
const remaining = retain(definition);
|
|
100
|
+
if (remaining.length) legacy[name] = remaining;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!definition || Array.isArray(definition) || typeof definition !== "object") {
|
|
104
|
+
throw new DaemonError("workspace_hooks_invalid", `invalid named hook definition: ${name}`);
|
|
105
|
+
}
|
|
106
|
+
const cleaned = { ...definition };
|
|
107
|
+
for (const event of events) if (Array.isArray(cleaned[event])) {
|
|
108
|
+
const remaining = retain(cleaned[event]);
|
|
109
|
+
if (remaining.length || cleaned[event].length === 0) cleaned[event] = remaining;
|
|
110
|
+
else delete cleaned[event];
|
|
111
|
+
}
|
|
112
|
+
workspace[name] = cleaned;
|
|
113
|
+
}
|
|
114
|
+
if (Object.keys(legacy).length) {
|
|
115
|
+
let name = "dd-flow-legacy";
|
|
116
|
+
while (Object.hasOwn(workspace, name)) name += "-retained";
|
|
117
|
+
workspace[name] = legacy;
|
|
118
|
+
}
|
|
119
|
+
const managed = workspace["dd-flow"] ?? {};
|
|
120
|
+
if (managed.enabled === false) throw new DaemonError("workspace_hooks_invalid", "dd-flow lifecycle hooks are disabled");
|
|
121
|
+
for (const [event, handlers] of Object.entries(hooks["dd-flow"])) {
|
|
122
|
+
managed[event] = [...(managed[event] ?? []), ...handlers];
|
|
123
|
+
}
|
|
124
|
+
workspace["dd-flow"] = managed;
|
|
125
|
+
const serializedWorkspace = `${JSON.stringify(workspace, null, 2)}\n`;
|
|
126
|
+
// Hook installation is a qualification/ownership boundary. Reusing the
|
|
127
|
+
// qualified command must not rewrite the project file when the pinned engine,
|
|
128
|
+
// daemon directory or workspace location changes.
|
|
129
|
+
const installed = isDeepStrictEqual(existing, JSON.parse(serializedWorkspace));
|
|
130
|
+
if (!installed && !install) throw new DaemonError("workspace_hooks_unqualified", "Antigravity hook definitions differ from the qualified contract", false, { hooks_path: workspaceHooks });
|
|
131
|
+
if (!installed) { await mkdir(workspaceConfig, { recursive: true, mode: 0o700 }); await writeFile(workspaceHooks, serializedWorkspace, { mode: 0o600 }); }
|
|
132
|
+
return { hooksSha256: createHash("sha256").update(JSON.stringify(hooks)).digest("hex"), workspaceHooksPath: workspaceHooks,
|
|
133
|
+
workspaceHooksSha256: createHash("sha256").update(installed ? existingText : serializedWorkspace).digest("hex"), workspaceHooksInstalled: installed ? "qualified_existing" : "qualified_install" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, requestId = null) {
|
|
137
|
+
const paths = locations(stateDir), request = { schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
138
|
+
return await new Promise((resolve, reject) => {
|
|
139
|
+
const client = net.createConnection(paths.socket); let buffer = "", settled = false, timer, checking = false, lastActivity = Date.now();
|
|
140
|
+
const clock = new ObservationClock({ timeoutMs, onGap: gap => { void appendFile(paths.journal, `${JSON.stringify({ kind: "observation_gap", payload: { ...gap, operation_id: request.id } })}\n`, { mode: 0o600 }).catch(() => {}); } });
|
|
141
|
+
const finish = (error, result) => { if (settled) return; settled = true; if (timer) clearInterval(timer); client.destroy(); error ? reject(error) : resolve(result); };
|
|
142
|
+
const poll = Math.min(5_000, Math.max(25, Math.floor(timeoutMs / 20)));
|
|
143
|
+
timer = setInterval(async () => {
|
|
144
|
+
if (settled || checking) return;
|
|
145
|
+
checking = true;
|
|
146
|
+
try {
|
|
147
|
+
if (operation !== "session.prompt") {
|
|
148
|
+
if (clock.sample()) finish(new DaemonError("daemon_timeout", `${operation} timed out`, true));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
let status = null;
|
|
152
|
+
try { status = await callDaemon(stateDir, "daemon.status", {}, Math.min(timeoutMs, 5_000)); }
|
|
153
|
+
catch { /* The original RPC retains the authoritative failure. */ }
|
|
154
|
+
const observed = Date.parse(status?.last_activity_at ?? "");
|
|
155
|
+
const progressed = status?.active_tree === true && Number.isFinite(observed) && observed > lastActivity;
|
|
156
|
+
if (progressed) lastActivity = observed;
|
|
157
|
+
if (!clock.sample(progressed ? lastActivity : undefined, progressed ? lastActivity : undefined)) return;
|
|
158
|
+
finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
|
|
159
|
+
operation_id: request.id,
|
|
160
|
+
last_activity_at: status?.last_activity_at ?? null,
|
|
161
|
+
active_tree: status?.active_tree === true
|
|
162
|
+
}));
|
|
163
|
+
} finally { checking = false; }
|
|
164
|
+
}, poll);
|
|
165
|
+
client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("data", chunk => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; try { const response = JSON.parse(buffer.slice(0, index)); response.ok ? finish(null, response.result) : finish(new DaemonError(response.error.code, response.error.message, response.error.retryable, response.error.details)); } catch (error) { finish(new DaemonError("daemon_protocol_mismatch", error.message)); } }); client.on("error", error => finish(new DaemonError("daemon_not_running", error.message, true))); client.on("end", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true))); client.on("close", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true)));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
170
|
+
// Called only after authorizeRetainedDaemonResume verifies the old daemon,
|
|
171
|
+
// provider processes and operation ledger. Native outcomes remain historical;
|
|
172
|
+
// their old liveness flags cannot describe the replacement process tree.
|
|
173
|
+
export function retainedAgyState(previous, sessionId) {
|
|
174
|
+
if (!sessionId || !previous?.sessions?.some(session => session.provider_session_id === sessionId)) return {};
|
|
175
|
+
if (previous.shutdown_state !== "clean" || previous.active_tree !== false) return previous;
|
|
176
|
+
return {
|
|
177
|
+
...previous,
|
|
178
|
+
unclaimed_activity: false,
|
|
179
|
+
retained_tree_settlement: { daemon_id: previous.daemon_id, session_id: sessionId, turn_generation: previous.turn_generation ?? 0, evidence: "verified_clean_daemon_shutdown" },
|
|
180
|
+
session_observations: (previous.session_observations ?? []).map(([id, observation]) => [id, id === sessionId ? { ...observation, retained_stop: observation.stop ?? null, stop: null } : observation]),
|
|
181
|
+
descendants: (previous.descendants ?? []).map(child => child.tree_settled === true ? child : { ...child, tree_settled: true, settlement_evidence: "prior_clean_daemon_shutdown", settlement_daemon_id: previous.daemon_id })
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export async function startDaemon(options) {
|
|
185
|
+
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
186
|
+
const previous = await readState(paths.dir), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")), config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_AGY_BIN ?? "agy", provider: String(options.provider ?? "google"), model: String(options.model ?? "gemini-3.1-pro-high"), reasoning: String(options.reasoning ?? "high"), mode: String(options.mode ?? "accept-edits"), projectRoot: noFlow ? cwd : await realpath(absolute(options.projectRoot, "--project-root")), ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), resourceHome: noFlow ? null : (process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null), env: noFlow ? {} : { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") }, entryPath: absolute(options.entryPath, "dd-agy entry"), daemonId: randomUUID(), resumeSessionId: options.sessionId ?? null, geminiDir: paths.gemini, appDataDir: "runtime", temporary: paths.temporary, noFlow };
|
|
187
|
+
await prepareRuntimeOwner(config, paths.dir);
|
|
188
|
+
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); return { ...status, already_running: true }; } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
|
|
189
|
+
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
190
|
+
if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous Antigravity daemon died with an active or unproven tree");
|
|
191
|
+
await removeSocket(paths); await prepare(paths, config);
|
|
192
|
+
const version = (await runAgy(config.bin, ["--version"], { cwd: config.cwd, env: sanitizedEnv(config), timeoutMs: 10_000 })).stdout.trim();
|
|
193
|
+
const resourceProcess = await registerDaemonProcess(config, { kind: "agy-daemon", owner: `agy:${path.basename(paths.dir)}`, operation: `agy-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
|
|
194
|
+
const retained = retainedAgyState(previous, config.resumeSessionId);
|
|
195
|
+
await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
|
|
196
|
+
const log = await open(paths.log, "a", 0o600), child = spawn(process.execPath, [config.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: sanitizedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
197
|
+
try {
|
|
198
|
+
await confirmDaemonProcess(config, resourceProcess, child);
|
|
199
|
+
const deadline = Date.now() + 30_000; while (Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 100));
|
|
200
|
+
const persisted = await readState(paths.dir);
|
|
201
|
+
if (persisted?.shutdown_state === "failed") {
|
|
202
|
+
const failure = persisted.startup_error ?? {};
|
|
203
|
+
throw new DaemonError(failure.code ?? "daemon_start_failed", failure.message ?? "Antigravity daemon failed before becoming ready", failure.retryable === true, failure.details);
|
|
204
|
+
}
|
|
205
|
+
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (status.provider_ready) return status; } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
|
|
206
|
+
}
|
|
207
|
+
throw new DaemonError("daemon_start_failed", "Antigravity daemon did not become ready", true);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.dir), config.daemonId);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); };
|
|
214
|
+
|
|
215
|
+
export class Runtime {
|
|
216
|
+
constructor(paths, state) { this.paths = paths; this.state = state; this.config = state.config; this.child = null; this.providerProcess = state.provider_process ?? null; this.providerFinalization = Promise.resolve(); this.buffer = ""; this.init = null; this.readyPromise = null; this.active = null; this.exited = false; this.toolObservations = new ToolObservations(); for (const reason of state.tool_observation_gaps ?? []) this.toolObservations.gaps.add(reason); for (const event of state.tool_observations ?? []) this.toolObservations.add(event); if (state.tool_calls?.length && !state.tool_observations) this.toolObservations.gaps.add("legacy_tools_not_attributed"); this.turnGeneration = state.turn_generation ?? 0; this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); this.lastResult = state.last_result ?? null; this.sessionObservations = new Map(state.session_observations ?? []); this.startupError = null; this.persisting = Promise.resolve(); this.draining = Promise.resolve(); this.lastActivityAt = state.last_activity_at ?? null; this.usageIngest = null; this.lastProviderEvent = null; }
|
|
217
|
+
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); const queued = performance.now(); this.persisting = this.persisting.then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
|
|
218
|
+
async journal(kind, payload = {}) { const line = `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), kind, ...payload })}\n`; for (const journal of new Set([this.paths.journal, this.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
219
|
+
args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", "--model", this.config.model, "--effort", this.config.reasoning, "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "6h", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
|
|
220
|
+
requireSessionIdentity(requestedId) {
|
|
221
|
+
const observedId = this.init?.conversation_id ?? null;
|
|
222
|
+
if (!requestedId || requestedId === observedId) return observedId;
|
|
223
|
+
throw new DaemonError("session_identity_mismatch", "requested Antigravity Session is not owned by this daemon", false, {
|
|
224
|
+
harness: "antigravity-cli", expected_session_id: requestedId, observed_session_id: observedId
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
markActivity() { this.lastActivityAt = new Date().toISOString(); }
|
|
228
|
+
observeProviderActivity(event) {
|
|
229
|
+
const fingerprint = createHash("sha256").update(JSON.stringify(event)).digest("hex");
|
|
230
|
+
if (this.lastProviderEvent === fingerprint) return;
|
|
231
|
+
this.lastProviderEvent = fingerprint;
|
|
232
|
+
if (["init", "step_update", "result"].includes(event.event)) this.markActivity();
|
|
233
|
+
}
|
|
234
|
+
async start(sessionId = null) {
|
|
235
|
+
if (this.child && !this.exited) {
|
|
236
|
+
await this.readyPromise;
|
|
237
|
+
this.requireSessionIdentity(sessionId);
|
|
238
|
+
return this.init;
|
|
239
|
+
}
|
|
240
|
+
await this.providerFinalization;
|
|
241
|
+
const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
|
|
242
|
+
let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
|
|
243
|
+
// The provider can reject before durable registration finishes. Keep a
|
|
244
|
+
// handler attached during that small window; `start()` still returns the
|
|
245
|
+
// original rejection below.
|
|
246
|
+
void this.readyPromise.catch(() => {});
|
|
247
|
+
this.providerProcess = await registerDaemonProcess(this.config, { kind: "agy-provider", owner: `agy:${path.basename(this.paths.dir)}`, operation: `agy-provider:${path.basename(this.paths.dir)}`, stdout: this.paths.log, stderr: this.paths.log });
|
|
248
|
+
const providerProcess = this.providerProcess;
|
|
249
|
+
try { await this.persist({ provider_process: providerProcess }); }
|
|
250
|
+
catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
|
|
251
|
+
try { this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
|
|
252
|
+
catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
|
|
253
|
+
const providerChild = this.child;
|
|
254
|
+
let allowProviderFinalization;
|
|
255
|
+
const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
|
|
256
|
+
this.child.stdout.setEncoding("utf8").on("data", chunk => { this.buffer += chunk; this.draining = this.draining.then(() => this.drain(readyResolve)).catch(error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } }); });
|
|
257
|
+
this.child.stderr.setEncoding("utf8").on("data", chunk => void this.journal("stderr", { text: String(chunk).slice(0, 4000) }));
|
|
258
|
+
this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
|
|
259
|
+
// A dead leader is not yet a finalized tree: stop must also join the
|
|
260
|
+
// pending drain, process-group check and final persistence/lease writes.
|
|
261
|
+
this.providerFinalization = new Promise((resolve, reject) => {
|
|
262
|
+
this.child.on("close", (code, signal) => {
|
|
263
|
+
void this.draining.then(async () => {
|
|
264
|
+
const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
|
|
265
|
+
const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
|
|
266
|
+
if (beforeInit) readyReject(error);
|
|
267
|
+
if (this.active) { this.active.reject(error); this.active = null; }
|
|
268
|
+
const patch = beforeInit ? { shutdown_state: "failed", startup_error: errorPayload(error), provider_exit: { code, signal }, active_tree: false } : { provider_exit: { code, signal }, active_tree: interrupted };
|
|
269
|
+
await providerRegistered;
|
|
270
|
+
this.retryProviderFinalization = async (graceMs = 1_000) => {
|
|
271
|
+
try {
|
|
272
|
+
await stopProcessGroup(providerChild, graceMs);
|
|
273
|
+
await this.persist(patch);
|
|
274
|
+
await finishDaemonProcess(this.config, providerProcess, beforeInit || interrupted ? "failed" : "stopped", beforeInit || interrupted ? "provider_interrupted" : "provider_stopped");
|
|
275
|
+
this.retryProviderFinalization = null;
|
|
276
|
+
} catch (cleanup) { await this.persist({ shutdown_state: "cleanup_failed", active_tree: true, cleanup_error: errorPayload(cleanup) }); throw cleanup; }
|
|
277
|
+
finally { if (beforeInit) this.server?.close(); }
|
|
278
|
+
};
|
|
279
|
+
await this.retryProviderFinalization();
|
|
280
|
+
}).then(resolve, reject);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
void this.providerFinalization.catch(() => {});
|
|
284
|
+
try { await confirmDaemonProcess(this.config, providerProcess, this.child); await this.persist({ provider_pid: this.child.pid }); scheduleDaemonProcessHeartbeat(this.config, providerProcess); }
|
|
285
|
+
catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
|
|
286
|
+
finally { allowProviderFinalization(); }
|
|
287
|
+
let readyTimer;
|
|
288
|
+
try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = setTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000); })]); }
|
|
289
|
+
finally { clearTimeout(readyTimer); }
|
|
290
|
+
}
|
|
291
|
+
async drain(readyResolve) { let index; while ((index = this.buffer.indexOf("\n")) >= 0) { const line = this.buffer.slice(0, index).trim(); this.buffer = this.buffer.slice(index + 1); if (!line) continue; let event; try { event = JSON.parse(line); } catch { await this.journal("invalid_json", { line: line.slice(0, 1000) }); continue; }
|
|
292
|
+
// Record an early provider rejection before journalling: a short-lived
|
|
293
|
+
// child can close while the asynchronous write is still pending.
|
|
294
|
+
if (!this.init && event.event === "result" && event.result?.status === "ERROR") this.startupError = new DaemonError("agy_provider_rejected", String(event.result.error ?? "Antigravity rejected the Session before initialization"), false, { provider_result: event.result });
|
|
295
|
+
this.observeProviderActivity(event); await this.journal("provider_event", { event }); if (event.event === "init") { this.init = { conversation_id: event.conversation_id, ...(event.init ?? {}) }; if (!this.init.conversation_id) throw new DaemonError("agy_init_missing", "Antigravity init has no conversation ID"); const requested = { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode, permission_mode: "always-proceed" }; this.profile = assertProfile(requested, observedProfile(this.init, requested)); await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: this.init.conversation_id, requested, observed: this.profile.observed, source: "native.init" }); await this.persist({ shutdown_state: "running", active_tree: false }); readyResolve(this.init); } else if (event.event === "step_update") this.observeStep(event.step_update ?? {}); else if (event.event === "result") { if (this.startupError) throw this.startupError; await this.finishTurn(event.result ?? {}); } } }
|
|
296
|
+
observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(sessionId) ?? {}; if (Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1)) this.childActivity(sessionId); if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) }); this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id)); for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) { const prior = this.descendants.get(child.conversation_id); this.descendants.set(child.conversation_id, { ...prior, provider_session_id: child.conversation_id, parent_provider_session_id: sessionId ?? null, role: child.role ?? null, subagent_type: child.type_name ?? null, log_uri: child.log_uri ?? null, workspace_uris: child.workspace_uris ?? [], status: prior?.status ?? "unknown" }); } }
|
|
297
|
+
toolSnapshot() { const summary = this.toolObservations.summary(); const failures_by_name = {}; for (const event of this.toolObservations.calls.values()) if (event.status === "failed") failures_by_name[event.name] = (failures_by_name[event.name] ?? 0) + 1; return { ...summary, by_name: summary.by_tool, failures_by_name }; }
|
|
298
|
+
async finishTurn(result) {
|
|
299
|
+
if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
|
|
300
|
+
if (!["SUCCESS", "ERROR", "CANCELLED"].includes(result.status)) throw new DaemonError("agy_terminal_result_invalid", "Antigravity result has no recognized terminal status", false, { provider_result: result });
|
|
301
|
+
if (result.conversation_id && result.conversation_id !== this.init?.conversation_id) {
|
|
302
|
+
await this.journal("foreign_terminal_observed", { conversation_id: result.conversation_id, result });
|
|
303
|
+
const child = this.descendants.get(result.conversation_id);
|
|
304
|
+
if (child) {
|
|
305
|
+
this.descendants.set(result.conversation_id, { ...child, status: result.status === "SUCCESS" ? "completed" : result.status === "ERROR" ? "failed" : "cancelled" });
|
|
306
|
+
await this.completePendingTurn();
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const current = this.active;
|
|
311
|
+
// The ordered stream returns one result per input. Model usage counters
|
|
312
|
+
// are not request IDs: native command rejections can report zero turns.
|
|
313
|
+
// prompt() drains earlier events before accepting another input.
|
|
314
|
+
if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
|
|
315
|
+
this.lastResult = result; this.markActivity();
|
|
316
|
+
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
317
|
+
const pendingTerminal = current && result.status === "SUCCESS";
|
|
318
|
+
if (pendingTerminal) current.terminalResult = result;
|
|
319
|
+
if (pendingTerminal && rootStop?.fullyIdle === true) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" });
|
|
320
|
+
const terminalReceipt = pendingTerminal ? this.receipt(result) : null;
|
|
321
|
+
await this.persist({ active_tree: pendingTerminal ? !terminalReceipt.settled : result.status === "RUNNING" || rootStop?.fullyIdle === false, last_result: result });
|
|
322
|
+
try { this.usageIngest = await forwardUsage(this.config, result.conversation_id ?? this.init.conversation_id, result, this.toolSnapshot(), [...this.descendants.values()]); }
|
|
323
|
+
catch (error) { this.usageIngest = { status: "failed", error: error.message ?? String(error) }; await this.journal("usage_ingest_failed", this.usageIngest); }
|
|
324
|
+
if (!current) return;
|
|
325
|
+
if (this.active !== current) return;
|
|
326
|
+
if (result.status === "ERROR" || result.status === "CANCELLED") {
|
|
327
|
+
this.active = null;
|
|
328
|
+
const message = String(result.error ?? "Antigravity returned ERROR"); const normalized = message.toLowerCase();
|
|
329
|
+
const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
|
|
330
|
+
: /rate.?limit|too many requests|\b429\b/.test(normalized) ? "agy_provider_rate_limited"
|
|
331
|
+
: /auth|unauthori[sz]ed|forbidden|account/.test(normalized) ? "agy_provider_account"
|
|
332
|
+
: "agy_provider_failed";
|
|
333
|
+
return current.reject(new DaemonError(code, message, ["agy_provider_quota_exhausted", "agy_provider_rate_limited"].includes(code), { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
|
|
334
|
+
}
|
|
335
|
+
await this.completePendingTurn();
|
|
336
|
+
}
|
|
337
|
+
async completePendingTurn() {
|
|
338
|
+
await this.observeHookFailures();
|
|
339
|
+
const current = this.active;
|
|
340
|
+
if (!current?.terminalResult || current.terminalResult !== this.lastResult) return;
|
|
341
|
+
await this.persist({ active_tree: !this.receipt().settled });
|
|
342
|
+
// Hooks can invalidate the tree while persistence is pending. Never
|
|
343
|
+
// publish a proof captured before that asynchronous boundary.
|
|
344
|
+
if (this.active !== current) return;
|
|
345
|
+
const receipt = this.receipt();
|
|
346
|
+
if (!receipt.settled) { await this.persist({ active_tree: true }); return; }
|
|
347
|
+
this.active = null;
|
|
348
|
+
current.resolve(receipt);
|
|
349
|
+
}
|
|
350
|
+
receipt(result = this.lastResult) {
|
|
351
|
+
const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
|
|
352
|
+
const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
|
|
353
|
+
const treeSettled = [...this.descendants.values()].every(child => child.tree_settled === true || ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
|
|
354
|
+
// A terminal root result is the provider's authoritative end-of-turn
|
|
355
|
+
// observation when AGY does not emit a root Stop hook. A received
|
|
356
|
+
// non-idle Stop still wins, and every discovered child must be terminal.
|
|
357
|
+
const rootSettled = observation?.stop?.fullyIdle !== false;
|
|
358
|
+
const terminalPending = this.active?.terminalResult === result;
|
|
359
|
+
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
360
|
+
}
|
|
361
|
+
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
362
|
+
async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null) {
|
|
363
|
+
if (!text) throw new DaemonError("prompt_required", "prompt is required");
|
|
364
|
+
await this.start(); await this.draining;
|
|
365
|
+
if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running");
|
|
366
|
+
if (this.state.last_hook_rejection && !this.receipt().settled) throw new DaemonError("tree_not_settled", "Rejected hook does not prove that the prior Antigravity tree stopped", false, { primary_error: this.state.last_hook_rejection });
|
|
367
|
+
if (this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
|
|
368
|
+
let resolveTurn, rejectTurn;
|
|
369
|
+
const reply = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject; });
|
|
370
|
+
// Cancellation can reject while admission persistence is still pending.
|
|
371
|
+
void reply.catch(() => {});
|
|
372
|
+
const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, previousError: this.lastResult?.error ?? null };
|
|
373
|
+
// Reserve before persistence so different operation IDs cannot overlap.
|
|
374
|
+
this.active = current;
|
|
375
|
+
this.hookRequests = new Map();
|
|
376
|
+
const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
|
|
377
|
+
this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
|
|
378
|
+
this.lastProviderEvent = null;
|
|
379
|
+
try {
|
|
380
|
+
await this.persist({ active_tree: true, last_hook_rejection: null });
|
|
381
|
+
assertDispatch();
|
|
382
|
+
if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
|
|
383
|
+
this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
|
|
384
|
+
if (error) { if (this.active === current) this.active = null; current.reject(error); }
|
|
385
|
+
});
|
|
386
|
+
} catch (error) { if (this.active === current) this.active = null; current.reject(error); }
|
|
387
|
+
return await reply;
|
|
388
|
+
}
|
|
389
|
+
childActivity(sessionId) {
|
|
390
|
+
if (sessionId === this.init?.conversation_id && !this.active) this.state.unclaimed_activity = true;
|
|
391
|
+
const child = this.descendants.get(sessionId);
|
|
392
|
+
if (!child) return;
|
|
393
|
+
this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, settlement_daemon_id: null, activity_generation: this.turnGeneration });
|
|
394
|
+
}
|
|
395
|
+
async childForHook(conversationId) {
|
|
396
|
+
if (conversationId === this.init?.conversation_id) return null;
|
|
397
|
+
const deadline = Date.now() + 1_000;
|
|
398
|
+
while (!this.descendants.has(conversationId) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 10));
|
|
399
|
+
const child = this.descendants.get(conversationId);
|
|
400
|
+
if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
|
|
401
|
+
if (child.parent_provider_session_id !== this.init?.conversation_id) throw new DaemonError("agy_child_parent_unqualified", "Nested Antigravity worker is not a direct child of the controlled Session", false, { conversation_id: conversationId, parent_conversation_id: child.parent_provider_session_id });
|
|
402
|
+
return child;
|
|
403
|
+
}
|
|
404
|
+
async rejectHook(error, event, payload, current = this.active) {
|
|
405
|
+
if (current && this.active !== current) return;
|
|
406
|
+
const rejection = this.state.last_hook_rejection && this.state.last_hook_rejection.turn_generation === current?.generation ? this.state.last_hook_rejection : { code: error.code ?? "agy_hook_rejected", message: error.message ?? String(error), details: error.details, event, conversation_id: payload.conversationId ?? null, tool: payload.toolCall?.name ?? null, daemon_id: this.config.daemonId, operation_id: current?.operationId ?? null, turn_generation: current?.generation ?? null };
|
|
407
|
+
this.state.last_hook_rejection = rejection;
|
|
408
|
+
if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
|
|
409
|
+
try { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }
|
|
410
|
+
catch (storageError) { rejection.receipt_error = { code: storageError.code, message: storageError.message }; }
|
|
411
|
+
}
|
|
412
|
+
async observeHookFailures() {
|
|
413
|
+
for (const [requestId, binding] of this.hookRequests ?? []) {
|
|
414
|
+
if (binding.current !== this.active || !this.active) continue;
|
|
415
|
+
let failure;
|
|
416
|
+
try { failure = JSON.parse(await readFile(path.join(this.paths.dir, "hook-failures", `${requestId}.json`), "utf8")); }
|
|
417
|
+
catch (error) { if (error.code === "ENOENT") continue; failure = { code: "native_hook_receipt_invalid", message: error.message }; }
|
|
418
|
+
if (failure.request_id !== undefined && failure.request_id !== requestId) continue;
|
|
419
|
+
await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async observeHook(event, payload, budget = null) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); budget?.remaining("identity"); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}), ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const parent = child?.parent_provider_session_id ?? null;
|
|
423
|
+
const observed = payload.model ? observedProfile(payload) : {};
|
|
424
|
+
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: "native.hook", evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
425
|
+
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null, persistence_timing: this.persistTiming ?? null }); await this.completePendingTurn(); return { event_id: eventId, daemon_id: this.config.daemonId, launch_cwd: this.config.cwd, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
426
|
+
}
|
|
427
|
+
async finalizeProviderForControl() {
|
|
428
|
+
const pending = this.providerFinalization;
|
|
429
|
+
try { await pending; }
|
|
430
|
+
catch (error) {
|
|
431
|
+
// A leaderless group may have exited naturally since the last check.
|
|
432
|
+
// Only control may recheck it; startup never retries failed cleanup.
|
|
433
|
+
if (this.providerFinalization !== pending) return await this.providerFinalization;
|
|
434
|
+
if (error.code !== "process_group_ownership_unknown" || !this.retryProviderFinalization) throw error;
|
|
435
|
+
this.providerFinalization = this.retryProviderFinalization(5_000);
|
|
436
|
+
await this.providerFinalization;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async cancel() { cancelPendingDispatch(this); if (!this.child) return { ...this.receipt(), settled: Boolean(this.exited) }; if (!this.exited) { try { process.kill(-this.child.pid, "SIGINT"); } catch (error) { if (error.code !== "ESRCH") throw error; } const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await stopProcessGroup(this.child, 5_000); } await this.finalizeProviderForControl(); await this.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
|
|
440
|
+
}
|
|
441
|
+
async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled))) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running"); if (cancelTree) await this.cancel(); else if (this.child && !this.exited) this.child.stdin.end(); const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await this.cancel(); await this.providerFinalization; }
|
|
442
|
+
async abortProvider(reason) {
|
|
443
|
+
const child = this.child, providerProcess = this.providerProcess;
|
|
444
|
+
await stopProcessGroup(child);
|
|
445
|
+
await this.finalizeProviderForControl();
|
|
446
|
+
await finishDaemonProcess(this.config, providerProcess, "failed", reason);
|
|
447
|
+
}
|
|
448
|
+
async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function serveDaemon(stateDir) {
|
|
452
|
+
const paths = locations(stateDir), state = await readState(paths.dir); if (!state?.config) throw new DaemonError("daemon_state_missing", "Antigravity daemon state is missing");
|
|
453
|
+
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
454
|
+
const runtime = new Runtime(paths, state); await removeSocket(paths); const server = net.createServer(socket => { let buffer = ""; socket.on("error", () => {}); socket.setEncoding("utf8"); socket.on("data", chunk => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; let request; try { request = JSON.parse(buffer.slice(0, index)); } catch (error) { socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, ok: false, error: errorPayload(error) })}\n`); return; } void durableDaemonDispatch(paths.dir, request, () => dispatch(runtime, request), () => dispatch(runtime, { operation: "session.inspect", params: request.params ?? {} })).then(result => socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result })}\n`), error => socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: false, error: errorPayload(error) })}\n`)); }); }); runtime.server = server;
|
|
455
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600);
|
|
456
|
+
try { await runtime.start(state.config.resumeSessionId); runtime.requireSessionIdentity(state.config.resumeSessionId); }
|
|
457
|
+
catch (error) {
|
|
458
|
+
await runtime.abortProvider("daemon_start_failed");
|
|
459
|
+
await runtime.persist({ shutdown_state: "failed", startup_error: errorPayload(error), active_tree: false }); await runtime.finishResource("failed", "daemon_start_failed");
|
|
460
|
+
await new Promise(resolve => server.close(resolve));
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
await new Promise(resolve => server.once("close", resolve));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function dispatch(runtime, request) {
|
|
467
|
+
if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
|
|
468
|
+
if (request.operation === "daemon.status") { await runtime.observeHookFailures(); return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, active_operation: runtime.active?.operationId ?? null, last_hook_rejection: runtime.state.last_hook_rejection ?? null, last_activity_at: runtime.lastActivityAt, provider_ready: Boolean(runtime.init && !runtime.exited), pid: process.pid, provider_pid: runtime.child?.pid ?? null, versions: runtime.state.versions, config: runtime.config, sessions: runtime.state.sessions ?? [], receipt: runtime.receipt() }; }
|
|
469
|
+
if (request.operation === "session.create") { if (params.sessionId) throw new DaemonError("invalid_create", "create does not accept a Session ID"); if (params.prompt) throw new DaemonError("invalid_create", "create does not execute a prompt; use session.prompt"); await runtime.start(); await runtime.zeroUsage(); return runtime.receipt(); }
|
|
470
|
+
if (request.operation === "session.prompt") { const assertDispatch = captureDispatchGuard(runtime); await runtime.start(params.sessionId ?? null); runtime.requireSessionIdentity(params.sessionId); return await runtime.prompt(params.prompt, assertDispatch, request.id); }
|
|
471
|
+
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
472
|
+
if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
473
|
+
if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
|
|
474
|
+
if (request.operation === "hook.observe") {
|
|
475
|
+
// Check the process binding before correlating a request with an active
|
|
476
|
+
// turn. A stale/foreign daemon must not poison an unrelated operation.
|
|
477
|
+
if (runtime.config.hookBinding === "environment-v1" && params.daemonId !== runtime.config.daemonId) throw new DaemonError("agy_hook_binding_invalid", "Antigravity hook binding does not name this daemon", false, { expected_daemon_id: runtime.config.daemonId, observed_daemon_id: params.daemonId ?? null });
|
|
478
|
+
const current = runtime.active, budget = hookBudget(25_000, params.deadlineAt);
|
|
479
|
+
runtime.hookRequests ??= new Map();
|
|
480
|
+
if (/^[A-Za-z0-9-]{1,128}$/.test(request.id)) runtime.hookRequests.set(request.id, { current, event: params.event, payload: { conversationId: params.payload?.conversationId, toolCall: { name: params.payload?.toolCall?.name } } });
|
|
481
|
+
try {
|
|
482
|
+
const result = await budget.run(async () => {
|
|
483
|
+
if (runtime.hookRequests.has(request.id) && current?.operationId) {
|
|
484
|
+
const directory = path.join(runtime.paths.dir, "operations", createHash("sha256").update(current.operationId).digest("hex"), "hooks");
|
|
485
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
486
|
+
await writeJson(path.join(directory, `${request.id}.json`), { schema_id: "dd-flow/hook-request@1", request_id: request.id, daemon_id: runtime.config.daemonId, operation_id: current.operationId, turn_generation: current.generation, event: params.event, conversation_id: params.payload?.conversationId ?? null, tool: params.payload?.toolCall?.name ?? null });
|
|
487
|
+
}
|
|
488
|
+
budget.remaining("observation");
|
|
489
|
+
return await runtime.observeHook(params.event, params.payload ?? {}, budget);
|
|
490
|
+
}, "observation");
|
|
491
|
+
await runtime.observeHookFailures(); budget.remaining("observation");
|
|
492
|
+
if (current && runtime.state.last_hook_rejection?.turn_generation === current.generation) throw Object.assign(new Error(runtime.state.last_hook_rejection.message), runtime.state.last_hook_rejection);
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}, current); throw error; }
|
|
496
|
+
}
|
|
497
|
+
if (request.operation === "daemon.stop") { await runtime.close(params.cancelTree === true); await runtime.persist({ shutdown_state: "clean", active_tree: false }); await runtime.finishResource(); setImmediate(() => runtime.server?.close()); return { stopped: true, clean: true, settled: true, daemon_id: runtime.config.daemonId }; }
|
|
498
|
+
throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
|
|
499
|
+
}
|
|
500
|
+
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { checkObservedProfile } from "./model-observations.mjs";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MODEL = "gemini-3.1-pro-high";
|
|
9
|
+
|
|
10
|
+
export class AgyError extends Error {
|
|
11
|
+
constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function executable(bin, args = []) {
|
|
15
|
+
return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runAgy(bin, args, options = {}) {
|
|
19
|
+
const target = executable(bin, args);
|
|
20
|
+
return await new Promise((resolve, reject) => {
|
|
21
|
+
const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
22
|
+
let stdout = "", stderr = "", settled = false;
|
|
23
|
+
const timer = setTimeout(() => { child.kill("SIGTERM"); finish(new AgyError("agy_timeout", `${args[0] ?? "agy"} timed out`, true)); }, options.timeoutMs ?? 30_000);
|
|
24
|
+
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
|
|
25
|
+
child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk);
|
|
26
|
+
child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
|
|
27
|
+
child.on("error", error => finish(error));
|
|
28
|
+
child.on("close", code => code === 0 ? finish(null, { stdout, stderr, code }) : finish(new AgyError("agy_cli_failed", stderr.trim() || stdout.trim() || `agy exited ${code}`, false, { exit_code: code })));
|
|
29
|
+
child.stdin.end(options.stdin ?? "");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function usageSnapshot(result = {}, toolCalls = {}) {
|
|
34
|
+
const usage = result.usage && typeof result.usage === "object" ? result.usage : {};
|
|
35
|
+
const numberOrNull = value => value == null || !Number.isFinite(Number(value)) ? null : Number(value);
|
|
36
|
+
return {
|
|
37
|
+
// A missing counter is unknown, not a measured zero.
|
|
38
|
+
input_tokens: numberOrNull(usage.input_tokens), output_tokens: numberOrNull(usage.output_tokens),
|
|
39
|
+
reasoning_tokens: numberOrNull(usage.thinking_tokens), cache_read_tokens: numberOrNull(usage.cache_read_tokens),
|
|
40
|
+
total_tokens: numberOrNull(usage.total_tokens), tool_calls: toolCalls
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function observedProfile(init = {}, requested = {}) {
|
|
45
|
+
return { provider: init.provider ?? null, model: init.model ?? null, reasoning: init.reasoning ?? null, mode: init.mode ?? null, permission_mode: init.permission_mode ?? null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function assertProfile(requested, observed) { return checkObservedProfile(requested, observed, { required: requested.permission_mode ? ["permission_mode"] : [] }); }
|
|
49
|
+
|
|
50
|
+
export async function doctor(options = {}) {
|
|
51
|
+
const bin = options.bin ?? process.env.DD_AGY_BIN ?? "agy", temporary = await mkdtemp(path.join(os.tmpdir(), "dd-agy-doctor-"));
|
|
52
|
+
try {
|
|
53
|
+
const version = (await runAgy(bin, ["--version"], { timeoutMs: 10_000 })).stdout.trim();
|
|
54
|
+
const models = await runAgy(bin, [`--gemini_dir=${temporary}`, "--app_data_dir=runtime", "models"], { timeoutMs: options.timeoutMs ?? 30_000 });
|
|
55
|
+
const modelIds = models.stdout.split(/\r?\n/).map(line => line.split(/\s+/)[0]).filter(Boolean);
|
|
56
|
+
if (!modelIds.includes(options.model ?? DEFAULT_MODEL)) throw new AgyError("agy_model_unavailable", `required Antigravity model is unavailable: ${options.model ?? DEFAULT_MODEL}`);
|
|
57
|
+
return { compatible: true, observed_runtime: { agy: version, dd_harness_contract: "dd-agy-harness@1" }, isolation: { gemini_dir: "verified", app_data_dir: "verified" }, auth: "available", models: modelIds };
|
|
58
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;
|