newmark-agent 0.5.14 → 0.6.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 (68) hide show
  1. package/dist/conversation-utility-host.bundle.cjs +4565 -2998
  2. package/dist/conversation-utility-host.js +1 -1
  3. package/dist/core/agent.d.ts +108 -16
  4. package/dist/core/agent.js +698 -195
  5. package/dist/core/agentKernel/agent.d.ts +1 -0
  6. package/dist/core/agentKernel/agent.js +9 -0
  7. package/dist/core/agentKernelDiagnostics.d.ts +4 -6
  8. package/dist/core/agentKernelDiagnostics.js +12 -9
  9. package/dist/core/agentKernelRunner.d.ts +5 -0
  10. package/dist/core/agentKernelRunner.js +250 -107
  11. package/dist/core/autoRouter.js +17 -27
  12. package/dist/core/config.d.ts +2 -0
  13. package/dist/core/config.js +7 -1
  14. package/dist/core/conversationCommandState.d.ts +17 -0
  15. package/dist/core/conversationCommandState.js +140 -0
  16. package/dist/core/conversationKernel.d.ts +25 -4
  17. package/dist/core/conversationKernel.js +360 -89
  18. package/dist/core/conversationListEvent.d.ts +6 -0
  19. package/dist/core/conversationListEvent.js +14 -0
  20. package/dist/core/electronUtilityAgentClient.d.ts +4 -1
  21. package/dist/core/electronUtilityAgentClient.js +4 -4
  22. package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
  23. package/dist/core/electronUtilityRuntimePool.js +11 -5
  24. package/dist/core/installUpdate.js +41 -29
  25. package/dist/core/modelResponseHealth.d.ts +17 -0
  26. package/dist/core/modelResponseHealth.js +193 -0
  27. package/dist/core/providerUsageAccounting.d.ts +47 -0
  28. package/dist/core/providerUsageAccounting.js +71 -0
  29. package/dist/core/requestContextEstimate.d.ts +18 -0
  30. package/dist/core/requestContextEstimate.js +54 -0
  31. package/dist/core/subagent.d.ts +89 -5
  32. package/dist/core/subagent.js +264 -41
  33. package/dist/core/subagentCommunication.d.ts +43 -0
  34. package/dist/core/subagentCommunication.js +167 -0
  35. package/dist/core/types.d.ts +34 -1
  36. package/dist/core/utilityAgentProtocol.d.ts +4 -0
  37. package/dist/core/workEventCoalescer.js +4 -1
  38. package/dist/core/wslAgentClient.d.ts +18 -5
  39. package/dist/core/wslAgentClient.js +79 -27
  40. package/dist/core/wslAgentProtocol.d.ts +7 -0
  41. package/dist/core/wslAgentRuntimePool.d.ts +8 -2
  42. package/dist/core/wslAgentRuntimePool.js +19 -6
  43. package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
  44. package/dist/core/wslRuntimeProcessTree.js +104 -0
  45. package/dist/llm/provider.d.ts +5 -12
  46. package/dist/llm/provider.js +150 -269
  47. package/dist/main.js +582 -356
  48. package/dist/preload.js +3 -3
  49. package/dist/providers/chat-completions.adapter.d.ts +2 -0
  50. package/dist/providers/chat-completions.adapter.js +84 -36
  51. package/dist/providers/provider-adapter.d.ts +2 -0
  52. package/dist/providers/provider-events.d.ts +23 -14
  53. package/dist/providers/provider-events.js +148 -43
  54. package/dist/providers/provider-headers.d.ts +7 -3
  55. package/dist/providers/provider-headers.js +109 -39
  56. package/dist/providers/provider-request-compat.d.ts +6 -0
  57. package/dist/providers/provider-request-compat.js +37 -0
  58. package/dist/providers/responses.adapter.d.ts +2 -0
  59. package/dist/providers/responses.adapter.js +192 -126
  60. package/dist/server.d.ts +9 -2
  61. package/dist/server.js +100 -24
  62. package/dist/tools/index.js +17 -1
  63. package/dist/ui/index.html +2515 -693
  64. package/dist/ui/lucide-sprite.svg +7 -0
  65. package/dist/ui/startup.html +6 -6
  66. package/dist/wsl-agent-host.bundle.cjs +4575 -3006
  67. package/dist/wsl-agent-host.js +9 -4
  68. package/package.json +23 -4
@@ -42,6 +42,7 @@ exports.toolResultObjectiveOutcome = toolResultObjectiveOutcome;
42
42
  const fs = __importStar(require("fs"));
43
43
  const path = __importStar(require("path"));
44
44
  const terminalTakeover_1 = require("../tools/terminalTakeover");
45
+ const autoRouter_1 = require("./autoRouter");
45
46
  const toolPolicy_1 = require("./toolPolicy");
46
47
  const performanceDiagnostics_1 = require("./performanceDiagnostics");
47
48
  const agentKernelDiagnostics_1 = require("./agentKernelDiagnostics");
@@ -280,13 +281,17 @@ function throwIfKernelAborted(signal) {
280
281
  throw error;
281
282
  }
282
283
  async function runAgentKernel(agent) {
284
+ agent.acknowledgeSubagentSettlementReceipts();
283
285
  const stopContextTimer = (0, performanceDiagnostics_1.performanceTimer)('context_prepare', { conversationId: agent.activeConversationId });
284
286
  const processSignal = agent.activeProcessSignal();
285
287
  if (processSignal?.aborted) {
286
288
  stopContextTimer();
287
289
  throwIfKernelAborted(processSignal);
288
290
  }
289
- if (!agent.engineModel()) {
291
+ // One configuration-checked provider slot per Build: retain compatibility
292
+ // learning and connection pools without sharing them with another Build.
293
+ const buildProviderCache = {};
294
+ if (!agent.engineModel(buildProviderCache)) {
290
295
  const message = 'No LLM configured. Add provider in Settings > Models.';
291
296
  agent.status = 'error';
292
297
  agent.saveWorkspaceConversationState();
@@ -332,15 +337,25 @@ async function runAgentKernel(agent) {
332
337
  const initialToolSurface = refreshToolSurface(true);
333
338
  const assembledContext = agent.assembleContextV2(initialToolSurface.systemPromptNotice);
334
339
  const systemPrompt = assembledContext.text;
340
+ const peerCacheIdentity = agent.peerRequestCacheIdentity(systemPrompt, agent.cachedToolDefinitions());
341
+ const peerRequestCache = agent.readPeerRequestCache(peerCacheIdentity);
342
+ if (peerRequestCache)
343
+ toolProvisioning.restore(peerRequestCache.initialTools, peerRequestCache.provisionedTools);
335
344
  throwIfKernelAborted(processSignal);
336
- let providerRequestCount = 0;
337
- let bootstrappedCompressionAt = agent.lastCompression?.at || '';
345
+ // Capture request-only metadata once for this Build. Removing it after the
346
+ // first tool call rewrites the system prefix before all retained messages.
347
+ let buildTaskFocusSnapshot = peerRequestCache?.taskFocus;
348
+ let peerInputCheckpointed = false;
349
+ let lastRequestReplaySafe = true;
350
+ // A completed useful provider turn starts a new request recovery budget.
351
+ // Empty/thinking-only replies and failures never reset that budget.
352
+ let requestProgressGeneration = 0;
338
353
  stopContextTimer();
339
354
  const kernel = new NativeAgent({
340
355
  streamFn: streamWithNewmarkProvider(agent, KernelStreamCompat),
341
356
  toolExecution: 'parallel',
342
357
  convertToLlm: (messages) => messages.filter(m => m.role === 'user' || m.role === 'assistant' || m.role === 'toolResult'),
343
- transformContext: async (messages, signal) => transformContext(agent, messages, signal),
358
+ transformContext: async (messages, signal) => transformContext(agent, messages, signal, buildProviderCache),
344
359
  resolveTools: () => {
345
360
  const definitions = refreshToolSurface().definitions;
346
361
  prepareAssistantToolVisibility(agent, definitions);
@@ -350,9 +365,10 @@ async function runAgentKernel(agent) {
350
365
  });
351
366
  kernel.state.systemPrompt = systemPrompt;
352
367
  kernel.state.model = toKernelModel(agent);
353
- kernel.state.tools = toKernelTools(agent, initialToolSurface.definitions, toolProvisioning);
368
+ kernel.state.tools = toKernelTools(agent, refreshToolSurface().definitions, toolProvisioning);
354
369
  kernel.state.messages = toKernelMessages(agent);
355
370
  agent.attachAgentKernelRuntime(kernel);
371
+ agent.flushDirectRootInbox();
356
372
  let detachProcessAbort = () => { };
357
373
  if (processSignal) {
358
374
  const abortKernel = () => kernel.abort();
@@ -381,6 +397,13 @@ async function runAgentKernel(agent) {
381
397
  let observedActivity = false;
382
398
  let observedThought = false;
383
399
  const unsubscribe = kernel.subscribe(async (event) => {
400
+ if (event.type === 'message_start' && event.message.role === 'assistant') {
401
+ // A single kernel.prompt can execute several tools before its final
402
+ // provider reply. Earlier tool progress cannot hide a later empty one.
403
+ observedActivity = false;
404
+ observedThought = false;
405
+ lastAssistant = null;
406
+ }
384
407
  await handleKernelEvent(agent, event, tokens);
385
408
  if (event.type === 'message_update') {
386
409
  const delta = event.assistantMessageEvent;
@@ -412,15 +435,17 @@ async function runAgentKernel(agent) {
412
435
  const assistant = lastAssistant;
413
436
  const text = assistant ? KernelMessageText(assistant) : '';
414
437
  const hasToolCall = !!assistant?.content?.some(content => content.type === 'toolCall');
438
+ const usableText = !!text.trim() && !agent.isLlmErrorText(text);
415
439
  const emptyResponse = !assistant
416
440
  || (!text.trim() && !hasToolCall && !observedActivity && String(assistant?.stopReason || '') !== 'aborted');
417
441
  return {
418
442
  text: emptyResponse ? '[Error] Provider returned an empty response.' : text,
419
443
  stopReason: String(assistant?.stopReason || ''),
420
444
  errorMessage: String(assistant?.errorMessage || (emptyResponse ? 'Provider returned an empty response.' : '')),
421
- activity: observedActivity || !!text.trim() || hasToolCall,
445
+ activity: observedActivity || usableText || hasToolCall,
422
446
  thoughtOnly: observedThought && !text.trim() && !hasToolCall
423
447
  && !['error', 'aborted'].includes(String(assistant?.stopReason || '')),
448
+ requestReplaySafe: lastRequestReplaySafe,
424
449
  };
425
450
  }
426
451
  finally {
@@ -455,15 +480,18 @@ async function runAgentKernel(agent) {
455
480
  try {
456
481
  const linkedPlanRevisionBeforeRun = agent.getLinkedPlan().revision;
457
482
  const modelBeforeKernelRun = agent.model;
458
- const preflightVisualFallback = !agent.activeModelConfig()?.vision
459
- ? await agent.finalVisualFallback('vision input not supported by the selected model', processSignal)
460
- : null;
461
- let lastTurn = preflightVisualFallback
462
- ? { text: preflightVisualFallback, stopReason: 'stop', errorMessage: '' }
463
- : await runWithCompressionResume([], false);
464
- if (preflightVisualFallback) {
465
- tokens.push({ type: 'text', text: preflightVisualFallback });
466
- agent.recordWorkStatus('Final visual fallback used: local mini OCR plus conservative text correction.');
483
+ let lastTurn = await runWithCompressionResume([], false);
484
+ if (kernelTurnFailed(agent, lastTurn)) {
485
+ const visualFallback = await agent.finalVisualFallback(lastTurn.errorMessage || lastTurn.text, processSignal);
486
+ if (visualFallback) {
487
+ tokens.push({ type: 'text', text: visualFallback });
488
+ agent.emitWorkEvent({ type: 'final_response', content: visualFallback });
489
+ agent.chatMessages.push({ role: 'assistant', content: visualFallback, mode: agent.modeName(), model: agent.model, timestamp: agent.nowLabel(), runId: agent.currentWorkRunId() || undefined });
490
+ agent.history.push({ role: 'assistant', content: visualFallback, run_id: agent.currentWorkRunId() || undefined });
491
+ agent.saveWorkspaceConversationState();
492
+ agent.recordWorkStatus('Final visual fallback used: local mini OCR plus conservative text correction.');
493
+ lastTurn = { ...lastTurn, text: visualFallback, errorMessage: '', stopReason: 'stop' };
494
+ }
467
495
  }
468
496
  if (modelBeforeKernelRun && modelBeforeKernelRun !== agent.model && !tokens.some(t => t.text?.includes('[Model fallback]'))) {
469
497
  const notice = `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.`;
@@ -475,29 +503,84 @@ async function runAgentKernel(agent) {
475
503
  });
476
504
  }
477
505
  let consecutiveEmptyResponses = 0;
506
+ let noProgressGeneration = requestProgressGeneration;
507
+ let routeRetries = 0;
508
+ const transientRetries = new Set();
478
509
  for (;;) {
479
- const emptyResponseState = (0, emptyResponseRetry_1.observeEmptyResponseOutcome)(consecutiveEmptyResponses, providerTurnIsEmpty(lastTurn));
480
- consecutiveEmptyResponses = emptyResponseState.consecutiveEmptyResponses;
481
- if (lastTurn.thoughtOnly) {
482
- removeTrailingThoughtOnlyAssistant(kernel.state.messages);
510
+ throwIfKernelAborted(processSignal);
511
+ if (noProgressGeneration !== requestProgressGeneration) {
512
+ consecutiveEmptyResponses = 0;
513
+ noProgressGeneration = requestProgressGeneration;
514
+ }
515
+ // Every outcome, including one produced by a route retry, passes through
516
+ // the same bounded no-progress recovery. Alternating empty/thought-only
517
+ // responses cannot reset the five-retry allowance.
518
+ if (providerTurnIsEmpty(lastTurn) || lastTurn.thoughtOnly) {
519
+ const emptyResponseState = (0, emptyResponseRetry_1.observeEmptyResponseOutcome)(consecutiveEmptyResponses, true);
520
+ consecutiveEmptyResponses = emptyResponseState.consecutiveEmptyResponses;
521
+ if (!emptyResponseState.retry) {
522
+ throw new ProviderRunError(`Provider returned an empty response or only hidden reasoning for ${consecutiveEmptyResponses} consecutive requests; the recovery limit was reached.`);
523
+ }
524
+ if (lastTurn.thoughtOnly)
525
+ removeTrailingThoughtOnlyAssistant(kernel.state.messages);
526
+ else
527
+ removeTrailingFailedAssistant(agent, kernel.state.messages);
528
+ const retryNumber = consecutiveEmptyResponses;
529
+ const reason = lastTurn.thoughtOnly ? 'only hidden reasoning without an answer or tool call' : 'an empty response';
530
+ const notice = `[Model retry] Provider returned ${reason}; retrying the same deployment (${retryNumber}/${emptyResponseRetry_1.MAX_EMPTY_RESPONSE_RETRIES}) after ${(0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses)}ms.`;
531
+ tokens.push({ type: 'text', text: notice });
532
+ agent.recordWorkStatus(notice);
533
+ await agent.waitForPlannedRouteRetry((0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses));
483
534
  lastTurn = await runWithCompressionResume([], false);
484
535
  continue;
485
536
  }
486
- if (!emptyResponseState.retry)
537
+ if (!kernelTurnFailed(agent, lastTurn) || lastTurn.requestReplaySafe === false)
487
538
  break;
488
- removeTrailingFailedAssistant(agent, kernel.state.messages);
489
- const retryNumber = consecutiveEmptyResponses;
490
- const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${retryNumber}/${emptyResponseRetry_1.MAX_EMPTY_RESPONSE_RETRIES}) after ${(0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses)}ms.`;
491
- tokens.push({ type: 'text', text: notice });
492
- agent.recordWorkStatus(notice);
493
- await agent.waitForPlannedRouteRetry((0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses));
494
- lastTurn = await runWithCompressionResume([], false);
495
- }
496
- let routeRetries = 0;
497
- while (kernelTurnFailed(agent, lastTurn) && routeRetries < 2) {
498
- const previous = agent.switchToFallbackModel(lastTurn.errorMessage || lastTurn.text);
499
- if (!previous)
539
+ const failureText = lastTurn.errorMessage || lastTurn.text;
540
+ const failure = (0, autoRouter_1.classifyRouteFailure)(failureText);
541
+ const deployment = agent.activeDeployment();
542
+ const retryKey = `${requestProgressGeneration}:${deployment?.providerId || ''}:${deployment?.modelId || agent.activeModelName()}`;
543
+ // Retry-After applies to 503/408 as well as 429; the routing classifier
544
+ // currently retains it only for rate limits. Never shorten the server's
545
+ // requested wait to fit our automatic retry budget.
546
+ const retryAfter = failureText.match(/retry[- ]after\s*[:=]?\s*(\d+(?:\.\d+)?)\s*(ms|s|seconds?)?/i);
547
+ const retryAfterMs = failure.retryAfterMs ?? (retryAfter
548
+ ? Number(retryAfter[1]) * ((retryAfter[2] || 's').toLowerCase() === 'ms' ? 1 : 1000) : undefined);
549
+ const retryDelayMs = retryAfterMs ?? 250;
550
+ const retryBudgetMs = Math.max(0, Math.min(5000, agent.lastRouteDecision?.retryBudgetMs ?? 5000));
551
+ const transientFailure = failure.retryable && ['transport', 'timeout', 'server_error', 'rate_limited'].includes(failure.type);
552
+ if (transientFailure && retryAfterMs !== undefined && retryAfterMs > retryBudgetMs)
500
553
  break;
554
+ const safeTransient = transientFailure
555
+ && lastTurn.requestReplaySafe === true && retryDelayMs <= retryBudgetMs;
556
+ const retryCurrentRequest = async () => {
557
+ transientRetries.add(retryKey);
558
+ removeTrailingFailedAssistant(agent, kernel.state.messages);
559
+ const notice = `[Model retry] Temporary provider failure; retrying the current request once on the same deployment after ${retryDelayMs}ms.`;
560
+ tokens.push({ type: 'text', text: notice });
561
+ agent.recordWorkStatus(notice);
562
+ await agent.waitForPlannedRouteRetry(retryDelayMs);
563
+ lastTurn = await runWithCompressionResume([], false);
564
+ };
565
+ // Fixed selections have no Auto retry planner. A completed tool from an
566
+ // earlier subturn does not make the *next*, still-uncommitted request
567
+ // unsafe to retry; its exact existing messages/results remain in place.
568
+ if (agent.model !== 'auto' && safeTransient && !transientRetries.has(retryKey)) {
569
+ await retryCurrentRequest();
570
+ continue;
571
+ }
572
+ const previous = routeRetries < 2 ? agent.switchToFallbackModel(failureText) : null;
573
+ if (!previous) {
574
+ if (agent.model === 'auto' && safeTransient && !transientRetries.has(retryKey)) {
575
+ await retryCurrentRequest();
576
+ continue;
577
+ }
578
+ break;
579
+ }
580
+ // The existing Auto same-deployment attempt consumes the same allowance;
581
+ // our local recovery must not add a second retry after it is exhausted.
582
+ if (agent.routeTransitionKind() === 'retry_same_deployment')
583
+ transientRetries.add(retryKey);
501
584
  removeTrailingFailedAssistant(agent, kernel.state.messages);
502
585
  routeRetries += 1;
503
586
  const notice = routeTransitionNotice(agent, previous);
@@ -509,20 +592,18 @@ async function runAgentKernel(agent) {
509
592
  fallback: { from: previous, to: agent.model, providerId: agent.activeDeployment()?.providerId },
510
593
  });
511
594
  kernel.state.model = toKernelModel(agent);
512
- const fallbackToolSurface = refreshToolSurface(true);
513
- kernel.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join('\n\n');
595
+ const toolSurfaceChanged = toolSurfaceIdentityForAgent(agent) !== activeToolSurfaceIdentity;
596
+ const fallbackToolSurface = refreshToolSurface();
597
+ // A same-deployment retry must retain the original Build system and
598
+ // provisioned schema sequence, even if a Guide changed the latest task.
599
+ // Actual deployment or capability transitions still refresh the surface.
600
+ if (toolSurfaceChanged) {
601
+ kernel.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join('\n\n');
602
+ }
514
603
  kernel.state.tools = toKernelTools(agent, fallbackToolSurface.definitions, toolProvisioning);
515
604
  await agent.waitForPlannedRouteRetry();
516
605
  lastTurn = await runWithCompressionResume([], false);
517
606
  }
518
- if (kernelTurnFailed(agent, lastTurn)) {
519
- const visualFallback = await agent.finalVisualFallback(lastTurn.errorMessage || lastTurn.text, processSignal);
520
- if (visualFallback) {
521
- tokens.push({ type: 'text', text: visualFallback });
522
- agent.recordWorkStatus('Final visual fallback used: local mini OCR plus conservative text correction.');
523
- lastTurn = { ...lastTurn, text: visualFallback, errorMessage: '', stopReason: 'stop' };
524
- }
525
- }
526
607
  if (kernelTurnFailed(agent, lastTurn)) {
527
608
  throw new ProviderRunError(normalizePublicProviderError(lastTurn.errorMessage || lastTurn.text, [agent.activeModelConfig()?.api_key]));
528
609
  }
@@ -569,41 +650,53 @@ async function runAgentKernel(agent) {
569
650
  const tools = context.tools || [];
570
651
  const brokerOnlySurface = tools.length > 0 && tools.every(tool => tool.name === TOOL_PROVISION_NAME || tool.name === 'skill' || ALWAYS_AVAILABLE_AGENT_TOOL_NAMES.has(tool.name));
571
652
  currentAgent.beginRouteAttempt();
653
+ lastRequestReplaySafe = true;
572
654
  try {
573
- const currentProvider = currentAgent.engineModel();
655
+ const currentProvider = currentAgent.engineModel(buildProviderCache);
574
656
  const currentModelName = currentAgent.activeModelName();
575
657
  if (!currentProvider || !currentModelName)
576
658
  throw new Error('No resolved model deployment is available.');
659
+ if (!peerInputCheckpointed) {
660
+ currentAgent.checkpointPeerInput();
661
+ peerInputCheckpointed = true;
662
+ }
577
663
  const { temperature, maxTokens, reasoningEffort } = currentProvider.intelligenceConfig(currentAgent.intelligence);
578
- const newmarkMessages = fromKernelMessages(context.messages);
579
- const currentCompressionAt = currentAgent.lastCompression?.at || '';
580
- const compressionCompleted = !!currentCompressionAt && currentCompressionAt !== bootstrappedCompressionAt;
581
- const includeBootstrap = providerRequestCount === 0 || compressionCompleted;
582
- // Keep the stable base prompt identical across tool sub-turns. The
583
- // request-scoped ledger/bootstrap is needed on the first request
584
- // (and once after compression), but re-injecting it on every round
585
- // makes otherwise cacheable prompt prefixes look like new prompts.
664
+ const newmarkMessages = fromKernelMessages(context.messages).map(message => message.role === 'assistant'
665
+ // Use the same public-content boundary on its first provider
666
+ // submission and after persistence. Trimming only the durable
667
+ // copy changes earlier tool-call envelopes on mailbox recovery.
668
+ ? { ...message, content: currentAgent.sanitizeAssistantOutput(String(message.content || '')) }
669
+ : message);
670
+ // Tool results and Guides append to messages; neither mutable task
671
+ // status nor a growing tool catalog may regenerate this snapshot.
672
+ // Compression replaces messages explicitly, not the stable system.
673
+ buildTaskFocusSnapshot ??= buildRequestTaskFocus(currentAgent, context.messages, {
674
+ activeTools: context.tools || [],
675
+ toolCatalog: currentAgent.cachedToolDefinitions(),
676
+ });
677
+ if (peerCacheIdentity)
678
+ currentAgent.persistPeerRequestCache({
679
+ version: 1, identity: peerCacheIdentity, taskFocus: buildTaskFocusSnapshot,
680
+ ...toolProvisioning.snapshot(),
681
+ });
586
682
  const requestSystemPrompt = [
587
683
  context.systemPrompt || '',
588
- includeBootstrap || compressionCompleted
589
- ? buildRequestTaskFocus(currentAgent, context.messages, {
590
- includeBootstrap,
591
- compressionCompleted,
592
- activeTools: context.tools || [],
593
- toolCatalog: currentAgent.cachedToolDefinitions(),
594
- })
595
- : '',
684
+ buildTaskFocusSnapshot,
596
685
  ].filter(Boolean).join('\n\n');
597
- providerRequestCount += 1;
598
- if (compressionCompleted)
599
- bootstrappedCompressionAt = currentCompressionAt;
600
- (0, agentKernelDiagnostics_1.emitRequestContextDiagnostic)({
601
- conversationId: currentAgent.activeConversationId,
602
- systemPrompt: requestSystemPrompt,
603
- messages: newmarkMessages,
604
- tools: context.tools || [],
605
- });
606
- for await (const token of currentProvider.chatStreamWithTools(currentModelName, newmarkMessages, requestSystemPrompt, temperature, maxTokens, toProviderToolDefinitions(context.tools || []), options?.signal, reasoningEffort, currentAgent.config.getBool('context', 'provider_session_id')
686
+ const providerTools = toProviderToolDefinitions(context.tools || []);
687
+ const usageRequest = currentAgent.beginProviderUsageRequest();
688
+ currentAgent.recordRequestContext(usageRequest, newmarkMessages, requestSystemPrompt, providerTools, currentModelName);
689
+ // Sorting/serializing the complete history is diagnostic work, not
690
+ // required request preparation. A sink can still opt in mid-Build.
691
+ if ((0, agentKernelDiagnostics_1.agentKernelDiagnosticsRequested)()) {
692
+ (0, agentKernelDiagnostics_1.emitRequestContextDiagnostic)({
693
+ conversationId: currentAgent.activeConversationId,
694
+ systemPrompt: requestSystemPrompt,
695
+ messages: newmarkMessages,
696
+ tools: context.tools || [],
697
+ });
698
+ }
699
+ for await (const token of currentProvider.chatStreamWithTools(currentModelName, newmarkMessages, requestSystemPrompt, temperature, maxTokens, providerTools, options?.signal, reasoningEffort, currentAgent.config.getBool('context', 'provider_session_id')
607
700
  ? currentAgent.activeConversationId
608
701
  : undefined)) {
609
702
  if (!firstTokenRecorded && ((token.type === 'text' && token.text) || (token.type === 'tool_call' && token.toolCall))) {
@@ -612,15 +705,8 @@ async function runAgentKernel(agent) {
612
705
  }
613
706
  if (process.env.NEWMARK_PROVIDER_DIAGNOSTICS === '1')
614
707
  console.error(`[NewmarkKernel] provider-token type=${token.type}`);
615
- if (options?.signal?.aborted)
616
- break;
617
708
  if (token.type === 'usage' && token.usage) {
618
- currentAgent.recordProviderUsage({
619
- input: token.usage.input,
620
- output: token.usage.output,
621
- cacheRead: token.usage.cacheRead,
622
- cacheWrite: token.usage.cacheWrite,
623
- });
709
+ currentAgent.recordProviderUsage(token.usage, usageRequest);
624
710
  (0, agentKernelDiagnostics_1.emitProviderUsageDiagnostic)({
625
711
  conversationId: currentAgent.activeConversationId,
626
712
  inputTokens: token.usage.input,
@@ -628,8 +714,15 @@ async function runAgentKernel(agent) {
628
714
  cacheReadTokens: token.usage.cacheRead,
629
715
  cacheWriteTokens: token.usage.cacheWrite,
630
716
  });
717
+ if (options?.signal?.aborted)
718
+ break;
631
719
  continue;
632
720
  }
721
+ if (options?.signal?.aborted)
722
+ break;
723
+ if ((token.type === 'text' && token.text && !currentAgent.isLlmErrorText(token.text))
724
+ || (token.type === 'tool_call' && token.toolCall))
725
+ lastRequestReplaySafe = false;
633
726
  if (token.reasoningContent) {
634
727
  const delta = token.reasoningContent.slice(thinking.length);
635
728
  thinking = token.reasoningContent;
@@ -708,6 +801,8 @@ async function runAgentKernel(agent) {
708
801
  }
709
802
  const final = assistantMessage(model, finalContent, finalContent.some(c => c.type === 'toolCall') ? 'toolUse' : 'stop');
710
803
  if (!currentAgent.isLlmErrorText(text)) {
804
+ if (finalContent.some(content => content.type === 'toolCall' || (content.type === 'text' && typeof content.text === 'string' && content.text.trim())))
805
+ requestProgressGeneration++;
711
806
  if (brokerOnlySurface && !finalContent.some(content => content.type === 'toolCall') && text)
712
807
  currentAgent.markRouteStreamCommitted();
713
808
  const durationMs = Math.max(1, Date.now() - requestStartedAt);
@@ -738,7 +833,7 @@ async function runAgentKernel(agent) {
738
833
  };
739
834
  }
740
835
  }
741
- async function transformContext(agent, messages, signal) {
836
+ async function transformContext(agent, messages, signal, providerCache) {
742
837
  const processSignal = agent.activeProcessSignal();
743
838
  if (processSignal?.aborted)
744
839
  return messages;
@@ -748,7 +843,7 @@ async function transformContext(agent, messages, signal) {
748
843
  // active model after an Auto/fallback transition can pair the wrong model
749
844
  // name with the provider captured for this compaction request.
750
845
  const compressionModel = agent.activeModelName();
751
- const provider = agent.engineModel();
846
+ const provider = agent.engineModel(providerCache);
752
847
  if (!provider || !compressionModel)
753
848
  return messages;
754
849
  // Context compression persists Agent history. Feed it only the public
@@ -835,27 +930,23 @@ function buildBuildContextBootstrap(agent, messages, options) {
835
930
  .filter(definition => toolDefinitionName(definition) !== TOOL_PROVISION_NAME)
836
931
  .map(definition => `- ${toolDefinitionName(definition)}: ${compactToolDescription(toolDefinitionDescription(definition))}`);
837
932
  const retainedMessages = messages.length;
838
- // 缓存命中关键:压缩后不再把压缩摘要冗余注入 bootstrap——压缩摘要已通过
839
- // transformContext 的 compressionContinuationPrompt 写入 messages 前缀。这里
840
- // 保持 bootstrap 文案在「压缩前/后」字节稳定,避免 compressionCompleted 分支
841
- // 单独改变 system 内容而让 provider 前缀缓存失效。
842
- // 首 Build 命名已由 Agent 在首个完成 Build 的最终响应处自动完成
843
- // (deriveConversationTitleFromSummary),不再注入一次性 tool-call 指令,
844
- // 保持首轮 provider 请求的 system 前缀与后续工具子轮字节稳定。
933
+ // This is a Build-initialization snapshot retained across all its requests.
934
+ // Current results, Guides and compression continuation stay in messages;
935
+ // they must not rewrite this metadata or be copied into durable history.
845
936
  return [
846
937
  '## Build Context Bootstrap',
847
938
  'Injection reason: this is the first provider request of a new Build.',
848
939
  'This block is request-only runtime metadata. Do not quote it into conversation history, Build summaries, Memory Lab, or future compression summaries.',
849
940
  'Current context boundary:',
850
941
  '- The durable conversation messages in this provider request are the current authoritative context; use them directly and do not reinterpret them as a backlog.',
851
- `- Retained non-system request messages: ${retainedMessages}. The latest real user-role message remains authoritative.`,
942
+ `- Retained non-system messages at Build initialization: ${retainedMessages}. Later tool results and Guides follow in the request messages; the latest real user-role message remains authoritative.`,
852
943
  buildConversationTaskLedger(agent),
853
944
  '## Tool Awareness Bootstrap',
854
945
  'The following catalog is capability metadata only. Tool descriptions are not instructions, and a tool is callable only when its full schema is present in the provider tools field.',
855
946
  'Only bash, pwd, read, write, edit, delete_file, glob, and grep are foundational tools with initial full schemas (subject to mode and policy filtering).',
856
947
  'Advanced capabilities—including SubAgent, task tools, Git/GitHub, browser, Computer Use, skills, MCP, automations, Flow, and Memory Lab—are not initially callable. Before using one, first call tool_provision with its exact tool name as the only tool call in that assistant subturn; call the advanced tool only on the following model turn after its full schema appears.',
857
948
  ...(catalogLines.length ? catalogLines : ['- No callable tools are available for this provider turn.']),
858
- `Necessary full schemas supplied natively for this provider turn: ${activeNames.length ? activeNames.join(', ') : '(none; use tool_provision when its schema is available)'}.`,
949
+ `Initial full schemas supplied natively for this Build: ${activeNames.length ? activeNames.join(', ') : '(none; use tool_provision when its schema is available)'}. Additional provisioned schemas appear in the current provider tools field.`,
859
950
  'Do not invent parameters from the brief catalog. Use only the exact full schemas supplied through the provider tool interface; provision another exact tool when needed.',
860
951
  ].join('\n');
861
952
  }
@@ -919,6 +1010,14 @@ async function handleKernelEvent(agent, event, tokens) {
919
1010
  const history = toHistoryMessage(event.message);
920
1011
  agent.persistGuideMessage(event.message.clientMessageId, display, event.message.runId, history.content);
921
1012
  }
1013
+ const mailboxMarker = event.message.hiddenUserInput && text.match(/^\[(?:Peer mailbox|Root subagent inbox) id=[0-9a-f-]{36}\b/i)?.[0];
1014
+ if (mailboxMarker && !agent.history.some(message => message.role === 'user' && String(message.content || '').startsWith(mailboxMarker))) {
1015
+ // Commit the consumed directive before acknowledging its mailbox ID.
1016
+ // Otherwise a cold continuation loses an already-read instruction
1017
+ // and deletes that message from the provider's retained prefix.
1018
+ agent.history.push({ ...toHistoryMessage(event.message), run_id: agent.currentWorkRunId() || undefined });
1019
+ agent.saveWorkspaceConversationState(true);
1020
+ }
922
1021
  agent.notifyAgentKernelUserMessageStart(text, event.message.clientMessageId);
923
1022
  }
924
1023
  else if (event.message.role === 'assistant') {
@@ -1010,9 +1109,10 @@ async function handleKernelEvent(agent, event, tokens) {
1010
1109
  const publicMessage = internalProvision ? { ...event.message, content: publicContent } : event.message;
1011
1110
  const text = agent.sanitizeAssistantOutput(KernelMessageText(publicMessage));
1012
1111
  const failed = event.message.stopReason === 'error' || agent.isLlmErrorText(text);
1013
- if (!failed)
1112
+ const aborted = event.message.stopReason === 'aborted';
1113
+ if (!failed && !aborted)
1014
1114
  emitBufferedAssistantText(agent, tokens);
1015
- if (text && !failed)
1115
+ if (text && !failed && !aborted)
1016
1116
  agent.emitWorkEvent({ type: realToolCalls.length ? 'response' : 'final_response', content: text });
1017
1117
  if ((text || realToolCalls.length) && event.message.stopReason !== 'aborted' && !failed) {
1018
1118
  if (text && !realToolCalls.length)
@@ -1021,6 +1121,7 @@ async function handleKernelEvent(agent, event, tokens) {
1021
1121
  // Keep tool-call metadata, but never replay a hidden-reasoning line
1022
1122
  // that was deliberately removed from the public completed message.
1023
1123
  historyMessage.content = text;
1124
+ historyMessage.run_id = agent.currentWorkRunId() || undefined;
1024
1125
  agent.history.push(historyMessage);
1025
1126
  agent.saveWorkspaceConversationState();
1026
1127
  }
@@ -1028,8 +1129,24 @@ async function handleKernelEvent(agent, event, tokens) {
1028
1129
  resetAssistantToolVisibility(agent);
1029
1130
  }
1030
1131
  else if (event.message.role === 'toolResult' && event.message.toolName !== TOOL_PROVISION_NAME) {
1031
- agent.history.push(toHistoryMessage(event.message));
1032
- agent.saveWorkspaceConversationState();
1132
+ const receipt = event.message.details?.settlementReceipt;
1133
+ const historyMessage = { ...toHistoryMessage(event.message), run_id: agent.currentWorkRunId() || undefined,
1134
+ ...(!event.message.isError && receipt ? { subagent_settlement_receipt: { ...receipt } } : {}),
1135
+ };
1136
+ agent.history.push(historyMessage);
1137
+ try {
1138
+ agent.saveWorkspaceConversationState();
1139
+ }
1140
+ catch (error) {
1141
+ // A later notification must not mistake this unsaved in-memory
1142
+ // receipt for a committed result after the persistence error.
1143
+ delete historyMessage.subagent_settlement_receipt;
1144
+ throw error;
1145
+ }
1146
+ // Retire redundant automatic wakes only after the full tool result and
1147
+ // its exact version receipt are durable. The receipt is never content.
1148
+ if (receipt && !event.message.isError)
1149
+ agent.acknowledgeSubagentSettlementReceipts();
1033
1150
  }
1034
1151
  break;
1035
1152
  }
@@ -1045,7 +1162,7 @@ function toKernelModel(agent) {
1045
1162
  provider: m?.provider || 'newmark',
1046
1163
  baseUrl: m?.provider_url || '',
1047
1164
  reasoning: !!m?.thinking,
1048
- input: m?.vision ? ['text', 'image'] : ['text'],
1165
+ input: ['text', 'image'],
1049
1166
  cost: {
1050
1167
  input: Number(m?.cost_per_1k_input || 0) * 1000,
1051
1168
  output: Number(m?.cost_per_1k_output || 0) * 1000,
@@ -1123,12 +1240,33 @@ class ToolProvisionSession {
1123
1240
  this.broker = this.brokerDefinition();
1124
1241
  }
1125
1242
  currentDefinitions() {
1126
- const active = new Set([...this.initialNames, ...this.provisionedNames]);
1127
1243
  return [
1128
- ...this.catalog.filter(definition => active.has(toolDefinitionName(definition))),
1244
+ ...this.catalog.filter(definition => this.initialNames.has(toolDefinitionName(definition))),
1129
1245
  this.broker,
1246
+ // Preserve the complete previously exposed schema sequence. Loading a
1247
+ // new tool appends its schema instead of inserting it before the broker
1248
+ // or reordering earlier provisions by catalog position.
1249
+ ...[...this.provisionedNames]
1250
+ .filter(name => !this.initialNames.has(name))
1251
+ .map(name => this.definitionsByName.get(name)),
1130
1252
  ];
1131
1253
  }
1254
+ snapshot() {
1255
+ return { initialTools: [...this.initialNames], provisionedTools: [...this.provisionedNames] };
1256
+ }
1257
+ restore(initial, provisioned) {
1258
+ // Reuse ordering only through the current policy-filtered catalog. A
1259
+ // persisted name is never authority to restore a revoked capability.
1260
+ this.initialNames.clear();
1261
+ for (const name of initial)
1262
+ if (this.definitionsByName.has(name))
1263
+ this.initialNames.add(name);
1264
+ this.provisionedNames.clear();
1265
+ for (const name of provisioned)
1266
+ if (this.definitionsByName.has(name))
1267
+ this.provisionedNames.add(name);
1268
+ this.broker = this.brokerDefinition();
1269
+ }
1132
1270
  metrics() {
1133
1271
  const active = this.currentDefinitions();
1134
1272
  return {
@@ -1356,7 +1494,9 @@ function routeToolSurfaceV2(agent, definitions, toolchain, task) {
1356
1494
  const plan = planner.plan({
1357
1495
  agentRunId: agent.runtimeActorId,
1358
1496
  buildBlockId: agent.activeConversationId || 'build',
1359
- userInput: task,
1497
+ // Mailbox deltas do not change the peer's assigned capability plan.
1498
+ // Keep this diagnostic fingerprint out of the changing request suffix.
1499
+ userInput: agent.isSubagentRuntime ? (agent.subagents.get(agent.runtimeActorId)?.prompt || task) : task,
1360
1500
  objective: '',
1361
1501
  previousToolCalls: [],
1362
1502
  toolUsageFrequency: new Map(),
@@ -1441,7 +1581,8 @@ function toKernelTools(agent, definitions, provisioning) {
1441
1581
  };
1442
1582
  }
1443
1583
  const args = JSON.stringify(params || {});
1444
- const rawText = await executeNewmarkTool(agent, name, args, fn.parameters, signal);
1584
+ let settlementReceipt;
1585
+ const rawText = await executeNewmarkTool(agent, name, args, fn.parameters, signal, receipt => { settlementReceipt = receipt; });
1445
1586
  if (signal?.aborted) {
1446
1587
  discardComputerUseVisionImage(name, rawText);
1447
1588
  throw abortError();
@@ -1474,7 +1615,9 @@ function toKernelTools(agent, definitions, provisioning) {
1474
1615
  }
1475
1616
  catch { }
1476
1617
  }
1477
- return { content, details: { tool: name, ok: true, terminate, ...(launchReceipt ? { launchReceipt } : {}), visionImagePath: visionImage.imagePath || undefined, ephemeralVisionImage: !!visionImage.image, capturedAttachmentId: capturedInput?.id, displayImage }, terminate };
1618
+ return { content, details: { tool: name, ok: true, terminate, ...(launchReceipt ? { launchReceipt } : {}),
1619
+ ...(settlementReceipt && text === rawText ? { settlementReceipt } : {}),
1620
+ visionImagePath: visionImage.imagePath || undefined, ephemeralVisionImage: !!visionImage.image, capturedAttachmentId: capturedInput?.id, displayImage }, terminate };
1478
1621
  },
1479
1622
  };
1480
1623
  }).filter((tool) => !!tool.name);
@@ -1604,8 +1747,6 @@ function visualFallbackImageInput(agent, name, text) {
1604
1747
  if (name !== 'screen_capture' && name !== 'computer_use' && name !== 'browser_use' && name !== 'pdf_read')
1605
1748
  return {};
1606
1749
  const model = agent.activeModelConfig();
1607
- if (!model?.vision)
1608
- return {};
1609
1750
  try {
1610
1751
  const parsed = JSON.parse(text);
1611
1752
  const nested = name === 'pdf_read' && parsed.result && typeof parsed.result === 'object'
@@ -1627,7 +1768,7 @@ function visualFallbackImageInput(agent, name, text) {
1627
1768
  return {};
1628
1769
  }
1629
1770
  }
1630
- async function executeNewmarkTool(agent, name, args, inputSchema, signal) {
1771
+ async function executeNewmarkTool(agent, name, args, inputSchema, signal, onSettlementReceipt) {
1631
1772
  const stopToolTimer = (0, performanceDiagnostics_1.performanceTimer)('tool_execution', { conversationId: agent.activeConversationId, detail: { tool: name } });
1632
1773
  try {
1633
1774
  const wsDir = agent.workspace.current?.path || agent.rootPath;
@@ -1663,10 +1804,13 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal) {
1663
1804
  return (await agent.handleSubagentContinueEnvelope(args)).output;
1664
1805
  if (name === 'subagent_list')
1665
1806
  return agent.handleSubagentListEnvelope(args).output;
1666
- if (name === 'subagent_read')
1667
- return agent.handleSubagentReadEnvelope(args).output;
1668
- if (name === 'subagent_result')
1669
- return agent.handleSubagentResultEnvelope(args).output;
1807
+ if (name === 'subagent_read' || name === 'subagent_result') {
1808
+ const result = name === 'subagent_read' ? agent.handleSubagentReadEnvelope(args) : agent.handleSubagentResultEnvelope(args);
1809
+ const receipt = result.metadata?.settlementReceipt;
1810
+ if (result.ok && receipt)
1811
+ onSettlementReceipt?.(receipt);
1812
+ return result.output;
1813
+ }
1670
1814
  if (name === 'subagent_close')
1671
1815
  return agent.handleSubagentCloseEnvelope(args).output;
1672
1816
  if (name === 'branch_list')
@@ -1766,8 +1910,7 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal) {
1766
1910
  actorId: agent.runtimeActorId,
1767
1911
  workspaceId: (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(wsDir),
1768
1912
  backend: process.env.NEWMARK_WSL_DISTRO ? 'wsl' : (process.platform === 'win32' ? 'windows' : process.platform),
1769
- allowEphemeralVisionImage: (name === 'screen_capture' || name === 'computer_use' || name === 'browser_use' || name === 'pdf_read' || name === 'ocr_read')
1770
- && !!agent.activeModelConfig()?.vision,
1913
+ allowEphemeralVisionImage: (name === 'screen_capture' || name === 'computer_use' || name === 'browser_use' || name === 'pdf_read' || name === 'ocr_read'),
1771
1914
  signal,
1772
1915
  });
1773
1916
  if (signal?.aborted)