@namzu/cli 20.0.0 → 21.0.0

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 (155) hide show
  1. package/CHANGELOG.md +255 -0
  2. package/README.md +42 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +24 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/acp.d.ts.map +1 -1
  7. package/dist/commands/acp.js +6 -1
  8. package/dist/commands/acp.js.map +1 -1
  9. package/dist/commands/run-stream.d.ts.map +1 -1
  10. package/dist/commands/run-stream.js +9 -1
  11. package/dist/commands/run-stream.js.map +1 -1
  12. package/dist/commands/run.d.ts.map +1 -1
  13. package/dist/commands/run.js +14 -11
  14. package/dist/commands/run.js.map +1 -1
  15. package/dist/config/load.d.ts.map +1 -1
  16. package/dist/config/load.js +21 -0
  17. package/dist/config/load.js.map +1 -1
  18. package/dist/config/schema.d.ts +11 -4
  19. package/dist/config/schema.d.ts.map +1 -1
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/doctor/checks/chain.d.ts.map +1 -1
  22. package/dist/doctor/checks/chain.js +11 -5
  23. package/dist/doctor/checks/chain.js.map +1 -1
  24. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  25. package/dist/doctor/checks/credentials.js +5 -1
  26. package/dist/doctor/checks/credentials.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/integrations/providers/access.d.ts +7 -0
  31. package/dist/integrations/providers/access.d.ts.map +1 -0
  32. package/dist/integrations/providers/access.js +17 -0
  33. package/dist/integrations/providers/access.js.map +1 -0
  34. package/dist/integrations/providers/discover.d.ts +17 -54
  35. package/dist/integrations/providers/discover.d.ts.map +1 -1
  36. package/dist/integrations/providers/discover.js +46 -58
  37. package/dist/integrations/providers/discover.js.map +1 -1
  38. package/dist/integrations/providers/harness-credentials.d.ts +14 -0
  39. package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
  40. package/dist/integrations/providers/harness-credentials.js +46 -0
  41. package/dist/integrations/providers/harness-credentials.js.map +1 -1
  42. package/dist/integrations/providers/model-reasoning.d.ts +4 -0
  43. package/dist/integrations/providers/model-reasoning.d.ts.map +1 -0
  44. package/dist/integrations/providers/model-reasoning.js +46 -0
  45. package/dist/integrations/providers/model-reasoning.js.map +1 -0
  46. package/dist/integrations/providers/register.d.ts +1 -1
  47. package/dist/integrations/providers/register.d.ts.map +1 -1
  48. package/dist/integrations/providers/register.js +11 -1
  49. package/dist/integrations/providers/register.js.map +1 -1
  50. package/dist/integrations/providers/registry.d.ts +4 -4
  51. package/dist/integrations/providers/registry.d.ts.map +1 -1
  52. package/dist/integrations/providers/registry.js +23 -0
  53. package/dist/integrations/providers/registry.js.map +1 -1
  54. package/dist/integrations/sessions/context-inventory.d.ts +4 -0
  55. package/dist/integrations/sessions/context-inventory.d.ts.map +1 -0
  56. package/dist/integrations/sessions/context-inventory.js +44 -0
  57. package/dist/integrations/sessions/context-inventory.js.map +1 -0
  58. package/dist/integrations/sessions/conversation-search.d.ts +60 -0
  59. package/dist/integrations/sessions/conversation-search.d.ts.map +1 -0
  60. package/dist/integrations/sessions/conversation-search.js +461 -0
  61. package/dist/integrations/sessions/conversation-search.js.map +1 -0
  62. package/dist/integrations/sessions/task-context.d.ts +4 -0
  63. package/dist/integrations/sessions/task-context.d.ts.map +1 -0
  64. package/dist/integrations/sessions/task-context.js +73 -0
  65. package/dist/integrations/sessions/task-context.js.map +1 -0
  66. package/dist/integrations/subagents/activity.d.ts +3 -1
  67. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  68. package/dist/integrations/subagents/activity.js +20 -2
  69. package/dist/integrations/subagents/activity.js.map +1 -1
  70. package/dist/integrations/subagents/runtime.d.ts +7 -4
  71. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  72. package/dist/integrations/subagents/runtime.js +105 -17
  73. package/dist/integrations/subagents/runtime.js.map +1 -1
  74. package/dist/memory/io.d.ts +12 -0
  75. package/dist/memory/io.d.ts.map +1 -0
  76. package/dist/memory/io.js +177 -0
  77. package/dist/memory/io.js.map +1 -0
  78. package/dist/memory/presentation.d.ts +4 -1
  79. package/dist/memory/presentation.d.ts.map +1 -1
  80. package/dist/memory/presentation.js +16 -2
  81. package/dist/memory/presentation.js.map +1 -1
  82. package/dist/memory/store.d.ts +17 -3
  83. package/dist/memory/store.d.ts.map +1 -1
  84. package/dist/memory/store.js +61 -35
  85. package/dist/memory/store.js.map +1 -1
  86. package/dist/output-schema.d.ts +4 -0
  87. package/dist/output-schema.d.ts.map +1 -0
  88. package/dist/output-schema.js +18 -0
  89. package/dist/output-schema.js.map +1 -0
  90. package/dist/tui/AgentExplorer.d.ts +13 -2
  91. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  92. package/dist/tui/AgentExplorer.js +85 -23
  93. package/dist/tui/AgentExplorer.js.map +1 -1
  94. package/dist/tui/App.d.ts.map +1 -1
  95. package/dist/tui/App.js +454 -64
  96. package/dist/tui/App.js.map +1 -1
  97. package/dist/tui/BrandHeader.d.ts +1 -4
  98. package/dist/tui/BrandHeader.d.ts.map +1 -1
  99. package/dist/tui/BrandHeader.js +2 -6
  100. package/dist/tui/BrandHeader.js.map +1 -1
  101. package/dist/tui/Composer.d.ts +2 -1
  102. package/dist/tui/Composer.d.ts.map +1 -1
  103. package/dist/tui/Composer.js +9 -2
  104. package/dist/tui/Composer.js.map +1 -1
  105. package/dist/tui/PermissionOverlay.d.ts +1 -1
  106. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  107. package/dist/tui/PermissionOverlay.js +8 -1
  108. package/dist/tui/PermissionOverlay.js.map +1 -1
  109. package/dist/tui/Picker.d.ts.map +1 -1
  110. package/dist/tui/Picker.js +94 -21
  111. package/dist/tui/Picker.js.map +1 -1
  112. package/dist/tui/TextPrompt.d.ts +3 -1
  113. package/dist/tui/TextPrompt.d.ts.map +1 -1
  114. package/dist/tui/TextPrompt.js +55 -9
  115. package/dist/tui/TextPrompt.js.map +1 -1
  116. package/dist/tui/Transcript.d.ts.map +1 -1
  117. package/dist/tui/Transcript.js +1 -1
  118. package/dist/tui/Transcript.js.map +1 -1
  119. package/dist/tui/agent.d.ts +18 -3
  120. package/dist/tui/agent.d.ts.map +1 -1
  121. package/dist/tui/agent.js +174 -58
  122. package/dist/tui/agent.js.map +1 -1
  123. package/dist/tui/model-choices.d.ts +3 -1
  124. package/dist/tui/model-choices.d.ts.map +1 -1
  125. package/dist/tui/model-choices.js +12 -1
  126. package/dist/tui/model-choices.js.map +1 -1
  127. package/dist/tui/model-search.d.ts +4 -0
  128. package/dist/tui/model-search.d.ts.map +1 -0
  129. package/dist/tui/model-search.js +11 -0
  130. package/dist/tui/model-search.js.map +1 -0
  131. package/dist/tui/model-selection-intent.d.ts +8 -0
  132. package/dist/tui/model-selection-intent.d.ts.map +1 -0
  133. package/dist/tui/model-selection-intent.js +96 -0
  134. package/dist/tui/model-selection-intent.js.map +1 -0
  135. package/dist/tui/model-switch-tool.d.ts +7 -0
  136. package/dist/tui/model-switch-tool.d.ts.map +1 -0
  137. package/dist/tui/model-switch-tool.js +74 -0
  138. package/dist/tui/model-switch-tool.js.map +1 -0
  139. package/dist/tui/model-switch.d.ts +35 -0
  140. package/dist/tui/model-switch.d.ts.map +1 -0
  141. package/dist/tui/model-switch.js +106 -0
  142. package/dist/tui/model-switch.js.map +1 -0
  143. package/dist/tui/permission-review.d.ts.map +1 -1
  144. package/dist/tui/permission-review.js +25 -12
  145. package/dist/tui/permission-review.js.map +1 -1
  146. package/dist/tui/run-interruption.d.ts +7 -0
  147. package/dist/tui/run-interruption.d.ts.map +1 -1
  148. package/dist/tui/run-interruption.js +41 -0
  149. package/dist/tui/run-interruption.js.map +1 -1
  150. package/dist/tui/slashCommands.d.ts.map +1 -1
  151. package/dist/tui/slashCommands.js +2 -1
  152. package/dist/tui/slashCommands.js.map +1 -1
  153. package/dist/tui/types.d.ts +3 -1
  154. package/dist/tui/types.d.ts.map +1 -1
  155. package/package.json +8 -7
package/dist/tui/agent.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * `emptySession()` whose `send()` yields a single error event so the UI
21
21
  * renders an actionable hint rather than crashing.
22
22
  */
23
- import { BOOT_EVENT_NAMES, BackgroundJobRegistry, DefaultPathBuilder, DiskCheckpointStore, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, WebFetchTool, batchNeedsReview, buildAskUserQuestionTool, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createReviewHandler, createToolPresenter, generateProjectId, generateRunId, generateSessionId, generateTenantId, generateTopicId, getBuiltinTools, isReviewExempt, query, resumeRun, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
23
+ import { BOOT_EVENT_NAMES, BackgroundJobRegistry, DefaultPathBuilder, DiskCheckpointStore, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, ToolRegistry, WebFetchTool, batchNeedsReview, buildAskUserQuestionTool, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createMemoryRecallStep, createReviewHandler, createToolPresenter, generateProjectId, generateRunId, generateSessionId, generateTenantId, generateTopicId, getBuiltinTools, isReviewExempt, query, resumeRun, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
24
24
  import { SubprocessComputerUseHost } from '@namzu/computer-use';
25
25
  import { realpath, stat } from 'node:fs/promises';
26
26
  import { join, parse, resolve } from 'node:path';
@@ -34,15 +34,22 @@ import { resolveSandbox, sandboxResolvedSeverity, } from '../context/sandbox.js'
34
34
  import { composeTurnSnapshot, readTurnSnapshot } from '../context/turn-snapshot.js';
35
35
  import { connectMcpServers, } from '../integrations/mcp/servers.js';
36
36
  import { createCliPluginRuntime } from '../integrations/plugins/runtime.js';
37
+ import { hasApiCredential, requiresCredentialForModel } from '../integrations/providers/access.js';
37
38
  import { CredentialRefreshRejectedError, CredentialWithdrawnError, PROVIDER_REGISTRY, chainCapabilityDisagreements, chainPositionName, describeAcceptedMismatch, describeCapabilityRefusal, discoverProviders, ensureFreshAnthropicToken, ensureFreshStoredCodexCredential, ensureRegistered, findDetected, isAnthropicOAuthToken, isRegistered, missingCredentialMessage, primaryProvider, readCodexCredentialFile, readPreferences, readSubscriptionCredential, resolveChainCapabilities, sameOAuthCredential, unresolvedMembers, unsupportedProviderMessage, } from '../integrations/providers/index.js';
39
+ import { modelReasoningView } from '../integrations/providers/model-reasoning.js';
40
+ import { createContextInventoryStep } from '../integrations/sessions/context-inventory.js';
41
+ import { buildConversationReadTool, buildConversationSearchTool, } from '../integrations/sessions/conversation-search.js';
42
+ import { createTaskContextStep } from '../integrations/sessions/task-context.js';
38
43
  import { ensurePrivateStateDirectory } from '../integrations/state/private-directory.js';
39
44
  import { discoverAgentDefinitions } from '../integrations/subagents/definitions.js';
40
45
  import { SubagentPathBuilder, resolveSubagentParent } from '../integrations/subagents/parent.js';
41
46
  import { CLI_INTERACTIVE_RUN_TIMEOUT_MS } from '../integrations/subagents/policy.js';
42
47
  import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
43
48
  import { cliLogger } from '../logging.js';
49
+ import { formatMemoryDiagnostics } from '../memory/presentation.js';
44
50
  import { composeMemoryPrompt, readMemory } from '../memory/store.js';
45
51
  import { projectRunConversation } from './conversation-history.js';
52
+ import { buildSwitchModelTool } from './model-switch-tool.js';
46
53
  /**
47
54
  * Let one caller stop waiting without cutting a shared queue in the middle.
48
55
  *
@@ -303,10 +310,12 @@ function credentialGap(prefs, detected) {
303
310
  // one ever did, it is not a credential problem and must not be reported as
304
311
  // one — a wrong diagnosis sends the operator to paste a key that would not
305
312
  // have helped.
306
- if (!entry || !entry.constructible || !entry.requiresApiKey)
313
+ if (!entry ||
314
+ !entry.constructible ||
315
+ !requiresCredentialForModel(entry, primary.model ?? entry.defaultModel))
307
316
  return null;
308
317
  const det = findDetected(detected, primary.id);
309
- if (det?.apiKey)
318
+ if (hasApiCredential(entry, det?.apiKey))
310
319
  return null;
311
320
  return { providerId: primary.id, reason: missingCredentialMessage(entry) };
312
321
  }
@@ -363,9 +372,12 @@ function foregroundOnlyBash(tool) {
363
372
  };
364
373
  }
365
374
  function builtinTools(backgroundJobs) {
366
- return getBuiltinTools().flatMap((tool) => {
367
- if (EXCLUDED_BUILTINS.has(tool.name))
375
+ return getBuiltinTools().flatMap((source) => {
376
+ if (EXCLUDED_BUILTINS.has(source.name))
368
377
  return [];
378
+ const tool = ['bash', 'write', 'edit'].includes(source.name)
379
+ ? { ...source, executionBarrier: true }
380
+ : source;
369
381
  if (backgroundJobs)
370
382
  return [tool];
371
383
  if (tool.name === 'job')
@@ -399,14 +411,8 @@ function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgrou
399
411
  // handing `getIndex()` to the synchronous overload before the first store
400
412
  // read makes a new process report every persisted memory as absent.
401
413
  registry.register(buildMemoryTools(memoryStore));
402
- // `search_tools` is deliberately NOT registered here. It is only useful
403
- // where deferred tools exist, and that differs between this function's two
404
- // callers: the session below passes a `taskStore`, so query() registers the
405
- // task tools deferred and the search has a roster; a sub-agent is built with
406
- // no task store, so its registry has nothing deferred and the tool could
407
- // only ever answer "no deferred tools matching X" — a capability advertised
408
- // every turn that costs a turn to discover is unusable. Mounting it is the
409
- // caller's decision, made where the roster is known.
414
+ // query() mounts search_tools only if a deferred roster actually exists,
415
+ // after runtime tools are registered. Ordinary CLI task tools are active.
410
416
  return { registry, memoryStore };
411
417
  }
412
418
  /** Refresh plugin skill metadata before each provider operation. */
@@ -464,7 +470,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
464
470
  return emptySession(`Unknown provider "${primary.id}" — pick another.`, 'invocation');
465
471
  }
466
472
  const det = findDetected(detected, primary.id);
467
- if (entry.requiresApiKey && (!det || !det.apiKey)) {
473
+ if (requiresCredentialForModel(entry, primary.model ?? entry.defaultModel) &&
474
+ !hasApiCredential(entry, det?.apiKey)) {
468
475
  // The BACKSTOP, not the operator-facing answer. The TUI never reaches this
469
476
  // line any more: `probeAgentSession` reports the same gap as a
470
477
  // `credentialGap` and the App routes into the picker, where a credential
@@ -473,7 +480,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
473
480
  // pieces of advice below are real: an environment variable, or
474
481
  // `--provider`. Keeping the refusal is what makes those runs fail rather
475
482
  // than quietly start on something else.
476
- return emptySession(`No credential found for ${entry.label}. Set one of: ${entry.envVars.join(', ')} — or pass --provider with one that is configured.`);
483
+ return emptySession(`No credential found for ${entry.label}${entry.id === 'zen' ? ' with the selected model. Choose muse-spark-1.3-contributor-free for public access' : ''}. Set one of: ${entry.envVars.join(', ')} — or pass --provider with one that is configured.`);
477
484
  }
478
485
  try {
479
486
  await ensureRegistered(primary.id);
@@ -523,7 +530,9 @@ export async function createAgentSession(prefs, detected, options = {}) {
523
530
  }
524
531
  let provider;
525
532
  try {
526
- provider = constructProvider(primary.id, det, model);
533
+ provider = constructProvider(primary.id, det, model, {
534
+ sessionId: scope.sessionId,
535
+ });
527
536
  }
528
537
  catch (err) {
529
538
  return emptySession(`Failed to construct ${entry.label}: ${err instanceof Error ? err.message : String(err)}`);
@@ -597,7 +606,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
597
606
  if (fresh === currentToken)
598
607
  return;
599
608
  try {
600
- const refreshedProvider = constructProvider('anthropic', { ...det, apiKey: fresh }, model);
609
+ const refreshedProvider = constructProvider('anthropic', { ...det, apiKey: fresh }, model, { sessionId: scope.sessionId });
601
610
  // Publish the pair together. If construction fails, both old values
602
611
  // remain live and the next operation can retry against the stored token.
603
612
  provider = refreshedProvider;
@@ -634,7 +643,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
634
643
  expiresAt: credential.expiresAt,
635
644
  origin: 'codex-file',
636
645
  },
637
- }, model);
646
+ }, model, { sessionId: scope.sessionId });
638
647
  provider = refreshedProvider;
639
648
  currentToken = credential.accessToken;
640
649
  currentCodexAccount = credential.accountId;
@@ -652,7 +661,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
652
661
  }
653
662
  if (credential.accessToken === currentToken)
654
663
  return;
655
- provider = constructProvider('anthropic', { ...det, apiKey: credential.accessToken }, model);
664
+ provider = constructProvider('anthropic', { ...det, apiKey: credential.accessToken }, model, { sessionId: scope.sessionId });
656
665
  currentToken = credential.accessToken;
657
666
  };
658
667
  const refreshStoredCodex = async (signal) => {
@@ -668,7 +677,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
668
677
  expiresAt: credential.expiresAt,
669
678
  origin: 'stored',
670
679
  },
671
- }, model);
680
+ }, model, { sessionId: scope.sessionId });
672
681
  provider = refreshedProvider;
673
682
  currentToken = credential.accessToken;
674
683
  currentCodexAccount = credential.accountId;
@@ -695,6 +704,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
695
704
  credentialTail = queued.catch(() => { });
696
705
  return signal ? observeWithSignal(queued, signal) : queued;
697
706
  };
707
+ // The TUI can replace its conversation without replacing this session object.
708
+ // Bind each admitted run to its captured conversation, including durable resumes;
709
+ // a Zen client must never generate a fresh Go session for each model call.
710
+ const providerForSession = (sessionId) => primary.id === 'zen' || primary.id === 'zen-go'
711
+ ? constructProvider(primary.id, det, model, { sessionId })
712
+ : provider;
698
713
  // Session-owned discovery with one drain cursor per run. A child shares the
699
714
  // discovered scopes without being able to consume the parent's update, and
700
715
  // an edit takes effect in this session rather than only after reconnecting.
@@ -874,9 +889,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
874
889
  computerUseReady: computerUseHost !== undefined,
875
890
  ...(computerUseError ? { computerUseError } : {}),
876
891
  });
877
- // This session passes a `taskStore` to query() below, which registers the
878
- // task tools deferred — so `search_tools` has something to find here.
879
- registry.register([SearchToolsTool]);
880
892
  // Web reach, opted into in the config file and nowhere else. The parent's
881
893
  // registry only: a child's config carries no provider, and a tool whose
882
894
  // provider is missing is a tool that reports itself unwired — truthful,
@@ -891,6 +903,18 @@ export async function createAgentSession(prefs, detected, options = {}) {
891
903
  // Best-effort — if the runtime can't stand up, the chat still works.
892
904
  const delegationScopes = new Map();
893
905
  const delegatedInputWaiters = new Map();
906
+ if (options.conversationSessions) {
907
+ const sessions = options.conversationSessions;
908
+ for (const build of [buildConversationSearchTool, buildConversationReadTool])
909
+ registry.register(build((context) => {
910
+ const owner = delegationScopes.get(context.runId);
911
+ if (!owner ||
912
+ owner.projectId !== sessions.projectId ||
913
+ owner.tenantId !== sessions.tenantId)
914
+ throw new Error('The requesting run does not own this conversation.');
915
+ return { sessions, sessionId: owner.sessionId };
916
+ }));
917
+ }
894
918
  let subagentRuntime;
895
919
  // Stays empty when the runtime below throws, which is the honest answer: the
896
920
  // catch is non-fatal and the session then genuinely has no delegate to
@@ -950,7 +974,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
950
974
  // child announcing a swap the parent never made, inside a tool result,
951
975
  // is a worse surface than the child simply failing and the parent
952
976
  // reporting it.
953
- buildProvider: () => constructProvider(primary.id, det ? { ...det, apiKey: currentToken ?? det.apiKey } : det, model),
977
+ buildProvider: (sessionId) => {
978
+ if (!sessionId && (primary.id === 'zen' || primary.id === 'zen-go')) {
979
+ throw new Error('A delegated provider requires its invoking conversation.');
980
+ }
981
+ return constructProvider(primary.id, det ? { ...det, apiKey: currentToken ?? det.apiKey } : det, model, { sessionId });
982
+ },
954
983
  buildTools: () => {
955
984
  // Sub-agents get the parent's working set minus `search_tools`:
956
985
  // they run without a task store, so nothing in their registry is
@@ -967,6 +996,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
967
996
  });
968
997
  subagentRuntime = sub;
969
998
  registry.register([sub.agentTool, sub.waitForTaskTool]);
999
+ if (sub.sendMessageTool)
1000
+ registry.register(sub.sendMessageTool);
970
1001
  allowedAgentIds = sub.allowedAgentIds;
971
1002
  }
972
1003
  catch (err) {
@@ -980,6 +1011,20 @@ export async function createAgentSession(prefs, detected, options = {}) {
980
1011
  // and got none had nothing on stderr to say why.
981
1012
  cliLogger().warn('sub-agent runtime unavailable this session', exceptionAttributes(err));
982
1013
  }
1014
+ // This capability belongs to the active main turn, never the child roster.
1015
+ const modelSwitchHandlers = new Map();
1016
+ if (options.allowModelSwitch) {
1017
+ registry.register(buildSwitchModelTool(async (request, context) => {
1018
+ const handler = modelSwitchHandlers.get(context.runId);
1019
+ if (!handler || context.abortSignal?.aborted) {
1020
+ return {
1021
+ kind: 'rejected',
1022
+ reason: 'This turn no longer owns model selection.',
1023
+ };
1024
+ }
1025
+ return handler(request, context.abortSignal);
1026
+ }));
1027
+ }
983
1028
  // `ask_user_question`, where somebody can answer. The SDK tool parks the
984
1029
  // run through the handler it was BUILT with, so that handler reads the
985
1030
  // turn's answerer through a holder the prelude fills: the tool is per
@@ -1021,8 +1066,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
1021
1066
  // query call, because the doctrine tells the model to plan with them and a
1022
1067
  // tool it must search for first is a tool it skips.
1023
1068
  //
1024
- // `search_tools` stays mounted above even so: a tool server or plugin can
1025
- // still register a deferred roster, and that is what the search is for. The
1069
+ // A tool server or plugin can still register a deferred roster; query()
1070
+ // mounts search_tools when one exists. Do not advertise an empty search. The
1026
1071
  // task tools are registered inside query(), after this function returns,
1027
1072
  // which is why the connect line reports no count of them — counting here
1028
1073
  // would mean restating query's registration order in the CLI.
@@ -1067,16 +1112,9 @@ export async function createAgentSession(prefs, detected, options = {}) {
1067
1112
  // Persists across turns: once the user picks "approve all", later tool
1068
1113
  // batches in this session run without prompting.
1069
1114
  const approval = { all: false };
1070
- // What a settled run leaves behind, over the SAME store `search_memory`
1071
- // reads on the next run. Built once per session rather than per turn: it
1072
- // holds no per-run state, and a per-turn construction would re-open the
1073
- // index for every message.
1074
- //
1075
- // Unconditional, unlike the answer gate. A gate changes what a run may do
1076
- // and so must be asked for; promotion changes only what survives it, and
1077
- // the alternative — the run's own extracted knowledge being discarded at
1078
- // settle — is what this repository has been doing all along by accident.
1079
- // A run that learned nothing still writes nothing.
1115
+ // Share the project store with tools and recall. Each run selects either
1116
+ // this extracted-claim promoter or explicit consolidation, never both.
1117
+ // Candidates without useful claims write nothing.
1080
1118
  const promoteMemory = createMemoryPromoter({ store: memoryStore });
1081
1119
  // Plugins are the last fallible startup resource. The ordering is ownership:
1082
1120
  // a malformed MCP entry cannot strand imported plugin hooks, and a plugin
@@ -1155,10 +1193,25 @@ export async function createAgentSession(prefs, detected, options = {}) {
1155
1193
  let reasoningEffortDefault;
1156
1194
  let effortNotice;
1157
1195
  try {
1158
- const capabilityView = withProviderFallback([
1196
+ const capabilityMembers = [
1159
1197
  { provider, model },
1160
- ...fallbackPlan.build(currentToken),
1161
- ]);
1198
+ ...fallbackPlan.build(currentToken, scope.sessionId),
1199
+ ];
1200
+ const capabilityView = withProviderFallback(await Promise.all(capabilityMembers.map(async (member) => {
1201
+ const memberModel = member.model ?? model;
1202
+ const known = member.provider.reasoningEffortLevelsFor
1203
+ ? member.provider.reasoningEffortLevelsFor(memberModel)
1204
+ : member.provider.effortLevelsFor?.(memberModel);
1205
+ // A model-owned answer (including []) needs no extra catalogue
1206
+ // request. Discover only missing capability information.
1207
+ const catalogue = known === undefined && member.provider.listModels
1208
+ ? await runPickerProviderOperation(undefined, (signal) => member.provider.listModels?.(signal) ?? Promise.resolve([])).catch(() => [])
1209
+ : [];
1210
+ return {
1211
+ ...member,
1212
+ provider: modelReasoningView(member.provider, memberModel, catalogue),
1213
+ };
1214
+ })));
1162
1215
  const offered = capabilityView.reasoningEffortLevelsFor
1163
1216
  ? capabilityView.reasoningEffortLevelsFor(model)
1164
1217
  : capabilityView.effortLevelsFor?.(model);
@@ -1199,7 +1252,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
1199
1252
  const pluginSkills = pluginRuntime
1200
1253
  ? await currentPluginSkills(pluginRuntime.skills)
1201
1254
  : undefined;
1202
- const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
1255
+ const curatedMemory = readMemory(undefined, cwd);
1256
+ for (const notice of formatMemoryDiagnostics(curatedMemory))
1257
+ cliLogger().warn(notice);
1258
+ const memoryPrompt = composeMemoryPrompt(curatedMemory);
1203
1259
  const environmentPrompt = composeEnvironmentPrompt({
1204
1260
  ...(await readEnvironmentFacts(cwd)),
1205
1261
  additionalDirectories: [...directories],
@@ -1219,15 +1275,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
1219
1275
  }
1220
1276
  delegatedResumeHandlers.set(entry.runId, resumeHandler);
1221
1277
  delegationScopes.set(entry.runId, { ...entry, topicId: scope.topicId });
1278
+ const runTaskStore = selectTaskStore(entry.runId, { ...entry, topicId: scope.topicId });
1222
1279
  try {
1223
1280
  return await resumeRun({
1224
- provider,
1225
- fallbackProviders: fallbackPlan.build(currentToken),
1281
+ provider: providerForSession(entry.sessionId),
1282
+ fallbackProviders: fallbackPlan.build(currentToken, entry.sessionId),
1226
1283
  tools: registry,
1227
1284
  pluginManager: pluginRuntime?.manager,
1228
1285
  skillRegistry: pluginRuntime?.skills,
1229
1286
  skills: pluginSkills,
1230
- taskStore: selectTaskStore(entry.runId, { ...entry, topicId: scope.topicId }),
1287
+ taskStore: runTaskStore,
1231
1288
  // The same availability the original run registered under.
1232
1289
  // A resumed run re-registers the task tools; leaving them at
1233
1290
  // the kernel's `deferred` default would hand the model a plan
@@ -1244,6 +1301,21 @@ export async function createAgentSession(prefs, detected, options = {}) {
1244
1301
  : {}),
1245
1302
  authorizationGate: gateFor(options.rules),
1246
1303
  compactionConfig: compactionConfigFor(options.compaction),
1304
+ prepareStep: [
1305
+ createTaskContextStep(runTaskStore, entry.tenantId),
1306
+ ...(options.memory?.recall === false
1307
+ ? []
1308
+ : [
1309
+ createMemoryRecallStep({
1310
+ store: memoryStore,
1311
+ identifierGrounding: options.memory?.identifierGrounding,
1312
+ }),
1313
+ ]),
1314
+ ...(options.conversationSessions ? [createContextInventoryStep()] : []),
1315
+ ],
1316
+ ...(options.compaction?.consolidate
1317
+ ? { consolidateInto: memoryStore }
1318
+ : { promoteMemory }),
1247
1319
  projectInstructionContext: projectInstructions.createRunContext(),
1248
1320
  pathBuilder,
1249
1321
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
@@ -1391,11 +1463,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
1391
1463
  reasoningEffortLevels,
1392
1464
  reasoningEffortDefault,
1393
1465
  compact: (messages) => operations.promise(undefined, async (signal) => {
1466
+ const sessionId = scope.sessionId;
1394
1467
  await prepareProviderCredential(signal);
1395
1468
  return compactNow({
1396
1469
  messages,
1397
1470
  config: compactionConfigFor(options.compaction),
1398
- provider,
1471
+ provider: providerForSession(sessionId),
1399
1472
  model,
1400
1473
  signal,
1401
1474
  });
@@ -1459,6 +1532,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
1459
1532
  throw new Error(`Run ${runId} already owns a delegated review channel.`);
1460
1533
  }
1461
1534
  delegatedResumeHandlers.set(runId, resumeHandler);
1535
+ if (opts?.onModelSwitch)
1536
+ modelSwitchHandlers.set(runId, opts.onModelSwitch);
1462
1537
  if (opts?.waitForInbound)
1463
1538
  delegatedInputWaiters.set(runId, opts.waitForInbound);
1464
1539
  const turnScope = { ...scope };
@@ -1481,7 +1556,11 @@ export async function createAgentSession(prefs, detected, options = {}) {
1481
1556
  const pluginSkills = pluginRuntime
1482
1557
  ? await currentPluginSkills(pluginRuntime.skills)
1483
1558
  : undefined;
1484
- const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
1559
+ const curatedMemory = readMemory(undefined, cwd);
1560
+ for (const notice of formatMemoryDiagnostics(curatedMemory)) {
1561
+ yield { kind: 'context', text: notice, shed: false };
1562
+ }
1563
+ const memoryPrompt = composeMemoryPrompt(curatedMemory);
1485
1564
  currentOnQuestion = opts?.onQuestion;
1486
1565
  const [environmentFacts, turnSnapshot] = await Promise.all([
1487
1566
  readEnvironmentFacts(cwd),
@@ -1553,9 +1632,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
1553
1632
  capturedAuthority = Object.freeze({ ...opts.goalRound });
1554
1633
  goalAuthorities.set(opts.runId, capturedAuthority);
1555
1634
  }
1635
+ const runTaskStore = selectTaskStore(runId, turnScope);
1556
1636
  try {
1557
1637
  yield* runTurn({
1558
- provider,
1638
+ provider: providerForSession(turnScope.sessionId),
1559
1639
  compactionConfig: compactionConfigFor(options.compaction),
1560
1640
  ...(options.compaction?.consolidate ? { consolidateInto: memoryStore } : {}),
1561
1641
  ...(jobRegistry
@@ -1571,7 +1651,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1571
1651
  // that expired hours ago — and a chain whose own members are stale
1572
1652
  // is a fallback that fails for the reason the fallback exists to
1573
1653
  // survive. Building a driver is a client object, not a request.
1574
- fallbackProviders: fallbackPlan.build(currentToken),
1654
+ fallbackProviders: fallbackPlan.build(currentToken, turnScope.sessionId),
1575
1655
  model,
1576
1656
  tools: registry,
1577
1657
  pluginManager: pluginRuntime?.manager,
@@ -1584,10 +1664,24 @@ export async function createAgentSession(prefs, detected, options = {}) {
1584
1664
  ...(options.limits ? { limits: options.limits } : {}),
1585
1665
  sandboxWorkspace,
1586
1666
  rules: options.rules,
1667
+ structuredOutput: options.structuredOutput,
1587
1668
  reviewAnswer: options.reviewAnswer,
1588
1669
  maxAnswerReviews: options.maxAnswerReviews,
1589
- promoteMemory,
1590
- taskStore: selectTaskStore(runId, turnScope),
1670
+ promoteMemory: options.compaction?.consolidate ? undefined : promoteMemory,
1671
+ prepareStep: [
1672
+ createTaskContextStep(runTaskStore, turnScope.tenantId),
1673
+ ...(options.memory?.recall === false
1674
+ ? []
1675
+ : [
1676
+ createMemoryRecallStep({
1677
+ store: memoryStore,
1678
+ query: lastUserText(messages),
1679
+ identifierGrounding: options.memory?.identifierGrounding,
1680
+ }),
1681
+ ]),
1682
+ ...(options.conversationSessions ? [createContextInventoryStep()] : []),
1683
+ ],
1684
+ taskStore: runTaskStore,
1591
1685
  systemPrompt,
1592
1686
  messages,
1593
1687
  projectInstructionContext: projectInstructions.createRunContext(),
@@ -1625,6 +1719,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1625
1719
  finally {
1626
1720
  if (delegatedResumeHandlers.get(runId) === resumeHandler) {
1627
1721
  delegatedResumeHandlers.delete(runId);
1722
+ modelSwitchHandlers.delete(runId);
1628
1723
  delegatedInputWaiters.delete(runId);
1629
1724
  delegationScopes.delete(runId);
1630
1725
  await subagentRuntime?.releaseRun(runId);
@@ -1656,7 +1751,8 @@ function planFallbacks(members, detected) {
1656
1751
  continue;
1657
1752
  }
1658
1753
  const det = findDetected(detected, member.id);
1659
- if (entry.requiresApiKey && !det?.apiKey) {
1754
+ if (requiresCredentialForModel(entry, member.model ?? entry.defaultModel) &&
1755
+ !hasApiCredential(entry, det?.apiKey)) {
1660
1756
  notices.push(`Provider chain: ${position} (${entry.label}) has no credential, so nothing will fall over to it. ` +
1661
1757
  `Set one of: ${entry.envVars.join(', ')}.`);
1662
1758
  continue;
@@ -1671,7 +1767,7 @@ function planFallbacks(members, detected) {
1671
1767
  }
1672
1768
  return {
1673
1769
  notices,
1674
- build(headToken) {
1770
+ build(headToken, sessionId) {
1675
1771
  const out = [];
1676
1772
  for (const { choice, det } of usable) {
1677
1773
  const entry = PROVIDER_REGISTRY[choice.id];
@@ -1681,7 +1777,9 @@ function planFallbacks(members, detected) {
1681
1777
  try {
1682
1778
  const credential = headToken !== undefined && det?.oauth ? { ...det, apiKey: headToken } : det;
1683
1779
  out.push({
1684
- provider: constructProvider(choice.id, credential, memberModel),
1780
+ provider: constructProvider(choice.id, credential, memberModel, {
1781
+ sessionId,
1782
+ }),
1685
1783
  model: memberModel,
1686
1784
  });
1687
1785
  }
@@ -1697,7 +1795,7 @@ function planFallbacks(members, detected) {
1697
1795
  },
1698
1796
  };
1699
1797
  }
1700
- export function constructProvider(id, det, model) {
1798
+ export function constructProvider(id, det, model, context = {}) {
1701
1799
  switch (id) {
1702
1800
  case 'anthropic': {
1703
1801
  const token = det?.apiKey ?? '';
@@ -1749,6 +1847,24 @@ export function constructProvider(id, det, model) {
1749
1847
  });
1750
1848
  return provider;
1751
1849
  }
1850
+ case 'zen':
1851
+ case 'zen-go': {
1852
+ const apiKey = hasApiCredential(PROVIDER_REGISTRY[id], det?.apiKey) ? det?.apiKey : undefined;
1853
+ if (id === 'zen-go' && apiKey === undefined)
1854
+ throw new Error('Zen Go requires an API key.');
1855
+ const { provider } = ProviderRegistry.create({
1856
+ ...(id === 'zen-go'
1857
+ ? { type: 'zen-go', apiKey: apiKey }
1858
+ : {
1859
+ type: 'zen',
1860
+ ...(apiKey === undefined ? {} : { apiKey }),
1861
+ }),
1862
+ baseURL: det?.baseUrl,
1863
+ model,
1864
+ ...(context.sessionId ? { sessionId: context.sessionId } : {}),
1865
+ });
1866
+ return provider;
1867
+ }
1752
1868
  case 'ollama': {
1753
1869
  const { provider } = ProviderRegistry.create({
1754
1870
  type: 'ollama',
@@ -2013,7 +2129,7 @@ function compactionConfigFor(compaction) {
2013
2129
  : {}),
2014
2130
  };
2015
2131
  }
2016
- async function* runTurn({ provider, compactionConfig, consolidateInto, backgroundJobs, backgroundJobOwner, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, limits, additionalDirectories, sandboxWorkspace, rules, reviewAnswer, maxAnswerReviews, promoteMemory, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, completionInbox, promptContributions, runtimeToolOverrides, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
2132
+ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroundJobs, backgroundJobOwner, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, limits, additionalDirectories, sandboxWorkspace, rules, structuredOutput, reviewAnswer, maxAnswerReviews, promoteMemory, prepareStep, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, completionInbox, promptContributions, runtimeToolOverrides, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
2017
2133
  const signal = opts?.signal;
2018
2134
  // One presenter for the whole stream, built from the registry this scope
2019
2135
  // already holds. Its absence HERE is what forced presentation to be name
@@ -2022,6 +2138,7 @@ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroun
2022
2138
  const presenter = createToolPresenter(tools);
2023
2139
  try {
2024
2140
  const events = query({
2141
+ ...(structuredOutput ? { structuredOutput } : {}),
2025
2142
  provider,
2026
2143
  pathBuilder,
2027
2144
  ...(opts?.runId ? { runId: opts.runId } : {}),
@@ -2068,11 +2185,9 @@ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroun
2068
2185
  // that shipped before gates existed.
2069
2186
  ...(reviewAnswer ? { reviewAnswer } : {}),
2070
2187
  ...(maxAnswerReviews !== undefined ? { maxAnswerReviews } : {}),
2071
- // Always present, unlike the gate: a gate changes what a run may
2072
- // do and so must be asked for; promotion changes only what
2073
- // survives it, and a run that learned nothing still writes
2074
- // nothing.
2188
+ // Undefined when the host selected consolidation as its writer.
2075
2189
  promoteMemory,
2190
+ ...(prepareStep ? { prepareStep } : {}),
2076
2191
  agentId: 'namzu',
2077
2192
  agentName: 'namzu',
2078
2193
  ...(systemPrompt ? { systemPrompt } : {}),
@@ -2347,6 +2462,7 @@ export function toAgentEvent(event, presenter) {
2347
2462
  // answer was refused.
2348
2463
  return {
2349
2464
  kind: 'done',
2465
+ text: event.result,
2350
2466
  ...(event.budget ? { budget: event.budget } : {}),
2351
2467
  ...(event.stopReason ? { stopReason: event.stopReason } : {}),
2352
2468
  };