@inneranimalmedia/agentsam-sdk 2.6.0 → 2.6.2

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.
Files changed (145) hide show
  1. package/docs/AUTH_IDENTITY_CONTRACT.md +25 -27
  2. package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
  3. package/docs/RELEASES.md +7 -7
  4. package/docs/SDK_WORKER.md +4 -3
  5. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  6. package/docs/TEST_TIERS.md +26 -0
  7. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  8. package/package.json +28 -7
  9. package/packages/agentsam-repository/README.md +15 -0
  10. package/packages/agentsam-repository/package.json +25 -0
  11. package/packages/agentsam-repository/src/contracts.js +113 -0
  12. package/packages/agentsam-repository/src/index.js +3 -0
  13. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  14. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  15. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  16. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  17. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  18. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  19. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  20. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  21. package/packages/identity/package.json +2 -2
  22. package/packages/identity/src/contracts/auth-config.js +23 -40
  23. package/packages/identity/tests/auth-config.test.mjs +14 -23
  24. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  25. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  26. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  27. package/protocol/repository/repository-contract.schema.json +24 -0
  28. package/protocol/repository/repository-dependency.schema.json +24 -0
  29. package/protocol/repository/repository-identity.schema.json +17 -0
  30. package/protocol/rpc/v1/common.proto +16 -0
  31. package/protocol/rpc/v1/errors.proto +35 -0
  32. package/protocol/rpc/v1/knowledge.proto +77 -0
  33. package/services/knowledge/package-lock.json +333 -0
  34. package/services/knowledge/package.json +5 -1
  35. package/src/agent/responses-runner.js +63 -35
  36. package/src/capabilities/repository-snapshot.js +3 -3
  37. package/src/cli.js +36 -29
  38. package/src/commands/account-auth.js +1 -1
  39. package/src/commands/context-economics.js +17 -2
  40. package/src/commands/context.js +1 -1
  41. package/src/commands/db.js +20 -3
  42. package/src/commands/deploy.js +2 -2
  43. package/src/commands/env.js +90 -0
  44. package/src/commands/knowledge.js +12 -4
  45. package/src/commands/merkle-persist.js +30 -11
  46. package/src/commands/merkle.js +1 -1
  47. package/src/commands/models.js +123 -65
  48. package/src/commands/ollama.js +26 -0
  49. package/src/commands/preferences.js +53 -26
  50. package/src/commands/providers.js +308 -0
  51. package/src/commands/shell.js +240 -48
  52. package/src/commands/tunnel.js +8 -8
  53. package/src/commands/whoami.js +42 -21
  54. package/src/errors/contract.js +236 -0
  55. package/src/errors/diagnostic.js +1 -0
  56. package/src/errors/index.js +14 -0
  57. package/src/index.js +13 -1
  58. package/src/knowledge/service/auth.js +13 -0
  59. package/src/knowledge/service/grpc-client.js +115 -0
  60. package/src/knowledge/service/grpc-codec.js +237 -0
  61. package/src/knowledge/service/grpc-server.js +83 -0
  62. package/src/knowledge/service/job-engine.js +248 -0
  63. package/src/knowledge/service/server.js +87 -135
  64. package/src/knowledge/source.js +1 -1
  65. package/src/lib/account-session.js +94 -25
  66. package/src/lib/auth.js +314 -51
  67. package/src/lib/cli-preferences.js +31 -4
  68. package/src/lib/core-client.js +75 -30
  69. package/src/lib/deploy-receipt/index.js +2 -2
  70. package/src/lib/detect-context.js +14 -16
  71. package/src/lib/knowledge-docker.js +6 -3
  72. package/src/lib/local-sessions.js +23 -2
  73. package/src/lib/local-status.js +1 -1
  74. package/src/lib/project-config.js +1 -1
  75. package/src/lib/provider-credentials.js +230 -14
  76. package/src/lib/slash-commands.js +5 -3
  77. package/src/local/migrations.js +93 -0
  78. package/src/local/runtime-store.js +141 -0
  79. package/src/local/sqlite.js +2 -0
  80. package/src/local-pty/server.js +113 -51
  81. package/src/models/discovery.js +327 -0
  82. package/src/providers/anthropic-messages.js +192 -0
  83. package/src/providers/cloudflare-chat.js +183 -0
  84. package/src/providers/factory.js +69 -0
  85. package/src/providers/gemini-generate-content.js +208 -0
  86. package/src/providers/index.js +5 -0
  87. package/src/providers/ollama-chat.js +148 -0
  88. package/src/providers/openai-responses.js +226 -75
  89. package/src/repository/index.js +14 -2
  90. package/src/rpc/generated/common_grpc_pb.js +1 -0
  91. package/src/rpc/generated/common_pb.js +536 -0
  92. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  93. package/src/rpc/generated/errors_pb.js +482 -0
  94. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  95. package/src/rpc/generated/knowledge_pb.js +2168 -0
  96. package/src/rpc/generated/package.json +3 -0
  97. package/src/security/trust-boundary.js +2 -2
  98. package/src/telemetry/events.js +4 -1
  99. package/src/ui/cli/activity.js +76 -0
  100. package/src/ui/cli/compaction.js +15 -0
  101. package/src/ui/cli/footer.js +39 -0
  102. package/src/ui/cli/help.js +194 -0
  103. package/src/ui/cli/plan.js +20 -0
  104. package/src/ui/cli/runtime-events.js +110 -0
  105. package/src/ui/cli/waiting.js +16 -0
  106. package/src/ui/merkle/render.js +1 -1
  107. package/test/account-session.test.mjs +48 -11
  108. package/test/apps-scaffold-contract.test.mjs +43 -0
  109. package/test/cli/preferences-runtime.test.mjs +11 -0
  110. package/test/cli/runtime-ui.test.mjs +74 -0
  111. package/test/error-diagnostics.test.mjs +57 -1
  112. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  113. package/test/integration/cli-help.test.mjs +37 -0
  114. package/test/integration/knowledge-rpc.test.mjs +112 -0
  115. package/test/integration/merkle-cli.test.mjs +61 -0
  116. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  117. package/test/integration/provider-env-cli.test.mjs +49 -0
  118. package/test/integration/provider-factory.test.mjs +197 -0
  119. package/test/integration/repository-company-graph.test.mjs +90 -0
  120. package/test/integration/runtime-migrations.test.mjs +82 -0
  121. package/test/knowledge-service.test.mjs +5 -0
  122. package/test/knowledge.test.mjs +16 -0
  123. package/test/live/terminal-transport.live.test.mjs +24 -0
  124. package/test/local-sessions.test.mjs +7 -1
  125. package/test/models.test.mjs +101 -4
  126. package/test/ollama.test.mjs +21 -0
  127. package/test/portable-context.test.mjs +1 -1
  128. package/test/provider-credentials.test.mjs +45 -1
  129. package/test/release-hygiene.test.mjs +13 -5
  130. package/test/responses-runner.test.mjs +3 -1
  131. package/test/sdk-worker-contract.test.mjs +5 -1
  132. package/test/shell.test.mjs +50 -8
  133. package/test/smoke.mjs +2 -1
  134. package/test/terminal/local-pty.mock.test.mjs +151 -0
  135. package/test/whoami-resume.test.mjs +6 -5
  136. package/src/lib/prompt-byok.js +0 -57
  137. package/src/lib/save-sdk-token.js +0 -19
  138. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  139. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  140. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  141. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  142. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  143. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  144. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  145. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -10,19 +10,28 @@ import { runDb } from './db.js';
10
10
  import { runDeploy } from './deploy.js';
11
11
  import { runStatus } from './status.js';
12
12
  import { runModels } from './models.js';
13
+ import { runProviders } from './providers.js';
13
14
  import { configureCliPreferences } from './preferences.js';
14
15
  import { runCloudflare } from './cloudflare.js';
15
- import { createRuntimeActivity } from '../ui/runtime-activity.js';
16
+ import { probeOllamaModel, resolveOllamaConfig } from './ollama.js';
17
+ import { createInlineActivity } from '../ui/cli/activity.js';
18
+ import { createCliRuntimePresenter } from '../ui/cli/runtime-events.js';
19
+ import { renderCliFooter } from '../ui/cli/footer.js';
16
20
  import { diagnosticFromError, renderDiagnosticError } from '../errors/index.js';
17
21
  import { getModelRecord } from '../models/index.js';
22
+ import { discoverProviderModels } from '../models/discovery.js';
18
23
  import { readCliPreferences, updateCliPreferences } from '../lib/cli-preferences.js';
19
24
  import { buildContextEconomicsReport, renderContextEconomics } from './context-economics.js';
20
- import { createOpenAIResponsesAdapter } from '../providers/index.js';
25
+ import { createProviderAdapter } from '../providers/index.js';
21
26
  import { createCapabilityAdapter, runResponsesAgent } from '../agent/index.js';
22
27
  import { resolveProviderCredential } from '../lib/provider-credentials.js';
23
- import { createLocalSession, saveLocalSession, sessionTitleFromInput } from '../lib/local-sessions.js';
28
+ import { createLocalSession, saveLocalSession, sessionTitleFromInput, localSessionElapsedMs } from '../lib/local-sessions.js';
24
29
  import { grantExecutionApproval, isExecutionApproved, toolApprovalKey } from '../lib/execution-approvals.js';
25
30
  import { runWhoami } from './whoami.js';
31
+ import { runLogin, runLogout } from './account-auth.js';
32
+ import { runHelp } from '../ui/cli/help.js';
33
+ import { readAccountSession } from '../lib/account-session.js';
34
+ import { startRuntimeRun, finishRuntimeRun, recordRuntimeCompaction } from '../local/runtime-store.js';
26
35
 
27
36
  function writeLine(write, value = '') { write(`${value}\n`); }
28
37
 
@@ -76,18 +85,20 @@ export function tokenizeShellLine(input = '') {
76
85
 
77
86
  export function renderShellCatalog() {
78
87
  const next = SHELL_PHASES.find((phase) => phase.status === 'next' || phase.status === 'current');
79
- const rows = SLASH_COMMANDS.map((row) => ` ${row.cmd.padEnd(14)} ${row.description}`).join('\n');
80
- return `
81
- ╔════════════════════════════════╗
82
- ║ Agent Sam Terminal ║
83
- ╚════════════════════════════════╝
84
-
85
- Current milestone: ${next?.label ?? 'interactive runtime'}
86
- Type / and press Enter for the scrollable command picker.
87
-
88
- Slash commands (${SLASH_COMMANDS.length} implemented):
89
- ${rows}
90
- `;
88
+ return [
89
+ '',
90
+ ' Agent Sam',
91
+ ' Type normally to work with the selected model.',
92
+ '',
93
+ ' / command picker',
94
+ ' /model model · reasoning · processing',
95
+ ' /usage tokens · cost · resume receipt',
96
+ ' /help focused help',
97
+ ' /exit return to host shell',
98
+ '',
99
+ ` ${next?.label ?? 'interactive runtime'} · ${SLASH_COMMANDS.length} commands available through /`,
100
+ '',
101
+ ].join('\n');
91
102
  }
92
103
 
93
104
  function parseDeployOptions(args, cwd) {
@@ -109,11 +120,53 @@ function spawnGit(cwd, args) {
109
120
 
110
121
  function selectedModel(cwd) {
111
122
  const preferences = readCliPreferences(cwd) || {};
112
- const model = getModelRecord(preferences.modelPreference);
123
+ const snapshot = preferences.modelSnapshot?.model_key === preferences.modelPreference
124
+ ? preferences.modelSnapshot
125
+ : null;
126
+ const model = snapshot || getModelRecord(preferences.modelPreference);
113
127
  if (!model) throw new Error('Select an exact provider-verified model with /model first so Agent Sam can verify supported runtime controls.');
114
128
  return { preferences, model };
115
129
  }
116
130
 
131
+ async function resolveModelForTurn(cwd, state) {
132
+ const selected = selectedModel(cwd);
133
+ const { preferences } = selected;
134
+ let model = selected.model;
135
+
136
+ if (model.provider === 'ollama') {
137
+ const config = resolveOllamaConfig({}, process.env);
138
+ const probe = await probeOllamaModel(model.provider_model_id, config, state.providerFetchImpl || fetch);
139
+ if (!probe.ok) throw new Error(`ollama_model_probe_failed:${model.provider_model_id}:${probe.error || 'unknown'}`);
140
+ model = {
141
+ ...model,
142
+ context_window: probe.context_window,
143
+ context_window_source: probe.context_window_source || 'unknown',
144
+ capabilities: { ...(model.capabilities || {}), local_runtime: true, ...(Object.fromEntries((probe.capabilities || []).map((name) => [name, true]))) },
145
+ };
146
+ updateCliPreferences(cwd, { modelPreference: model.model_key, modelSnapshot: model });
147
+ return { preferences: readCliPreferences(cwd) || preferences, model, credential: null, verification: 'local_runtime' };
148
+ }
149
+
150
+ const credential = resolveProviderCredential(model.provider, { home: state.home });
151
+ if (!credential.configured) {
152
+ throw new Error(`provider_credential_unavailable:${model.provider}:${credential.error || credential.env || 'not_configured'}`);
153
+ }
154
+
155
+ const discovery = await discoverProviderModels(model.provider, credential, { fetchImpl: state.providerFetchImpl || fetch });
156
+ if (discovery.ok) {
157
+ const live = discovery.models.find((row) => row.provider_model_id === model.provider_model_id || row.model_key === model.model_key);
158
+ if (!live) throw new Error(`selected_model_not_available_for_credential:${model.provider}:${model.provider_model_id}`);
159
+ model = live;
160
+ updateCliPreferences(cwd, { modelPreference: live.model_key, modelSnapshot: live });
161
+ return { preferences: readCliPreferences(cwd) || preferences, model, credential, verification: 'provider_api' };
162
+ }
163
+
164
+ if (preferences.modelSnapshot?.availability === 'available') {
165
+ return { preferences, model, credential, verification: 'cached_provider_snapshot', discoveryError: discovery.error || null };
166
+ }
167
+ throw new Error(`provider_model_discovery_failed:${model.provider}:${discovery.error || 'unknown'}`);
168
+ }
169
+
117
170
  async function chooseReasoning(cwd, args, state) {
118
171
  const { preferences, model } = selectedModel(cwd);
119
172
  let effort = String(args[0] || '').trim().toLowerCase();
@@ -167,7 +220,7 @@ export async function runLocalAgent(goal, write, options = {}) {
167
220
  }
168
221
  const base = String(process.env.AGENTSAM_LOCAL_URL || 'http://127.0.0.1:8787').replace(/\/$/, '');
169
222
  const fetchImpl = options.fetchImpl || fetch;
170
- const activity = options.activity || createRuntimeActivity({ write, phase: 'thinking', interactive: options.interactive });
223
+ const activity = options.activity || createInlineActivity({ write, label: 'Thinking', interactive: options.interactive });
171
224
  let response;
172
225
  activity.start('thinking');
173
226
  try {
@@ -213,20 +266,40 @@ function formatUsd(value) {
213
266
  return currency + (amount < 0.01 ? amount.toFixed(6) : amount.toFixed(4));
214
267
  }
215
268
 
269
+ function formatElapsed(ms) {
270
+ const totalSeconds = Math.max(0, Math.round(Number(ms || 0) / 1000));
271
+ const hours = Math.floor(totalSeconds / 3600);
272
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
273
+ const seconds = totalSeconds % 60;
274
+ if (hours) return `${hours}h ${minutes}m ${seconds}s`;
275
+ if (minutes) return `${minutes}m ${seconds}s`;
276
+ return `${seconds}s`;
277
+ }
278
+
216
279
  export function renderSessionReceipt(session) {
217
280
  if (!session) return '';
218
281
  const usage = session.cumulative_usage || {};
219
282
  const input = Number(usage.input_tokens || 0);
220
283
  const output = Number(usage.output_tokens || 0);
221
284
  const cached = Number(usage.cached_input_tokens || 0);
285
+ const cacheWrite = Number(usage.cache_write_tokens || 0);
222
286
  const reasoning = Number(usage.reasoning_tokens || 0);
223
287
  const total = input + output;
224
288
  const active = Number(session.usage_snapshot?.current_context?.input_tokens || 0);
289
+ const model = session.provider_model_id || session.model_key || 'model unavailable';
290
+ const breakdown = session.cost_breakdown_usd || {};
291
+ const componentTotal = ['input', 'cached_input', 'cache_write', 'output'].reduce((sum, key) => sum + Number(breakdown[key] || 0), 0);
292
+ const elapsed = localSessionElapsedMs(session);
225
293
  const lines = [
226
294
  '',
227
- `Token usage: total=${formatCount(total)} input=${formatCount(input)}${cached ? ` (+ ${formatCount(cached)} cached)` : ''} output=${formatCount(output)}${reasoning ? ` reasoning=${formatCount(reasoning)}` : ''}`,
228
- `Cost: ${formatUsd(session.total_cost_usd)} · ${session.model_key || 'model unavailable'}${session.actual_service_tier ? ` · ${session.actual_service_tier}` : ''}`,
295
+ 'Session summary',
296
+ `Token usage: total=${formatCount(total)} input=${formatCount(input)}${cached ? ` (+ ${formatCount(cached)} cached)` : ''}${cacheWrite ? ` (+ ${formatCount(cacheWrite)} cache write)` : ''} output=${formatCount(output)}${reasoning ? ` reasoning=${formatCount(reasoning)}` : ''}`,
297
+ `Spent: ${formatUsd(session.total_cost_usd)} · ${model}${session.actual_service_tier ? ` · ${session.actual_service_tier}` : ''}`,
298
+ `Elapsed: ${formatElapsed(elapsed)}`,
229
299
  ];
300
+ if (componentTotal > 0) {
301
+ lines.push(`Cost breakdown: input ${formatUsd(breakdown.input)} · cached ${formatUsd(breakdown.cached_input)} · cache write ${formatUsd(breakdown.cache_write)} · output ${formatUsd(breakdown.output)}`);
302
+ }
230
303
  if (active) lines.push(`Active context: ${formatCount(active)} tokens`);
231
304
  lines.push('', 'To continue this session, run:', ` agentsam resume ${session.id}`, '', 'Or run:', ' agentsam resume', '', 'and select:', ` ${session.title || 'this session'}`, '');
232
305
  return lines.join('\n');
@@ -263,20 +336,28 @@ function safeToolInput(value, depth = 0) {
263
336
  }
264
337
 
265
338
  async function approveModelRequest(preflight, state) {
339
+ const projected = Number(preflight.projected_max_call_cost_usd);
340
+ const threshold = Number(process.env.AGENTSAM_CONFIRM_CALL_COST_USD || 0);
341
+ if (!(Number.isFinite(projected) && projected >= 0)) return true;
342
+ if (!(Number.isFinite(threshold) && threshold > 0) || projected <= threshold) return true;
343
+
266
344
  const approvedCeiling = Number(state.session?.approved_projected_call_cost_usd || 0);
267
- if (approvedCeiling > 0 && preflight.projected_max_call_cost_usd <= approvedCeiling) return true;
345
+ if (approvedCeiling > 0 && projected <= approvedCeiling) return true;
268
346
  if (!state.interactive) return false;
347
+
348
+ state.activity?.clear?.();
269
349
  writeLine(state.write, '');
270
- writeLine(state.write, ' Model request');
271
- writeLine(state.write, ` model ${preflight.model}`);
272
- writeLine(state.write, ` reasoning ${preflight.reasoning_effort}`);
273
- writeLine(state.write, ` processing ${preflight.service_tier}`);
274
- writeLine(state.write, ` context ~${formatCount(preflight.estimated_input_tokens)} input tokens`);
275
- writeLine(state.write, ` max call ${formatUsd(preflight.projected_max_call_cost_usd)} conservative ceiling`);
276
- if (Number.isFinite(preflight.tokens_until_pricing_threshold)) writeLine(state.write, ` price cliff ${formatCount(preflight.tokens_until_pricing_threshold)} tokens headroom`);
277
- const approved = await confirm({ message: 'Send this request?', initialValue: true });
350
+ writeLine(state.write, ' ◆ Model cost approval');
351
+ writeLine(state.write, ` model ${preflight.model}`);
352
+ writeLine(state.write, ` reasoning ${preflight.reasoning_effort}`);
353
+ writeLine(state.write, ` processing ${preflight.service_tier}`);
354
+ writeLine(state.write, ` context ~${formatCount(preflight.estimated_input_tokens)} input tokens`);
355
+ writeLine(state.write, ` max call ${formatUsd(projected)} conservative ceiling`);
356
+ if (Number.isFinite(preflight.tokens_until_pricing_threshold)) writeLine(state.write, ` headroom ${formatCount(preflight.tokens_until_pricing_threshold)} tokens`);
357
+ const approved = await confirm({ message: 'Allow this call?', initialValue: false });
278
358
  if (isCancel(approved) || approved !== true) return false;
279
- persistSession(state, { approved_projected_call_cost_usd: Math.max(approvedCeiling, preflight.projected_max_call_cost_usd) });
359
+ persistSession(state, { approved_projected_call_cost_usd: Math.max(approvedCeiling, projected) });
360
+ state.activity?.start?.('Working');
280
361
  return true;
281
362
  }
282
363
 
@@ -288,6 +369,7 @@ async function approveToolExecution(request, state) {
288
369
  if (!state.interactive) return false;
289
370
 
290
371
  const summary = JSON.stringify(safeToolInput(request.input || {}));
372
+ state.activity?.clear?.();
291
373
  writeLine(state.write, '');
292
374
  writeLine(state.write, ' Agent Sam needs execution permission');
293
375
  writeLine(state.write, ` action ${key}`);
@@ -306,26 +388,52 @@ async function approveToolExecution(request, state) {
306
388
  });
307
389
  if (isCancel(choice) || choice === 'deny') return false;
308
390
  if (choice === 'always') grantExecutionApproval({ cwd: state.cwd, key, label: key }, { home: state.home });
391
+ state.activity?.start?.(`Working · ${request.capability_id}`);
309
392
  return true;
310
393
  }
311
394
 
312
395
  async function runInteractiveModelTurn(prompt, state) {
313
- const { preferences, model } = selectedModel(state.cwd);
314
- if (model.provider !== 'openai') throw new Error(`interactive_provider_not_implemented:${model.provider}`);
315
- const credential = resolveProviderCredential(model.provider, { home: state.home });
316
- if (!credential.configured) throw new Error(`provider_credential_unavailable:${model.provider}:${credential.error || credential.env || 'not_configured'}`);
317
-
318
- const activity = createRuntimeActivity({ write: state.write, phase: 'thinking', interactive: state.interactive });
319
- const provider = createOpenAIResponsesAdapter({ apiKey: credential.value });
396
+ const resolved = await resolveModelForTurn(state.cwd, state);
397
+ const { preferences, model, credential } = resolved;
398
+ const provider = createProviderAdapter({
399
+ modelRecord: model,
400
+ credential,
401
+ endpoint: model.provider === 'ollama' ? process.env.OLLAMA_BASE_URL : undefined,
402
+ fetchImpl: state.providerFetchImpl,
403
+ });
320
404
  const capabilityAdapter = createCapabilityAdapter();
405
+
321
406
  const samePolicy = state.session
322
407
  && state.session.model_key === model.model_key
323
408
  && state.session.reasoning_effort === preferences.reasoningEffort
324
409
  && state.session.requested_service_tier === preferences.serviceTier;
325
- const previousResponseId = samePolicy ? state.session?.provider_state?.previous_response_id : null;
410
+ const previousProviderState = samePolicy ? state.session?.provider_state : null;
326
411
  const previousUsageSnapshot = samePolicy ? state.session?.usage_snapshot : null;
412
+ const accountId = readAccountSession({ home: state.home })?.account_id || null;
413
+ let runtimeRunId = null;
414
+ try {
415
+ runtimeRunId = await startRuntimeRun({
416
+ cwd: state.cwd,
417
+ account_id: accountId,
418
+ mode: 'agent',
419
+ model_key: model.model_key,
420
+ reasoning_effort: preferences.reasoningEffort,
421
+ service_tier: preferences.serviceTier,
422
+ });
423
+ } catch {
424
+ runtimeRunId = null;
425
+ }
426
+
427
+ const activity = createInlineActivity({
428
+ write: state.write,
429
+ interactive: state.interactive,
430
+ label: `Thinking · ${model.provider_model_id}`,
431
+ });
432
+ state.activity = activity;
433
+ const presenter = createCliRuntimePresenter({ activity, write: state.write, state });
434
+ const startedAt = Date.now();
435
+ activity.start(`Thinking · ${model.provider_model_id}`);
327
436
 
328
- activity.start('thinking');
329
437
  let result;
330
438
  try {
331
439
  result = await runResponsesAgent({
@@ -334,30 +442,80 @@ async function runInteractiveModelTurn(prompt, state) {
334
442
  cwd: state.cwd,
335
443
  prompt,
336
444
  model: model.model_key,
445
+ modelRecord: model,
337
446
  reasoningEffort: preferences.reasoningEffort,
338
447
  serviceTier: preferences.serviceTier,
339
- previousResponseId,
448
+ previousProviderState,
340
449
  previousUsageSnapshot,
341
450
  cumulativeUsage: state.session?.cumulative_usage || null,
342
451
  promptCacheKey: state.session?.id || undefined,
343
- runId: state.session?.id || undefined,
452
+ runId: runtimeRunId || state.session?.id || undefined,
344
453
  beforeRequest: (preflight) => approveModelRequest(preflight, state),
345
454
  beforeTool: (request) => approveToolExecution(request, state),
346
455
  emit(event) {
456
+ presenter.handle(event);
347
457
  if (event?.type === 'usage.snapshot') state.usageSnapshot = event.payload;
348
458
  },
349
459
  });
350
460
  } catch (error) {
351
- activity.fail('failed');
461
+ activity.fail('Failed');
462
+ state.activity = null;
463
+ if (runtimeRunId) {
464
+ try {
465
+ await finishRuntimeRun({
466
+ cwd: state.cwd,
467
+ id: runtimeRunId,
468
+ status: 'failed',
469
+ error_code: error?.code || 'interactive_error',
470
+ error_message: error?.message || String(error),
471
+ latency_ms: Date.now() - startedAt,
472
+ });
473
+ } catch { /* execution result remains primary */ }
474
+ }
352
475
  throw error;
353
476
  }
354
- activity.succeed('done');
477
+
478
+ activity.succeed(result.tool_receipts?.length ? `Done · ${result.tool_receipts.length} tool call${result.tool_receipts.length === 1 ? '' : 's'}` : 'Done');
479
+ state.activity = null;
480
+
481
+ const compacted = presenter.compactionReceipt();
482
+ if (compacted) writeLine(state.write, compacted);
483
+
355
484
  if (result.output_text) {
356
485
  writeLine(state.write, '');
357
486
  writeLine(state.write, result.output_text);
358
487
  writeLine(state.write, '');
359
488
  }
489
+
360
490
  state.usageSnapshot = result.usage_snapshot;
491
+ if (runtimeRunId) {
492
+ try {
493
+ await finishRuntimeRun({
494
+ cwd: state.cwd,
495
+ id: runtimeRunId,
496
+ status: 'completed',
497
+ actual_service_tier: result.actual_service_tier,
498
+ usage: result.cumulative_usage || result.usage_snapshot?.cumulative || {},
499
+ cost_usd: result.total_cost_usd || 0,
500
+ latency_ms: Date.now() - startedAt,
501
+ });
502
+ if (presenter.state.lastCompaction) {
503
+ await recordRuntimeCompaction({
504
+ cwd: state.cwd,
505
+ account_id: accountId,
506
+ agent_run_id: runtimeRunId,
507
+ session_id: state.session?.id,
508
+ provider: model.provider,
509
+ model_key: model.model_key,
510
+ tokens_before: presenter.state.lastCompaction.tokens_before,
511
+ tokens_after: presenter.state.lastCompaction.tokens_after,
512
+ summary_text: presenter.state.lastCompaction.summary_text || '',
513
+ source_kind: model.provider === 'ollama' ? 'filesystem' : 'api',
514
+ metadata: { verification: resolved.verification, compaction_id: presenter.state.lastCompaction.compaction_id || null },
515
+ });
516
+ }
517
+ } catch { /* local telemetry persistence is best-effort */ }
518
+ }
361
519
  if (state.session) {
362
520
  persistSession(state, {
363
521
  status: 'active',
@@ -366,13 +524,24 @@ async function runInteractiveModelTurn(prompt, state) {
366
524
  reasoning_effort: result.reasoning_effort,
367
525
  requested_service_tier: result.requested_service_tier,
368
526
  actual_service_tier: result.actual_service_tier,
369
- provider_state: { provider: model.provider, previous_response_id: result.response_id },
527
+ provider_state: { provider: model.provider, ...(result.provider_state || {}) },
370
528
  usage_snapshot: result.usage_snapshot,
371
529
  cumulative_usage: result.cumulative_usage,
372
530
  total_cost_usd: Number(state.session.total_cost_usd || 0) + Number(result.total_cost_usd || 0),
531
+ cost_breakdown_usd: Object.fromEntries(['input', 'cached_input', 'cache_write', 'output'].map((key) => [
532
+ key, Number(state.session.cost_breakdown_usd?.[key] || 0) + Number(result.cost_breakdown_usd?.[key] || 0),
533
+ ])),
373
534
  last_error: null,
374
535
  });
375
536
  }
537
+
538
+ writeLine(state.write, renderCliFooter({
539
+ model: result.model || model.provider_model_id,
540
+ usageSnapshot: result.usage_snapshot,
541
+ tier: result.actual_service_tier,
542
+ elapsedMs: Date.now() - startedAt,
543
+ }));
544
+ writeLine(state.write, '');
376
545
  return result;
377
546
  }
378
547
 
@@ -390,13 +559,13 @@ export async function dispatchShellLine(line, state = {}) {
390
559
  await showCommandPicker(state);
391
560
  break;
392
561
  case '/help':
393
- write(renderShellCatalog());
562
+ await runHelp(args, { write, interactive: state.interactive });
394
563
  break;
395
564
  case '/exit':
396
565
  case '/quit':
397
566
  return { handled: true, exit: true, cwd: state.cwd };
398
567
  case '/model': {
399
- const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false, section: 'model' });
568
+ const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false, section: 'model', home: state.home });
400
569
  state.cwd = configured.identity.root;
401
570
  break;
402
571
  }
@@ -425,10 +594,21 @@ export async function dispatchShellLine(line, state = {}) {
425
594
  case '/models':
426
595
  await runModels(args, { cwd: state.cwd, write, home: state.home });
427
596
  break;
597
+ case '/providers':
598
+ await runProviders(args, { write, home: state.home, interactive: state.interactive });
599
+ break;
600
+ case '/login':
601
+ await runLogin(args, { write, home: state.home });
602
+ break;
603
+ case '/logout':
604
+ runLogout(args, { write, home: state.home });
605
+ if (state.session) write(renderSessionReceipt(state.session));
606
+ break;
428
607
  case '/whoami':
429
608
  await runWhoami(args, { write, home: state.home });
430
609
  break;
431
610
  case '/session':
611
+ case '/usage':
432
612
  if (state.session) write(renderSessionReceipt(state.session));
433
613
  else writeLine(write, ' No persistent session is active in this shell invocation.');
434
614
  break;
@@ -437,7 +617,7 @@ export async function dispatchShellLine(line, state = {}) {
437
617
  await runCloudflare(args.length ? args : ['commands'], { cwd: state.cwd, write });
438
618
  break;
439
619
  case '/settings': {
440
- const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false });
620
+ const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false, home: state.home });
441
621
  state.cwd = configured.identity.root;
442
622
  break;
443
623
  }
@@ -502,6 +682,7 @@ export async function runShell(argv = [], options = {}) {
502
682
  usageSnapshot: options.usageSnapshot || options.session?.usage_snapshot || null,
503
683
  session: options.session || null,
504
684
  home: options.home,
685
+ providerFetchImpl: options.providerFetchImpl,
505
686
  };
506
687
  const sub = argv[0] || '';
507
688
  if (sub === 'list' || sub === 'status') { write(renderShellCatalog()); return; }
@@ -515,7 +696,12 @@ export async function runShell(argv = [], options = {}) {
515
696
 
516
697
  if (state.session) {
517
698
  state.cwd = path.resolve(state.session.cwd || state.cwd);
518
- state.session = saveLocalSession({ ...state.session, status: 'active', cwd: state.cwd }, { home: state.home });
699
+ state.session = saveLocalSession({
700
+ ...state.session,
701
+ status: 'active',
702
+ cwd: state.cwd,
703
+ active_started_at: new Date().toISOString(),
704
+ }, { home: state.home });
519
705
  state.usageSnapshot = state.session.usage_snapshot || state.usageSnapshot;
520
706
  } else {
521
707
  const preferences = readCliPreferences(state.cwd) || {};
@@ -551,7 +737,13 @@ export async function runShell(argv = [], options = {}) {
551
737
  if (rl.terminal) { rl.setPrompt(promptText()); rl.prompt(); }
552
738
  }
553
739
  if (state.session) {
554
- persistSession(state, { status: interrupted ? 'interrupted' : 'paused', cwd: state.cwd });
740
+ const activeElapsedMs = localSessionElapsedMs(state.session);
741
+ persistSession(state, {
742
+ status: interrupted ? 'interrupted' : 'paused',
743
+ cwd: state.cwd,
744
+ active_elapsed_ms: activeElapsedMs,
745
+ active_started_at: null,
746
+ });
555
747
  if (options.receipt !== false) write(renderSessionReceipt(state.session));
556
748
  }
557
749
  return state.session;
@@ -7,7 +7,7 @@
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
8
  import { authenticateViaBrowser } from '../lib/auth.js';
9
9
  import { postJson } from '../lib/core-client.js';
10
- import { resolveAccountSdkKey } from '../lib/account-session.js';
10
+ import { resolveAccountAuth } from '../lib/account-session.js';
11
11
 
12
12
  const DEFAULT_PORT = 3099;
13
13
 
@@ -33,7 +33,7 @@ function parseArgs(argv) {
33
33
  else if (a === '--platform') opts.platform = argv[++i] || opts.platform;
34
34
  else if (a === '--shell') opts.shell = argv[++i] || opts.shell;
35
35
  else if (a === '--token' && argv[i + 1]) {
36
- process.env.AGENTSAM_SDK_KEY = argv[++i];
36
+ process.env.AGENTSAM_API_KEY = argv[++i];
37
37
  }
38
38
  }
39
39
  return opts;
@@ -51,13 +51,13 @@ function ensureCloudflared() {
51
51
  }
52
52
 
53
53
  async function resolveToken() {
54
- const existing = resolveAccountSdkKey({ env: process.env }).value;
55
- if (existing.startsWith('sdk_')) return existing;
54
+ const existing = resolveAccountAuth({ env: process.env });
55
+ if (existing.value) return existing.value;
56
+ if (existing.error) throw new Error(existing.error);
56
57
  const session = await authenticateViaBrowser();
57
- const tok = String(session?.access_token || '').trim();
58
- if (!tok.startsWith('sdk_')) throw new Error('IAM auth did not return an sdk_ bearer key');
59
- process.env.AGENTSAM_SDK_KEY = tok;
60
- return tok;
58
+ const token = String(session?.access_token || '').trim();
59
+ if (!token) throw new Error('IAM auth did not return a browser session credential');
60
+ return token;
61
61
  }
62
62
 
63
63
  async function assertLocalPty(port) {
@@ -1,49 +1,68 @@
1
1
  import { getJson } from '../lib/core-client.js';
2
- import { resolveAccountSdkKey } from '../lib/account-session.js';
2
+ import {
3
+ describeAccountSession,
4
+ resolveAccountApiKey,
5
+ resolveAccountAuth,
6
+ } from '../lib/account-session.js';
3
7
  import { listProviderCredentialStatus } from '../lib/provider-credentials.js';
4
8
 
5
9
  function writeLine(write, value = '') { write(`${value}\n`); }
6
10
 
7
11
  export async function collectWhoami(options = {}) {
8
12
  const env = options.env || process.env;
9
- const sdk = resolveAccountSdkKey({ env, explicit: options.token || '', home: options.home });
10
- const token = sdk.value;
13
+ const apiKey = resolveAccountApiKey({ env, explicit: options.token || '', home: options.home });
14
+ const browserSession = describeAccountSession({ env, home: options.home });
15
+ const active = resolveAccountAuth({ env, explicit: options.token || '', home: options.home });
11
16
  const credentials = listProviderCredentialStatus({ env, home: options.home });
12
- if (!token) return {
13
- schema_version: 1,
17
+
18
+ const base = {
19
+ schema_version: 2,
14
20
  authenticated: false,
15
21
  authority: 'iam',
16
22
  identity: null,
17
- sdk_credential: { configured: false, source: null },
23
+ active_auth: {
24
+ configured: Boolean(active.value || active.error),
25
+ kind: active.kind || null,
26
+ source: active.source || null,
27
+ valid: null,
28
+ error: active.error || null,
29
+ },
30
+ api_key: {
31
+ configured: Boolean(apiKey.value || apiKey.error),
32
+ source: apiKey.source || null,
33
+ valid: apiKey.error ? false : null,
34
+ error: apiKey.error || null,
35
+ },
36
+ browser_session: browserSession,
18
37
  provider_credentials: credentials,
19
38
  };
39
+
40
+ if (!active.value) return base;
41
+
20
42
  try {
21
- const loader = options.contextLoader || ((sdkKey) => getJson('/api/sdk/context', sdkKey));
22
- const context = await loader(token);
43
+ const loader = options.contextLoader || ((token) => getJson('/api/sdk/context', token));
44
+ const context = await loader(active.value);
23
45
  return {
24
- schema_version: 1,
46
+ ...base,
25
47
  authenticated: true,
26
- authority: 'iam',
27
48
  identity: {
28
49
  user_id: context?.user_id || null,
29
50
  account_id: context?.account_id || null,
30
51
  email: context?.email || context?.user?.email || null,
31
52
  },
32
- sdk_credential: { configured: true, source: sdk.source, valid: true },
33
- provider_credentials: credentials,
53
+ active_auth: { ...base.active_auth, valid: true, error: null },
54
+ api_key: active.kind === 'api_key' ? { ...base.api_key, valid: true, error: null } : base.api_key,
34
55
  cloudflare_connected: context?.cloudflare?.ok === true,
35
56
  byok: context?.byok && typeof context.byok === 'object'
36
57
  ? Object.fromEntries(Object.entries(context.byok).map(([key, value]) => [key, { configured: value?.configured === true }]))
37
58
  : {},
38
59
  };
39
60
  } catch (error) {
61
+ const message = error?.message || String(error);
40
62
  return {
41
- schema_version: 1,
42
- authenticated: false,
43
- authority: 'iam',
44
- identity: null,
45
- sdk_credential: { configured: true, source: sdk.source, valid: false, error: error?.message || String(error) },
46
- provider_credentials: credentials,
63
+ ...base,
64
+ active_auth: { ...base.active_auth, valid: false, error: message },
65
+ api_key: active.kind === 'api_key' ? { ...base.api_key, valid: false, error: message } : base.api_key,
47
66
  };
48
67
  }
49
68
  }
@@ -56,17 +75,19 @@ export function renderWhoami(status) {
56
75
  if (status.identity?.email) lines.push(` email ${status.identity.email}`);
57
76
  if (status.identity?.account_id) lines.push(` account ${status.identity.account_id}`);
58
77
  if (status.identity?.user_id) lines.push(` user ${status.identity.user_id}`);
78
+ lines.push(` active auth ${status.active_auth?.kind || 'unknown'} · ${status.active_auth?.source || 'runtime'}`);
59
79
  } else {
60
80
  lines.push(' authenticated no');
61
- lines.push(` SDK key ${status.sdk_credential?.configured ? 'present but not validated' : 'not configured'}`);
62
- if (status.sdk_credential?.error) lines.push(` error ${status.sdk_credential.error}`);
81
+ lines.push(` API key ${status.api_key?.configured ? status.api_key?.valid === false ? 'invalid' : 'configured' : 'not configured'}`);
82
+ lines.push(` browser login ${status.browser_session?.configured ? 'stored' : 'not configured'}`);
83
+ if (status.active_auth?.error) lines.push(` error ${status.active_auth.error}`);
63
84
  }
64
85
  lines.push('');
65
86
  lines.push(' Provider credentials');
66
87
  for (const row of status.provider_credentials || []) {
67
88
  const state = row.configured ? 'available' : row.error ? `blocked (${row.error})` : 'not configured';
68
89
  const source = row.source ? ` · ${row.source}` : '';
69
- lines.push(` ${String(row.provider).padEnd(12)} ${state}${source}`);
90
+ lines.push(` ${String(row.provider).padEnd(16)} ${state}${source}`);
70
91
  }
71
92
  lines.push('');
72
93
  lines.push(' Secret values are never printed by whoami.');