jeo-code 0.12.1 → 0.12.3

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.
@@ -54,6 +54,10 @@ import chalk from "chalk";
54
54
  import { callLlm, type Message } from "../agent/loop";
55
55
  import { friendlyProviderError } from "../util/provider-error";
56
56
  import { readGlobalConfig, saveConfigPatch, resolveWikiRoot } from "../agent/state";
57
+ import { AgyCliError, type AgyTurnResult } from "../agent/antigravity-cli";
58
+ import { runPreToolHooks, runPostTurnHooks } from "../agent/hooks";
59
+ import { runAgySessionTurn } from "./launch/agy-turn";
60
+ import { listProviderModels } from "../ai/model-discovery";
57
61
  import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
58
62
  import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, liveProviderCatalogModels, resolveRoleModel, CODEX_MODELS, qualifyModelId, modelServableWithConfig, isLocalProviderReachable } from "../ai";
59
63
  import { rehydrateLiveModels, writeModelCache } from "../ai/model-cache";
@@ -350,7 +354,7 @@ export function providerPickEntries(live: ProviderModelsResult[], want: Provider
350
354
  if (fromLive.length) return fromLive;
351
355
  // An OpenAI or Antigravity empty/error response is authoritative for this picker.
352
356
  // Offering the static capability catalog here would fabricate account availability.
353
- if (want === "openai" || want === "antigravity") return [];
357
+ if (want === "openai" || want === "antigravity" || want === "agy") return [];
354
358
  const catalog = catalogByProvider(want);
355
359
  if (catalog.length) {
356
360
  return catalog.map((m, i) => ({ index: i + 1, provider: want, model: qualifyModelId(m.providerModel, want) }));
@@ -420,9 +424,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
420
424
  } catch {}
421
425
  const cfg = await readGlobalConfig();
422
426
  const defaultModel = cfg.defaultModel;
423
- const initialSessionModel =
427
+ let initialSessionModel =
424
428
  flags.model ??
425
429
  (flags.modelRole ? resolveRoleModel(flags.modelRole, cfg) : flags.provider ? providerDefaultModel(flags.provider) : undefined);
430
+ if (flags.provider === "agy" && !flags.model && !flags.modelRole) {
431
+ const available = await listProviderModels("agy", { config: cfg });
432
+ if (!available.ok || !available.models.length) {
433
+ console.error(sanitizeForTerminal(`error: ${available.error ?? "the official agy CLI returned no available models"}`));
434
+ process.exitCode = 1;
435
+ return;
436
+ }
437
+ initialSessionModel = available.models[0];
438
+ }
426
439
  if (flags.provider && initialSessionModel) {
427
440
  const { provider } = await describeModel(initialSessionModel);
428
441
  if (provider !== flags.provider) {
@@ -955,6 +968,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
955
968
  const subagentRegistry = new SubagentRegistry();
956
969
  const jobRegistry = new JobRegistry();
957
970
  let activeTui: LaunchTui | null = null;
971
+ let activeAgyController: AbortController | undefined;
972
+ let activeAgyTurn: Promise<AgyTurnResult> | undefined;
958
973
  const monitorRegistry = new MonitorRegistry({
959
974
  onLine: (record, line) => {
960
975
  const notice = `[monitor ${record.id}] ${line}`;
@@ -968,6 +983,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
968
983
  const cleanupSession = async (): Promise<void> => {
969
984
  if (cleanedUp) return;
970
985
  cleanedUp = true;
986
+ activeAgyController?.abort();
987
+ await activeAgyTurn?.catch(() => undefined);
971
988
  subagentRegistry.cancelAll();
972
989
  jobRegistry.cancelAll();
973
990
  monitorRegistry.cancelAll();
@@ -1048,6 +1065,96 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1048
1065
  // gjc-style); a string → show that compact label instead of the raw input.
1049
1066
  opts?: { userCard?: string | null },
1050
1067
  ): Promise<{ done: boolean; steps: number; reply: string; rendered: boolean; usage: string }> => {
1068
+ const selectedModel = sessionModel || defaultModel;
1069
+ if (selectedModel.startsWith("agy/")) {
1070
+ const oneShot = !!flags.print || flags.message.length > 0 || !process.stdin.isTTY;
1071
+ if (flags.noTools || flags.tools || flags.maxSteps > 0 || flags.systemPrompt || flags.appendSystemPrompt || flags.thinking) {
1072
+ if (oneShot) process.exitCode = 1;
1073
+ throw new AgyCliError("The official agy agent owns its tools, system prompt, step budget, and reasoning settings. Remove jeo's raw-model tool/system/step/thinking flags and select an exact agy model slug instead.", "protocol", 1);
1074
+ }
1075
+ const cliTui = useTui ? new LaunchTui({ model: selectedModel, provider: "agy", sessionId, maxSteps: 0, cwd, branch }) : null;
1076
+ const notice = (message: string): void => {
1077
+ const safe = sanitizeForTerminal(message);
1078
+ if (cliTui) cliTui.events().onNotice?.(safe);
1079
+ else console.error(safe);
1080
+ };
1081
+ let hardExit = false;
1082
+ const harness = createInFlightAbortHarness({
1083
+ captureEsc: !!cliTui,
1084
+ onAbortNotice: notice,
1085
+ onNoise: () => cliTui?.repaint(),
1086
+ pasteActive: () => queueBusyPasteActive?.() ?? false,
1087
+ onBufferedInput: chunk => {
1088
+ if (queueBusyInput?.(chunk) && jeoEnv("NO_LIVE_DRAFT") !== "1") {
1089
+ cliTui?.setLivePromptInput(expandSentinel(queueBusySnapshot?.().text ?? ""));
1090
+ }
1091
+ },
1092
+ onHardExit: () => { hardExit = true; },
1093
+ });
1094
+ activeAgyController = harness.controller;
1095
+ activeTui = cliTui;
1096
+ interactiveTurnActive = !!cliTui;
1097
+ lastUserInput = userInput;
1098
+ lastTurnReasoning = "";
1099
+ lastTurnArtifacts = [];
1100
+ try {
1101
+ cliTui?.start();
1102
+ cliTui?.setTurnTitle(userInput);
1103
+ const card = opts?.userCard === undefined ? userInput : opts.userCard;
1104
+ if (card?.trim()) cliTui?.flushUserCard(card);
1105
+ notice("Official agy agent: uses its own tools, sandbox, and permissions; jeo approvals do not mediate its inner tool calls.");
1106
+ const pre = await runPreToolHooks(cwd, "agy", { model: selectedModel, prompt: userInput }, harness.controller.signal, notice);
1107
+ if (pre.vetoed) throw new AgyCliError(pre.error ?? "Official CLI delegation blocked by a jeo hook", "upstream", 1);
1108
+ sessionNotifyEndpoint?.sendContextUpdate("turn_start", notifyRedact ? "[redacted]" : userInput.slice(0, 200), { model: selectedModel });
1109
+ let streamed = "";
1110
+ activeAgyTurn = runAgySessionTurn({
1111
+ model: selectedModel, prompt: userInput, cwd, history, sessionId, images,
1112
+ signal: harness.controller.signal,
1113
+ mode: flags.modelRole === "plan" ? "plan" : undefined,
1114
+ onNotice: notice,
1115
+ onText: cliTui ? text => {
1116
+ streamed += sanitizeForTerminal(text);
1117
+ cliTui.events().onModelStream?.(streamed);
1118
+ } : undefined,
1119
+ });
1120
+ const result = await activeAgyTurn;
1121
+ const post = await runPostTurnHooks(cwd, "agy", { model: selectedModel, prompt: userInput }, true, result.response, harness.controller.signal, notice);
1122
+ if (post.diags.length) throw new AgyCliError(`Official CLI completed, but a jeo post-turn hook failed: ${post.diags.map(diag => diag.output).join("\n")}`, "upstream", 1);
1123
+ const reply = sanitizeForTerminal(result.response);
1124
+ lastReply = result.response;
1125
+ lastActiveModel = selectedModel;
1126
+ if (result.usage) {
1127
+ sessionUsage.inputTokens += result.usage.inputTokens;
1128
+ sessionUsage.outputTokens += result.usage.outputTokens;
1129
+ cliTui?.events().onUsage?.(result.usage);
1130
+ }
1131
+ sessionUsage.turns++;
1132
+ const usage = result.usage ? `\n${formatUsage(result.usage)}` : "";
1133
+ sessionNotifyEndpoint?.sendContextUpdate("turn_end", notifyRedact ? "[redacted]" : reply.slice(0, 200), { model: selectedModel, usage });
1134
+ sessionNotifyEndpoint?.sendTurnStream(notifyRedact ? "[redacted]" : reply);
1135
+ cliTui?.events().onStep?.(result.steps);
1136
+ cliTui?.finish(reply);
1137
+ return { done: true, steps: result.steps, reply, rendered: !!cliTui, usage };
1138
+ } catch (error) {
1139
+ if (oneShot) {
1140
+ process.exitCode = error instanceof AgyCliError
1141
+ ? (error.kind === "cancelled" ? 130 : Math.max(1, error.exitCode ?? 1))
1142
+ : 1;
1143
+ }
1144
+ cliTui?.finish(`! ${friendlyProviderError(error)}`, { ok: false });
1145
+ throw error;
1146
+ } finally {
1147
+ harness.dispose();
1148
+ activeAgyTurn = undefined;
1149
+ activeAgyController = undefined;
1150
+ activeTui = null;
1151
+ interactiveTurnActive = false;
1152
+ if (hardExit) {
1153
+ process.exitCode = 130;
1154
+ if (!oneShot) forceExitFromCtrlC();
1155
+ }
1156
+ }
1157
+ }
1051
1158
  const turnConfig = await readGlobalConfig();
1052
1159
  // Fall back to the model resolved at THIS session's start (frozen snapshot), NOT
1053
1160
  // the live global default. A concurrent `jeo` session running `/model` persists the
@@ -1552,7 +1659,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1552
1659
  const fullTools = {
1553
1660
  ...DEFAULT_TOOLS,
1554
1661
  task: createTaskTool({
1555
- config: { ...turnConfig, defaultModel: activeModel },
1662
+ // Subagents inherit the parent's EFFECTIVE routing state (`/route on|off`
1663
+ // override ?? config) — task-tool.ts gates its failover reroute on it.
1664
+ config: { ...turnConfig, defaultModel: activeModel, routing: { ...turnConfig.routing, enabled: routingEnabled } },
1665
+
1556
1666
  signal: ac.signal,
1557
1667
  steer: drainSteer,
1558
1668
  registry: subagentRegistry,
@@ -1603,12 +1713,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1603
1713
  // mutated in place (`.add`) as the fallback loop below tries more models — this
1604
1714
  // closure always reads its live contents, never a stale snapshot.
1605
1715
  const attemptedRouteModels = new Set<string>([activeModel]);
1606
- // A NON-routed turn (routing disabled, or the user pinned a model via /model)
1607
- // still needs the equivalent-pool fallback on a usage-limit/rate-limit/credential
1608
- // failure — the exact "5-hour usage window exhausted, model just stops working"
1609
- // case this loop exists to fix. Synthesize a same-size-class RouteDecision
1610
- // (inferTierForModel) so `equivalentRouteFallback` has a tier/pool to search even
1611
- // with routing off; a real `routed` decision (routing enabled) is used unchanged.
1716
+ // The synthesized decision below is only the BASE for `equivalentRouteFallback`'s
1717
+ // tier/pool search when the pre-call router produced no decision (routing on but
1718
+ // fail-open, or a /model pin with `/route on`). It is inert when routing is off:
1719
+ // the reroute loop and the engine's bail predicates are both gated on
1720
+ // `routingEnabled`, so `/route off` means a FIXED model with no failover.
1721
+ // (`inferTierForModel` gives the same-size-class tier/pool to search.)
1722
+
1612
1723
  let fallbackBaseDecision: RouteDecision = routed ?? {
1613
1724
  model: activeModel,
1614
1725
  thinking: activeThinking,
@@ -1678,8 +1789,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1678
1789
  signal: ac.signal,
1679
1790
  sessionKey: cacheKey,
1680
1791
  steer: drainSteer,
1681
- rateLimitFallbackAvailable,
1682
- safetyFallbackAvailable,
1792
+ // Withheld when routing is off: the bail predicates only make sense paired
1793
+ // with the reroute loop below, which `routingEnabled` also gates.
1794
+ ...(routingEnabled ? { rateLimitFallbackAvailable, safetyFallbackAvailable } : {}),
1795
+
1683
1796
  events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
1684
1797
  });
1685
1798
  const routeFailureReason = (doneReason: unknown): string | null => {
@@ -1759,7 +1872,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1759
1872
  const routeFallbackMax = Number.isFinite(routeFallbackMaxRaw) && routeFallbackMaxRaw >= 0
1760
1873
  ? Math.floor(routeFallbackMaxRaw)
1761
1874
  : 3;
1762
- for (let routeFallbackAttempt = 0; routeFallbackAttempt < routeFallbackMax; routeFallbackAttempt++) {
1875
+ for (let routeFallbackAttempt = 0; routingEnabled && routeFallbackAttempt < routeFallbackMax; routeFallbackAttempt++) {
1876
+
1763
1877
  // A loop guard is a local safety boundary, not a provider failure. A
1764
1878
  // fresh loop on an equivalent model loses the guard state and could
1765
1879
  // re-execute a previously skipped mutation.
@@ -1855,10 +1969,20 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1855
1969
  }
1856
1970
  // A completed turn with an empty done-reason must NOT masquerade as a step-limit
1857
1971
  // failure ("reached the 3-step limit" after the model called done at step 3).
1858
- const reply = stripSafetyFallbackTag(result.doneReason ?? "")
1972
+ const baseReply = stripSafetyFallbackTag(result.doneReason ?? "")
1859
1973
  || (result.done
1860
1974
  ? `(done in ${result.steps} step${result.steps === 1 ? "" : "s"} — the model returned no summary)`
1861
1975
  : `(reached the ${result.steps}-step limit without signaling done)`);
1976
+ // Routing off = the model is fixed and nothing fails over. When the turn died on
1977
+ // an account-level limit, say so and name the one switch that re-enables failover.
1978
+ const failoverHint = !routingEnabled && !result.done && (() => {
1979
+ const failure = new Error(result.doneReason ?? "");
1980
+ return isRateLimitError(failure) || isUsageLimitError(failure);
1981
+ })()
1982
+ ? "\n(routing is off — the model is fixed and no equivalent-model failover was attempted. Run '/route on' to allow failover.)"
1983
+ : "";
1984
+ const reply = `${baseReply}${failoverHint}`;
1985
+
1862
1986
  if (sessionNotifyEndpoint) {
1863
1987
  const usageStr = result.usage ? formatUsage(result.usage) : undefined;
1864
1988
  if (notifyRedact) {
@@ -1965,7 +2089,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1965
2089
  // > 0) and stays silent (no stdout noise) since one-shot output may be
1966
2090
  // piped/parsed by a script.
1967
2091
  const maybeDistillOneShot = async (): Promise<void> => {
1968
- if (sessionUsage.turns > 0) {
2092
+ if (sessionUsage.turns > 0 && !(sessionModel || defaultModel).startsWith("agy/")) {
1969
2093
  await spawnDetachedDistill(history, cwd, sessionModel || defaultModel);
1970
2094
  }
1971
2095
  };
@@ -5152,7 +5276,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
5152
5276
  // hermes-style experience distill (plan/gjc-inheritance.md B6) — now handed to a
5153
5277
  // DETACHED child (round-16): /exit and ^C^C return the shell IMMEDIATELY instead
5154
5278
  // of blocking up to 20s on a final LLM call. The child writes MEMORY.md atomically.
5155
- if (sessionUsage.turns > 0) {
5279
+ if (sessionUsage.turns > 0 && !(sessionModel || defaultModel).startsWith("agy/")) {
5156
5280
  const spawned = await spawnDetachedDistill(history, cwd, sessionModel || defaultModel);
5157
5281
  if (spawned) console.log(chalk.gray("(session memory distilling in background)"));
5158
5282
  }
@@ -7,6 +7,8 @@
7
7
  import { isUsageLimitError, isRefusalError, isConnectionError, ConnectionContextError } from "./retry";
8
8
  import { companyLabel } from "../ai/model-catalog";
9
9
  import { ProviderHttpError, ProviderStreamError } from "../ai/providers/errors";
10
+ import { AgyCliError } from "../agent/antigravity-cli";
11
+ import { sanitizeForTerminal } from "../tui/components/code-view";
10
12
  // Re-export: engine.ts and callers import the refusal predicate from here; the
11
13
  // definition lives in retry.ts so defaultRetryable can fail fast without a cycle.
12
14
  export { isRefusalError } from "./retry";
@@ -71,6 +73,7 @@ function retryAfterMsOf(err: unknown): number | undefined {
71
73
  }
72
74
 
73
75
  export function friendlyProviderError(err: unknown): string {
76
+ if (err instanceof AgyCliError) return sanitizeForTerminal(err.message);
74
77
  if (err instanceof ConnectionContextError) {
75
78
  const provider = companyLabel(err.provider);
76
79
  const target = err.baseUrl ? ` at ${err.baseUrl}` : "";