@inneranimalmedia/agentsam-sdk 2.6.0 → 2.6.1

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