@kinetica/admin-agent 0.1.2 → 0.1.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.
package/README.md CHANGED
@@ -161,14 +161,15 @@ wasting ~28.7 MB as raw storage. Both issues have been remediated.
161
161
 
162
162
  Set environment variables or use a `.env` file. The agent loads `.env` automatically at startup (shell-set variables always take precedence). Any missing values are prompted interactively.
163
163
 
164
- | Variable | Description | Required |
165
- | --------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------- |
166
- | `ANTHROPIC_API_KEY` | Anthropic API key for Claude | No — OAuth login used if unset |
167
- | `KINETICA_URL` | Kinetica instance URL (e.g. `http://host:9191` or bare `host:9191`) | Prompted if unset |
168
- | `KINETICA_USER` | Kinetica username | Prompted if unset |
169
- | `KINETICA_PASS` | Kinetica password | Prompted if unset (masked, never saved to .env) |
170
- | `KINETICA_HTTPS_ONLY` | Set to `1` to refuse plaintext HTTP fallback entirely strict mode for production clusters | No |
171
- | `DEBUG` | Set to `1` to log HTTP requests and the assembled system-prompt token size to stderr | No |
164
+ | Variable | Description | Required |
165
+ | ------------------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
166
+ | `ANTHROPIC_API_KEY` | Anthropic API key for Claude | No — OAuth login used if unset |
167
+ | `ADMIN_AGENT_MAX_BUDGET` | Per-session budget cap in USD for API-key billing (overridden by `--max-budget`; default `5.00`) | No |
168
+ | `KINETICA_URL` | Kinetica instance URL (e.g. `http://host:9191` or bare `host:9191`) | Prompted if unset |
169
+ | `KINETICA_USER` | Kinetica username | Prompted if unset |
170
+ | `KINETICA_PASS` | Kinetica password | Prompted if unset (masked, never saved to .env) |
171
+ | `KINETICA_HTTPS_ONLY` | Set to `1` to refuse plaintext HTTP fallback entirely strict mode for production clusters | No |
172
+ | `DEBUG` | Set to `1` to log HTTP requests and the assembled system-prompt token size to stderr | No |
172
173
 
173
174
  ```bash
174
175
  cp .env.example .env # fill in values — or let the agent create it for you
@@ -208,7 +209,12 @@ npm run dev -- --logout
208
209
 
209
210
  ### Session Budget
210
211
 
211
- Each session enforces a **$5.00 maximum API cost**. The agent reports actual spend in the session summary on exit.
212
+ Each session has a **budget guard** to prevent runaway spend. Its form depends on how you authenticate with Anthropic:
213
+
214
+ - **API-key billing** — the session enforces a dollar cap (default **$5.00**). Raise it with the `--max-budget=<USD>` flag or the `ADMIN_AGENT_MAX_BUDGET` environment variable (the flag wins when both are set). When estimated spend crosses ~80% of the cap, the agent warns on stderr and is instructed to save a partial report and wind down. If the cap is reached, the session ends with a message showing how to re-run with more headroom — and any report saved up to that point remains in `reports/`.
215
+ - **OAuth (Claude Pro/Max subscription)** — no dollar cap is imposed (you are not billed per token). The session is bounded by the **turn limit** (100 turns) instead.
216
+
217
+ The active guard is printed at startup, and the session summary reports per-investigation and total spend (API-key billing only). The dollar cap is enforced precisely by the Claude Agent SDK; the ~80% warning is an estimate from per-turn token usage, so it is approximate by design.
212
218
 
213
219
  ### Degraded Mode
214
220
 
@@ -225,9 +231,12 @@ admin-agent --login-method=TYPE # Login method: claudeai (Pro/Max) or console
225
231
  admin-agent --login-org=UUID # Target organization UUID for OAuth
226
232
  admin-agent --logout # Log out from Anthropic account and exit
227
233
  admin-agent --model=NAME # Override agent model (sonnet | haiku | opus); default: sonnet
234
+ admin-agent --max-budget=USD # Per-session budget cap in USD (API-key billing only); default: 5.00
228
235
  ```
229
236
 
230
- The `--model` flag swaps the primary model for a single session. `haiku` is cheaper and faster for simple triage; `opus` is slower and more expensive but produces deeper reasoning on complex investigations. The fallback model remains `haiku` regardless of the primary choice, so availability is unchanged.
237
+ The `--model` flag swaps the primary model for a single session. `haiku` is cheaper and faster for simple triage; `opus` is slower and more expensive but produces deeper reasoning on complex investigations. The fallback model remains `haiku` regardless of the primary choice, so availability is unchanged. When you omit `--model` in an interactive terminal, the agent shows a startup picker (defaulting to `sonnet`); non-interactive runs use the default without prompting.
238
+
239
+ The `--max-budget` flag sets the per-session dollar cap for API-key billing (see [Session Budget](#session-budget)). It overrides `ADMIN_AGENT_MAX_BUDGET` and has no effect under OAuth subscription billing, which is turn-limited instead.
231
240
 
232
241
  ## Tools
233
242
 
@@ -306,7 +315,7 @@ The agent is designed with defense-in-depth for database administration:
306
315
  - **Two-step approval for batch column alter** — `kinetica_alter_table_columns` requires the operator to select columns via a checklist, then confirm the exact SQL preview
307
316
  - **Audit trail** — every mutation logs a redacted audit line to stderr (EXECUTED/FAILED + fingerprinted input summary) and appears in the report's "Mutations Applied" table with before/after state
308
317
  - **Report scrubbing** — saved reports are scrubbed of URLs, auth headers, Basic/Bearer credentials, cookies, and passwords before writing to disk
309
- - **Budget cap** — $5.00 max API cost per session prevents runaway spend
318
+ - **Budget guard** — a per-session dollar cap (default $5.00, configurable via `--max-budget` or `ADMIN_AGENT_MAX_BUDGET`) prevents runaway spend on API-key billing; OAuth subscription sessions are bounded by the turn limit instead
310
319
 
311
320
  To report a security vulnerability, please see [SECURITY.md](SECURITY.md). Do not open a public GitHub issue for security issues.
312
321
 
@@ -375,7 +384,7 @@ References provide domain knowledge (not diagnostic runbooks). Create a `.md` fi
375
384
  - `sql-create-index` — column index syntax, chunk skip index, when to use which
376
385
  - `version-quirks-7.2` — endpoint/property differences between 7.2.x and earlier releases
377
386
 
378
- > **Heads up — prompt budget:** all playbooks and references are front-loaded into a single system prompt at startup, so its token cost grows with the knowledge corpus. A startup tripwire (`agent/prompt-budget.ts`) prints the assembled prompt size under `DEBUG` and warns on stderr once it exceeds ~15,000 estimated tokens. Current baseline is ~13.4k tokens (6 playbooks + 9 references). If you add substantial knowledge and trip that warning, treat it as the cue to switch from "load everything" to keyword-based playbook selection.
387
+ > **Heads up — prompt budget:** all playbooks and references are front-loaded into a single system prompt at startup, so its token cost grows with the knowledge corpus. A startup tripwire (`agent/prompt-budget.ts`) prints the assembled prompt size under `DEBUG` and warns on stderr once it exceeds ~20,000 estimated tokens. Current baseline is ~13.4k tokens (6 playbooks + 9 references). If you add substantial knowledge and trip that warning, treat it as the cue to switch from "load everything" to keyword-based playbook selection.
379
388
 
380
389
  ## Development
381
390
 
@@ -472,7 +481,7 @@ admin-agent
472
481
 
473
482
  **Agent hits budget cap**
474
483
 
475
- - Default is $5.00 per session. For complex multi-table investigations, consider running focused sessions per table
484
+ - Applies to API-key billing only (default $5.00 per session). Raise it for the next run with `--max-budget=10` or `export ADMIN_AGENT_MAX_BUDGET=10`. The agent warns at ~80% so it can save a partial report before the cap is reached. For complex multi-table investigations, consider running focused sessions per table. OAuth (Pro/Max) sessions are turn-limited rather than dollar-capped.
476
485
 
477
486
  **Empty or missing report**
478
487
 
@@ -111,7 +111,7 @@ function printBanner(model) {
111
111
  const version = getVersion();
112
112
  const subtitle = `admin-agent ${import_picocolors.default.dim(`v${version}`)}`;
113
113
  const header = model ? `${subtitle}
114
- ${import_picocolors.default.dim(`model: ${model}`)}` : subtitle;
114
+ ${import_picocolors.default.dim(`Model: ${model}`)}` : subtitle;
115
115
  process.stderr.write("\n\n" + gradientize(LOGO) + "\n\n" + header + "\n");
116
116
  return subtitle;
117
117
  }
@@ -3550,6 +3550,13 @@ Monitor your context window usage during long investigations:
3550
3550
  - If you detect that context is getting full (many rounds, many large tool responses), warn the operator: "The session context is getting long. Consider starting a fresh session after this report to maintain investigation quality. Your reports are saved to disk."
3551
3551
  - Do NOT continue investigating when context is too full \u2014 write the report with evidence gathered so far.
3552
3552
 
3553
+ ## Budget & Length Awareness
3554
+
3555
+ The session has a per-session budget guard that can end the run before you finish \u2014 and the operator may see a warning that you are "approaching the budget guard". To make sure a diagnostic always survives an early cutoff:
3556
+ - During a long or expensive investigation (many rounds or many large tool responses), proactively call \`save_report\` with \`partial: true\` to checkpoint your findings so far. A partial report is far better than none.
3557
+ - If the operator warns you that the budget guard is approaching, STOP gathering new evidence: immediately save a \`partial: true\` report with the evidence you have, state your best current hypothesis, and wind down the turn.
3558
+ - Treat the guard as a normal limit, not an error \u2014 never apologize for it; just preserve the work.
3559
+
3553
3560
  ---
3554
3561
 
3555
3562
  ## Output Formatting
@@ -3678,6 +3685,69 @@ function checkPromptBudget(prompt, opts) {
3678
3685
  };
3679
3686
  }
3680
3687
 
3688
+ // src/agent/session-budget.ts
3689
+ var DEFAULT_MAX_BUDGET_USD = 5;
3690
+ var DEFAULT_WARN_FRACTION = 0.8;
3691
+ var BUDGET_ENV_VAR = "ADMIN_AGENT_MAX_BUDGET";
3692
+ var MODEL_PRICING = {
3693
+ sonnet: { inputPerMTok: 3, outputPerMTok: 15, cacheReadPerMTok: 0.3, cacheCreationPerMTok: 3.75 },
3694
+ haiku: { inputPerMTok: 1, outputPerMTok: 5, cacheReadPerMTok: 0.1, cacheCreationPerMTok: 1.25 },
3695
+ opus: { inputPerMTok: 15, outputPerMTok: 75, cacheReadPerMTok: 1.5, cacheCreationPerMTok: 18.75 }
3696
+ };
3697
+ function fromSdkUsage(raw) {
3698
+ const u = raw ?? {};
3699
+ return {
3700
+ inputTokens: u.input_tokens,
3701
+ outputTokens: u.output_tokens,
3702
+ cacheReadInputTokens: u.cache_read_input_tokens,
3703
+ cacheCreationInputTokens: u.cache_creation_input_tokens
3704
+ };
3705
+ }
3706
+ function safeCount(value) {
3707
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
3708
+ }
3709
+ function isValidBudget(value) {
3710
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
3711
+ }
3712
+ function estimateTurnCostUsd(usage, model) {
3713
+ if (!usage) return 0;
3714
+ const price = MODEL_PRICING[model];
3715
+ const input5 = safeCount(usage.inputTokens);
3716
+ const output = safeCount(usage.outputTokens);
3717
+ const cacheRead = safeCount(usage.cacheReadInputTokens);
3718
+ const cacheCreation = safeCount(usage.cacheCreationInputTokens);
3719
+ return (input5 * price.inputPerMTok + output * price.outputPerMTok + cacheRead * price.cacheReadPerMTok + cacheCreation * price.cacheCreationPerMTok) / 1e6;
3720
+ }
3721
+ function resolveMaxBudgetUsd(flagValue, env = process.env) {
3722
+ if (isValidBudget(flagValue)) return flagValue;
3723
+ const raw = env[BUDGET_ENV_VAR];
3724
+ if (raw !== void 0 && raw !== "") {
3725
+ const parsed = Number(raw);
3726
+ if (isValidBudget(parsed)) return parsed;
3727
+ }
3728
+ return DEFAULT_MAX_BUDGET_USD;
3729
+ }
3730
+ function createBudgetTracker(opts) {
3731
+ const warnFraction = opts.warnFraction ?? DEFAULT_WARN_FRACTION;
3732
+ const warnAt = opts.maxUsd * warnFraction;
3733
+ let spent = 0;
3734
+ let warned = false;
3735
+ return {
3736
+ add(usage, model) {
3737
+ spent += estimateTurnCostUsd(usage, model);
3738
+ },
3739
+ spentUsd() {
3740
+ return spent;
3741
+ },
3742
+ shouldWarn() {
3743
+ return !warned && spent > warnAt;
3744
+ },
3745
+ markWarned() {
3746
+ warned = true;
3747
+ }
3748
+ };
3749
+ }
3750
+
3681
3751
  // src/report/save-report.ts
3682
3752
  var import_promises3 = require("fs/promises");
3683
3753
  var import_node_path4 = require("path");
@@ -3970,13 +4040,29 @@ function createSpinner() {
3970
4040
 
3971
4041
  // src/agent/run-agent.ts
3972
4042
  var MCP_SERVER_NAME = "kinetica-diagnostics";
4043
+ var SAVE_REPORT_TOOL_NAME = `mcp__${MCP_SERVER_NAME}__save_report`;
4044
+ function contentCallsSaveReport(content) {
4045
+ if (!Array.isArray(content)) return false;
4046
+ return content.some((block) => {
4047
+ if (typeof block !== "object" || block === null) return false;
4048
+ const { type, name } = block;
4049
+ return type === "tool_use" && name === SAVE_REPORT_TOOL_NAME;
4050
+ });
4051
+ }
4052
+ function formatCostSuffix(costUsd) {
4053
+ return costUsd !== void 0 && costUsd > 0 ? ` Cost: $${costUsd.toFixed(4)}.` : "";
4054
+ }
4055
+ function formatMetricsLine(turns, durationMs, durationApiMs, costUsd) {
4056
+ const durationSec = Math.round(durationMs / 1e3);
4057
+ const apiPct = durationMs > 0 ? Math.round(durationApiMs / durationMs * 100) : 0;
4058
+ return `Turns: ${turns}. Duration: ${durationSec}s (${apiPct}% API).${formatCostSuffix(costUsd)}`;
4059
+ }
3973
4060
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "end", "q"]);
3974
4061
  var SUPPORTED_MODELS = ["sonnet", "haiku", "opus"];
3975
4062
  var DEFAULT_AGENT_MODEL = "sonnet";
3976
- var DEFAULT_MAX_BUDGET_USD = 5;
3977
4063
  var ALLOWED_TOOL_NAMES = [
3978
4064
  ...DIAGNOSTIC_TOOL_NAMES.map((name) => `mcp__${MCP_SERVER_NAME}__${name}`),
3979
- `mcp__${MCP_SERVER_NAME}__save_report`,
4065
+ SAVE_REPORT_TOOL_NAME,
3980
4066
  `mcp__${MCP_SERVER_NAME}__${ALTER_TABLE_COLUMNS_TOOL_NAME}`
3981
4067
  ];
3982
4068
  var DISALLOWED_TOOLS = ["Bash", "Edit", "Write", "MultiEdit"];
@@ -4070,7 +4156,10 @@ async function displayDegradedStatus(session2) {
4070
4156
  }
4071
4157
  process.stderr.write("\n");
4072
4158
  }
4073
- async function runAgent(session2, kineticaVersion, degraded, model) {
4159
+ async function runAgent(session2, kineticaVersion, degraded, model, runOptions) {
4160
+ const authMethod = runOptions?.authMethod ?? "api_key";
4161
+ const dollarCapped = authMethod === "api_key";
4162
+ const resolvedBudgetUsd = runOptions?.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD;
4074
4163
  const [catalogSchemas, playbooks, references] = await Promise.all([
4075
4164
  degraded ? Promise.resolve(void 0) : discoverCatalogSchemas(session2),
4076
4165
  loadPlaybooks(),
@@ -4086,7 +4175,7 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4086
4175
  const budget = checkPromptBudget(systemPrompt);
4087
4176
  if (process.env.DEBUG) {
4088
4177
  process.stderr.write(
4089
- import_picocolors8.default.dim(`system prompt: ~${budget.tokens} tokens (${budget.chars} chars)
4178
+ import_picocolors8.default.dim(`System prompt: ~${budget.tokens} tokens (${budget.chars} chars)
4090
4179
  `)
4091
4180
  );
4092
4181
  }
@@ -4126,23 +4215,28 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4126
4215
  fallbackModel: "haiku",
4127
4216
  thinking: { type: "adaptive" },
4128
4217
  maxTurns: 100,
4129
- maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
4218
+ // Only impose a dollar cap for per-token billing. For OAuth subscription users
4219
+ // the SDK would otherwise cut them off at a notional dollar figure they never pay;
4220
+ // omitting it leaves the turn limit (maxTurns) as their guard.
4221
+ ...dollarCapped ? { maxBudgetUsd: resolvedBudgetUsd } : {},
4130
4222
  persistSession: false,
4131
4223
  includePartialMessages: true,
4132
4224
  abortController,
4133
4225
  env: { ...process.env, CLAUDE_AGENT_SDK_CLIENT_APP: "admin-agent" }
4134
4226
  };
4227
+ const guardLine = dollarCapped ? import_picocolors8.default.dim(`Budget guard: $${resolvedBudgetUsd.toFixed(2)} (raise with --max-budget)
4228
+ `) : import_picocolors8.default.dim("Budget guard: subscription (Pro/Max) \u2014 turn-limited\n");
4135
4229
  if (degraded) {
4136
4230
  process.stderr.write("\nKinetica Diagnostic Session Ready (DEGRADED MODE)\n");
4137
4231
  process.stderr.write(
4138
4232
  "DB engine (port 9191) is unreachable. Only host manager tools are available.\n\n"
4139
4233
  );
4140
4234
  await displayDegradedStatus(session2);
4141
- process.stderr.write("Type 'exit' to end the session.\n\n");
4142
4235
  } else {
4143
4236
  process.stderr.write("\nKinetica Diagnostic Session Ready\n");
4144
- process.stderr.write("Type 'exit' to end the session.\n\n");
4145
4237
  }
4238
+ process.stderr.write(guardLine);
4239
+ process.stderr.write("Type 'exit' to end the session.\n\n");
4146
4240
  const turnGate = createTurnGate();
4147
4241
  const agentQuery = (0, import_claude_agent_sdk4.query)({
4148
4242
  prompt: makeInteractivePrompt(abortController, turnGate, spinner),
@@ -4163,6 +4257,9 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4163
4257
  let lastStreamCharWasNewline = true;
4164
4258
  const tableAligner = createStreamingTableAligner();
4165
4259
  let hadNonAbortError = false;
4260
+ let reportSavedThisRun = false;
4261
+ let invBase = { turns: 0, duration: 0, api: 0, cost: 0 };
4262
+ const budgetTracker = dollarCapped ? createBudgetTracker({ maxUsd: resolvedBudgetUsd }) : void 0;
4166
4263
  try {
4167
4264
  for await (const message of agentQuery) {
4168
4265
  if (message.type === "stream_event") {
@@ -4189,6 +4286,23 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4189
4286
  process.stderr.write("\n");
4190
4287
  lastStreamCharWasNewline = true;
4191
4288
  }
4289
+ if (budgetTracker) {
4290
+ budgetTracker.add(fromSdkUsage(assistantMsg.message.usage), effectiveModel);
4291
+ if (budgetTracker.shouldWarn()) {
4292
+ spinner.stop();
4293
+ process.stderr.write(
4294
+ import_picocolors8.default.yellow(
4295
+ `
4296
+ \u26A0 Approaching budget guard (~$${budgetTracker.spentUsd().toFixed(2)} / $${resolvedBudgetUsd.toFixed(2)}) \u2014 wrapping up soon. Save a partial report now if you want to preserve findings.
4297
+ `
4298
+ )
4299
+ );
4300
+ budgetTracker.markWarned();
4301
+ }
4302
+ }
4303
+ if (contentCallsSaveReport(assistantMsg.message.content)) {
4304
+ reportSavedThisRun = true;
4305
+ }
4192
4306
  if (assistantMsg.message.stop_reason === "end_turn") {
4193
4307
  spinner.stop();
4194
4308
  turnGate.open();
@@ -4217,10 +4331,21 @@ API error: ${label}
4217
4331
  cacheCreationTokens = usages.reduce((sum, u) => sum + (u.cacheCreationInputTokens ?? 0), 0);
4218
4332
  if (resultMsg.subtype === "error_max_turns") {
4219
4333
  process.stderr.write(
4220
- "\nInvestigation hit turn limit. Partial report may be available.\n"
4334
+ import_picocolors8.default.yellow(
4335
+ `
4336
+ Reached the turn limit (${numTurns} turns) \u2014 a safety guard, not an error. Any report the agent saved is in reports/. Start a fresh session to continue.
4337
+ `
4338
+ )
4221
4339
  );
4222
4340
  } else if (resultMsg.subtype === "error_max_budget_usd") {
4223
- process.stderr.write("\nBudget limit reached.\n");
4341
+ const spentStr = totalCostUsd > 0 ? ` ($${totalCostUsd.toFixed(2)} spent)` : "";
4342
+ process.stderr.write(
4343
+ import_picocolors8.default.yellow(
4344
+ `
4345
+ Reached the $${resolvedBudgetUsd.toFixed(2)} budget guard${spentStr} \u2014 a safety limit, not an error. Re-run with --max-budget=<amount> (or set ADMIN_AGENT_MAX_BUDGET) for more headroom. Any report the agent saved is in reports/.
4346
+ `
4347
+ )
4348
+ );
4224
4349
  } else if (resultMsg.subtype === "error_during_execution") {
4225
4350
  process.stderr.write(
4226
4351
  "\nExecution error \u2014 the agent encountered an unrecoverable failure.\n"
@@ -4236,6 +4361,24 @@ Agent session ended with error: ${resultMsg.subtype}
4236
4361
  Permission denials: ${denied}
4237
4362
  `);
4238
4363
  }
4364
+ if (reportSavedThisRun) {
4365
+ const line = formatMetricsLine(
4366
+ numTurns - invBase.turns,
4367
+ durationMs - invBase.duration,
4368
+ durationApiMs - invBase.api,
4369
+ dollarCapped ? totalCostUsd - invBase.cost : void 0
4370
+ );
4371
+ process.stderr.write(`
4372
+ Investigation complete \u2014 ${line}
4373
+ `);
4374
+ invBase = {
4375
+ turns: numTurns,
4376
+ duration: durationMs,
4377
+ api: durationApiMs,
4378
+ cost: totalCostUsd
4379
+ };
4380
+ reportSavedThisRun = false;
4381
+ }
4239
4382
  turnGate.open();
4240
4383
  } else if (message.type === "system") {
4241
4384
  const sysMsg = message;
@@ -4307,27 +4450,26 @@ Agent error: ${message}
4307
4450
  process.stderr.write(remaining);
4308
4451
  }
4309
4452
  turnGate.open();
4310
- const durationSec = Math.round(durationMs / 1e3);
4311
- const apiPct = durationMs > 0 ? Math.round(durationApiMs / durationMs * 100) : 0;
4312
- const costStr = totalCostUsd > 0 ? ` Cost: $${totalCostUsd.toFixed(4)}.` : "";
4453
+ const sessionCost = dollarCapped ? totalCostUsd : void 0;
4313
4454
  if (process.env.DEBUG && (cacheReadTokens > 0 || cacheCreationTokens > 0)) {
4314
4455
  process.stderr.write(
4315
4456
  import_picocolors8.default.dim(
4316
- `cache: ${cacheReadTokens} read / ${cacheCreationTokens} created input tokens (read > 0 confirms the system prompt is served from cache)
4457
+ `Cache: ${cacheReadTokens} read / ${cacheCreationTokens} created input tokens (read > 0 confirms the system prompt is served from cache)
4317
4458
  `
4318
4459
  )
4319
4460
  );
4320
4461
  }
4321
4462
  if (hadNonAbortError) {
4322
- process.stderr.write(`
4323
- Session ended due to error. Turns: ${numTurns}.${costStr}
4324
- `);
4325
- } else {
4326
4463
  process.stderr.write(
4327
4464
  `
4328
- Session ended. Turns: ${numTurns}. Duration: ${durationSec}s (${apiPct}% API).${costStr}
4465
+ Session ended due to error. Turns: ${numTurns}.${formatCostSuffix(sessionCost)}
4329
4466
  `
4330
4467
  );
4468
+ } else {
4469
+ const line = formatMetricsLine(numTurns, durationMs, durationApiMs, sessionCost);
4470
+ process.stderr.write(`
4471
+ Session ended. ${line}
4472
+ `);
4331
4473
  }
4332
4474
  }
4333
4475
  }
@@ -4904,12 +5046,14 @@ function printHelp() {
4904
5046
  " --login-org=UUID Target organization UUID for OAuth",
4905
5047
  " --logout Log out from Anthropic account and exit",
4906
5048
  ` --model=NAME Override agent model (${SUPPORTED_MODELS.join(" | ")}); default: sonnet`,
5049
+ " --max-budget=USD Per-session budget cap in USD (API-key billing only); default: 5.00",
4907
5050
  "",
4908
5051
  " Environment variables:",
4909
- " ANTHROPIC_API_KEY Anthropic API key (if not set, OAuth login via browser is used)",
4910
- " KINETICA_URL Kinetica endpoint URL",
4911
- " KINETICA_USER Admin username",
4912
- " KINETICA_PASS Admin password",
5052
+ " ANTHROPIC_API_KEY Anthropic API key (if not set, OAuth login via browser is used)",
5053
+ " ADMIN_AGENT_MAX_BUDGET Per-session budget cap in USD (overridden by --max-budget)",
5054
+ " KINETICA_URL Kinetica endpoint URL",
5055
+ " KINETICA_USER Admin username",
5056
+ " KINETICA_PASS Admin password",
4913
5057
  ""
4914
5058
  ];
4915
5059
  process.stdout.write(lines.join("\n") + "\n");
@@ -4954,13 +5098,30 @@ async function main() {
4954
5098
  return;
4955
5099
  }
4956
5100
  }
5101
+ const budgetArg = args.find((a) => a.startsWith("--max-budget="));
5102
+ const budgetValue = budgetArg?.split("=")[1];
5103
+ let maxBudgetFlag;
5104
+ if (budgetValue !== void 0) {
5105
+ const parsed = Number(budgetValue);
5106
+ if (!isValidBudget(parsed)) {
5107
+ process.stderr.write(
5108
+ import_picocolors14.default.red(
5109
+ `Error: invalid --max-budget value "${budgetValue}". Use a positive number, e.g. --max-budget=10
5110
+ `
5111
+ )
5112
+ );
5113
+ process.exitCode = 1;
5114
+ return;
5115
+ }
5116
+ maxBudgetFlag = parsed;
5117
+ }
4957
5118
  loadEnvFile();
4958
5119
  printBanner();
4959
5120
  if (model === void 0 && process.stdin.isTTY) {
4960
5121
  model = await selectModel();
4961
5122
  }
4962
5123
  const effectiveModel = model ?? DEFAULT_AGENT_MODEL;
4963
- process.stderr.write(import_picocolors14.default.dim(`model: ${effectiveModel}
5124
+ process.stderr.write(import_picocolors14.default.dim(`Model: ${effectiveModel}
4964
5125
  `));
4965
5126
  const authResult = await authenticateAnthropic({ forceLogin, loginMethod, loginOrgUUID });
4966
5127
  if (authResult.method === "oauth") {
@@ -4970,9 +5131,13 @@ async function main() {
4970
5131
  } else {
4971
5132
  process.stderr.write(import_picocolors14.default.dim("Authenticated via API key\n"));
4972
5133
  }
5134
+ const maxBudgetUsd = resolveMaxBudgetUsd(maxBudgetFlag);
4973
5135
  const { session: connectedSession, kineticaVersion, degraded } = await connectWithRetry();
4974
5136
  session = connectedSession;
4975
- await runAgent(session, kineticaVersion, degraded, model);
5137
+ await runAgent(session, kineticaVersion, degraded, model, {
5138
+ authMethod: authResult.method,
5139
+ maxBudgetUsd
5140
+ });
4976
5141
  }
4977
5142
  function getSession() {
4978
5143
  return session;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetica/admin-agent",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Autonomous diagnostic agent for Kinetica databases",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Kinetica",