newmark-agent 0.3.12 → 0.4.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 (52) hide show
  1. package/dist/cli-commands.d.ts +1 -0
  2. package/dist/cli-commands.js +11 -2
  3. package/dist/context/domain/types.d.ts +37 -0
  4. package/dist/context/services/context-orchestrator.js +2 -0
  5. package/dist/conversation-utility-host.bundle.cjs +1147 -131
  6. package/dist/conversation-utility-host.js +3 -0
  7. package/dist/core/agent.d.ts +139 -5
  8. package/dist/core/agent.js +962 -82
  9. package/dist/core/agentKernel/agent-loop.js +29 -3
  10. package/dist/core/agentKernel/types.d.ts +7 -0
  11. package/dist/core/agentKernelRunner.d.ts +2 -0
  12. package/dist/core/agentKernelRunner.js +121 -19
  13. package/dist/core/conversationKernel.d.ts +5 -0
  14. package/dist/core/conversationKernel.js +29 -0
  15. package/dist/core/dshCompatibility.d.ts +198 -0
  16. package/dist/core/dshCompatibility.js +600 -0
  17. package/dist/core/electronUtilityAgentClient.d.ts +4 -0
  18. package/dist/core/electronUtilityAgentClient.js +4 -0
  19. package/dist/core/electronUtilityRuntimePool.d.ts +8 -0
  20. package/dist/core/electronUtilityRuntimePool.js +14 -0
  21. package/dist/core/mcpManager.d.ts +1 -0
  22. package/dist/core/mcpManager.js +100 -10
  23. package/dist/core/subagent.d.ts +6 -0
  24. package/dist/core/subagent.js +22 -1
  25. package/dist/core/toolPolicy.d.ts +6 -0
  26. package/dist/core/toolPolicy.js +49 -1
  27. package/dist/core/types.d.ts +1 -1
  28. package/dist/core/utilityAgentProtocol.d.ts +8 -1
  29. package/dist/core/workspace.d.ts +9 -0
  30. package/dist/core/workspace.js +48 -1
  31. package/dist/core/wslAgentClient.d.ts +4 -0
  32. package/dist/core/wslAgentClient.js +4 -0
  33. package/dist/core/wslAgentProtocol.d.ts +8 -1
  34. package/dist/core/wslAgentRuntimePool.d.ts +8 -0
  35. package/dist/core/wslAgentRuntimePool.js +15 -0
  36. package/dist/launcher.js +8 -0
  37. package/dist/llm/provider.d.ts +1 -1
  38. package/dist/llm/provider.js +4 -3
  39. package/dist/main.js +163 -11
  40. package/dist/preload.js +11 -0
  41. package/dist/providers/chat-completions.adapter.js +41 -15
  42. package/dist/providers/provider-adapter.d.ts +3 -0
  43. package/dist/toolchain/registry/tool-registry.d.ts +13 -1
  44. package/dist/toolchain/registry/tool-registry.js +8 -0
  45. package/dist/toolchain/registry-seeder.js +51 -5
  46. package/dist/tools/index.js +11 -2
  47. package/dist/tools/nativeTools.js +5 -1
  48. package/dist/ui/index.html +2530 -205
  49. package/dist/ui/lucide-sprite.svg +26 -0
  50. package/dist/wsl-agent-host.bundle.cjs +1147 -131
  51. package/dist/wsl-agent-host.js +3 -0
  52. package/package.json +4 -2
package/dist/main.js CHANGED
@@ -73,6 +73,7 @@ const startupPrewarm_1 = require("./core/startupPrewarm");
73
73
  const runtimeShutdown_1 = require("./core/runtimeShutdown");
74
74
  const runtimeLifecycle_1 = require("./core/runtimeLifecycle");
75
75
  const compat_1 = require("./core/compat");
76
+ const dshCompatibility_1 = require("./core/dshCompatibility");
76
77
  const mcpManager_1 = require("./core/mcpManager");
77
78
  const cli_help_1 = require("./cli-help");
78
79
  const APP_NAME = 'Newmark Agent';
@@ -123,6 +124,7 @@ let browserUseEngine = null;
123
124
  // the authoritative Browser-Use/right-sidebar binding.
124
125
  const browserGuestContentsByHost = new Map();
125
126
  const browserGuestBindingsByRuntime = new Map();
127
+ const browserGuestKeyboardBridgeIds = new Set();
126
128
  function browserGuestRuntimeKey(target) {
127
129
  return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
128
130
  }
@@ -686,9 +688,13 @@ function resolveTuiWorkspacePath(args, root) {
686
688
  // while making the safe one-argument form fully self-contained.
687
689
  return pathArgValue(args, '--root') ? root : process.cwd();
688
690
  }
691
+ // Set immediately after argument resolution so startup failures from an
692
+ // explicit temporary root are recorded beside that root instead of leaking a
693
+ // diagnostic file into the user's canonical runtime.
694
+ let startupRuntimeRoot = '';
689
695
  function startupLogPath() {
690
696
  try {
691
- const userData = userRuntimeRoot();
697
+ const userData = startupRuntimeRoot || userRuntimeRoot();
692
698
  fs.mkdirSync(userData, { recursive: true });
693
699
  return path.join(userData, 'startup.log');
694
700
  }
@@ -841,7 +847,29 @@ function registerBrowserGuest(host, guest, requestedTarget) {
841
847
  conversationId: target.conversationId,
842
848
  });
843
849
  browserGuestContentsByHost.set(host.id, guest.id);
850
+ if (!browserGuestKeyboardBridgeIds.has(guest.id)) {
851
+ browserGuestKeyboardBridgeIds.add(guest.id);
852
+ // WebView keyboard events do not bubble to the host renderer. Reserve only
853
+ // the two app-wide discovery surfaces and leave navigation/editing keys to
854
+ // the page itself.
855
+ guest.on('before-input-event', (event, input) => {
856
+ if (input.type !== 'keyDown' || input.isAutoRepeat || host.isDestroyed())
857
+ return;
858
+ const key = String(input.key || '').toLowerCase();
859
+ const noSecondaryModifiers = !input.alt && !input.control && !input.meta && !input.shift;
860
+ let commandId = '';
861
+ if (key === 'f1' && noSecondaryModifiers)
862
+ commandId = 'help.keyboardShortcuts';
863
+ else if (key === 'p' && (input.control || input.meta) && input.shift && !input.alt)
864
+ commandId = 'app.commandPalette';
865
+ if (!commandId)
866
+ return;
867
+ event.preventDefault();
868
+ host.send('keyboard:command', { id: commandId, source: 'browserGuest' });
869
+ });
870
+ }
844
871
  guest.once('destroyed', () => {
872
+ browserGuestKeyboardBridgeIds.delete(guest.id);
845
873
  if (browserGuestContentsByHost.get(host.id) === guest.id)
846
874
  browserGuestContentsByHost.delete(host.id);
847
875
  const binding = browserGuestBindingsByRuntime.get(target.runtimeKey);
@@ -1035,6 +1063,7 @@ const args = userArgs();
1035
1063
  const command = args.find(a => a === 'flow' || a === 'edit');
1036
1064
  const isTuiArg = args.some(arg => arg.toLowerCase() === '--tui');
1037
1065
  const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
1066
+ const cliCommand = args.find(a => cli_commands_1.CLI_COMMANDS.includes(a));
1038
1067
  const isFlowArg = command === 'flow';
1039
1068
  const isEditArg = command === 'edit';
1040
1069
  const isHelpArg = !hasCliCommand && (args.some(arg => ['--help', '-h'].includes(arg.toLowerCase())) || args[0]?.toLowerCase() === 'help');
@@ -1048,18 +1077,34 @@ if (invalidArgument) {
1048
1077
  console.error(`Invalid Newmark argument: ${invalidArgument}`);
1049
1078
  process.exit(2);
1050
1079
  }
1080
+ // Keep command-specific help on the same early-exit path as top-level help.
1081
+ // This matters for the packaged Electron entry: a command such as
1082
+ // `send --help` must never create a GUI window, start prewarm work, or touch a
1083
+ // user-data profile merely to print its contract.
1084
+ if (hasCliCommand && cliCommand && (0, cli_commands_1.cliHelpRequested)(args)) {
1085
+ console.log((0, cli_commands_1.cliCommandHelp)(cliCommand));
1086
+ process.exit(0);
1087
+ }
1051
1088
  // Electron's Chromium profile is a separate state boundary from Newmark's
1052
1089
  // business root. Bind both before any ready event so --root cannot leave
1053
1090
  // Preferences, DIPS, DevTools ports, cookies, or session storage in the real
1054
1091
  // default AppData directory. The dedicated subdirectories keep Chromium's
1055
1092
  // files separate from the durable Newmark config/workspace files.
1056
1093
  const runtimeRoot = resolveRoot(args);
1057
- const electronUserDataRoot = path.join(runtimeRoot, 'Electron');
1094
+ startupRuntimeRoot = runtimeRoot;
1095
+ const explicitElectronUserDataRoot = pathArgValue(args, '--user-data-dir');
1096
+ const electronUserDataRoot = path.resolve(explicitElectronUserDataRoot || path.join(runtimeRoot, 'Electron'));
1058
1097
  const electronSessionDataRoot = path.join(electronUserDataRoot, 'session-data');
1059
1098
  try {
1060
1099
  fs.mkdirSync(electronSessionDataRoot, { recursive: true });
1061
1100
  electron_1.app.setPath('userData', electronUserDataRoot);
1062
1101
  electron_1.app.setPath('sessionData', electronSessionDataRoot);
1102
+ // app.setPath is the Electron API contract; the switch makes the same
1103
+ // boundary visible to Chromium children, including the console-wrapper
1104
+ // `--` forwarding path where an early child may otherwise retain the default
1105
+ // profile before the first BrowserWindow is created.
1106
+ electron_1.app.commandLine.removeSwitch('user-data-dir');
1107
+ electron_1.app.commandLine.appendSwitch('user-data-dir', electronUserDataRoot);
1063
1108
  }
1064
1109
  catch (error) {
1065
1110
  console.error(`Unable to isolate Electron user-data directory: ${error instanceof Error ? error.message : String(error)}`);
@@ -1659,6 +1704,12 @@ else {
1659
1704
  win.focus();
1660
1705
  }, 150);
1661
1706
  });
1707
+ win.webContents.on('unresponsive', () => {
1708
+ logStartupFailure('renderer-unresponsive', new Error(`Window renderer became unresponsive (webContents ${win.webContents.id})`));
1709
+ });
1710
+ win.webContents.on('responsive', () => {
1711
+ recordStartup(`renderer-responsive-${win.webContents.id}`);
1712
+ });
1662
1713
  if (!automationWakeMode)
1663
1714
  win.maximize();
1664
1715
  if (!automationWakeMode && showWindow) {
@@ -1674,15 +1725,18 @@ else {
1674
1725
  return;
1675
1726
  if (agent) {
1676
1727
  const closeBehavior = agent.config.getStr('general', 'close_behavior');
1728
+ recordStartup(`window-close-requested-${closeBehavior || 'exit'}`);
1677
1729
  if (closeBehavior === 'minimize') {
1678
1730
  e.preventDefault();
1679
1731
  win.hide();
1680
1732
  createTray();
1733
+ recordStartup('window-hidden-to-tray');
1681
1734
  return;
1682
1735
  }
1683
1736
  if (agent.config.getBool('general', 'auto_archive_on_close')) {
1684
1737
  agent.archiveSession();
1685
1738
  }
1739
+ recordStartup('window-close-exit');
1686
1740
  }
1687
1741
  });
1688
1742
  win.on('closed', () => {
@@ -2299,6 +2353,7 @@ else {
2299
2353
  (0, runtimeLifecycle_1.markRuntimeLifecycleClean)(root, 'main');
2300
2354
  };
2301
2355
  electron_1.app.on('will-quit', event => {
2356
+ recordStartup('will-quit');
2302
2357
  // Window-close and tray-exit both enter this path. The runtime pools
2303
2358
  // must get a bounded graceful-shutdown window regardless of which
2304
2359
  // surface initiated the close; otherwise a stuck child can keep the
@@ -2462,6 +2517,32 @@ else {
2462
2517
  activeFlowsByRuntimeKey.delete(key);
2463
2518
  agent.clearStoredFlowSuspension(state.target.conversationId);
2464
2519
  };
2520
+ // Archive is a destructive lifecycle boundary. It must cancel the Flow
2521
+ // owner itself before touching the runtime pool; Flow runs bypass the
2522
+ // ConversationKernel and therefore cannot be stopped by the pool alone.
2523
+ // Marking the state before aborting is important: the provider may reject
2524
+ // on the same turn, and its late catch/finally must not recreate a paused
2525
+ // conversation after archive has removed it.
2526
+ const interruptActiveFlowForArchive = (target) => {
2527
+ const key = activeFlowStateKey(target);
2528
+ const state = activeFlowsByRuntimeKey.get(key) || null;
2529
+ if (!state)
2530
+ return;
2531
+ state.archiveRequested = true;
2532
+ try {
2533
+ state.abortController?.abort(new Error(`Flow discarded because conversation was archived: ${state.name || state.workflow.name}`));
2534
+ }
2535
+ catch { }
2536
+ state.flowAgent?.abortActiveKernelRun();
2537
+ state.flowAgent?.interruptRunningConversationWorkRuns(state.target, 'force_interrupted');
2538
+ state.flowAgent?.clearStoredFlowSuspension(state.target.conversationId);
2539
+ if (agent?.workspace.current
2540
+ && target.workspace
2541
+ && path.resolve(agent.workspace.current.path) === path.resolve(target.workspace.path)) {
2542
+ agent.clearStoredFlowSuspension(target.conversationId);
2543
+ }
2544
+ activeFlowsByRuntimeKey.delete(key);
2545
+ };
2465
2546
  const utilityHostToolHandler = (0, utilityHostToolRouter_1.createUtilityHostToolHandler)({
2466
2547
  persistenceRoot: root,
2467
2548
  isToolEnabled: toolName => !!agent && (0, nativeTools_1.isNativeToolEnabled)(toolName, agent.config.nativeToolEnabled()),
@@ -2851,6 +2932,14 @@ else {
2851
2932
  };
2852
2933
  }
2853
2934
  catch (e) {
2935
+ if (flowState.archiveRequested) {
2936
+ // Archive already force-finalized the current Build ledger. Do not
2937
+ // persist an interrupted Flow suspension or return Flow ownership
2938
+ // to the renderer after the target has been removed.
2939
+ flowAgent.pendingOptions = [];
2940
+ suspended = false;
2941
+ return { ok: false, archived: true, error: 'Flow discarded because the conversation was archived.' };
2942
+ }
2854
2943
  if (e instanceof flow_runner_1.FlowQuestionPendingError) {
2855
2944
  suspended = true;
2856
2945
  flowAgent.flowPc = e.componentId;
@@ -2944,6 +3033,11 @@ else {
2944
3033
  const flowKey = activeFlowStateKey(flowTarget);
2945
3034
  const flowAgent = isolatedConversationAgent(flowTarget);
2946
3035
  suspension.flowAgent = flowAgent;
3036
+ // A previous Flow may have been interrupted just before its isolated
3037
+ // Agent flushed the work ledger. Reconcile only this explicitly paused
3038
+ // target before starting the next component; the normal runner guard
3039
+ // remains intact for genuine concurrent Builds.
3040
+ flowAgent.interruptRunningConversationWorkRuns(flowTarget, 'interrupted');
2947
3041
  const flowAbortController = new AbortController();
2948
3042
  suspension.abortController = flowAbortController;
2949
3043
  activeFlowsByRuntimeKey.set(flowKey, suspension);
@@ -2978,6 +3072,11 @@ else {
2978
3072
  };
2979
3073
  }
2980
3074
  catch (error) {
3075
+ if (suspension.archiveRequested) {
3076
+ flowAgent.pendingOptions = [];
3077
+ suspendedAgain = false;
3078
+ return { ok: false, archived: true, error: 'Flow discarded because the conversation was archived.' };
3079
+ }
2981
3080
  if (error instanceof flow_runner_1.FlowQuestionPendingError) {
2982
3081
  suspendedAgain = true;
2983
3082
  flowAgent.flowPc = error.componentId;
@@ -2999,9 +3098,9 @@ else {
2999
3098
  workRuns: flowAgent.getConversationSnapshot(flowAgent.activeConversationId).workRuns,
3000
3099
  };
3001
3100
  }
3002
- if (isUserFlowAbort(error)) {
3003
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
3004
- }
3101
+ // A Stop/Esc during a resumed Flow is another pause, not a terminal
3102
+ // IPC failure. Fall through to the same interrupted-suspension path as
3103
+ // the initial Flow run so repeated pause/resume clicks remain valid.
3005
3104
  suspendedAgain = true;
3006
3105
  const resumeFailure = error;
3007
3106
  const interruptedComponentId = typeof resumeFailure.componentId === 'number'
@@ -3154,6 +3253,12 @@ else {
3154
3253
  runtimeDeferred: false,
3155
3254
  };
3156
3255
  });
3256
+ electron_1.ipcMain.handle('agent:setConversationBranchCommunication', async (_event, targetInput, enabled) => {
3257
+ if (!agent)
3258
+ return false;
3259
+ const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3260
+ return mutateTargetConversation(target, () => isolatedConversationAgent(target).setBranchCommunication(enabled !== false));
3261
+ });
3157
3262
  electron_1.ipcMain.handle('agent:computerUseState', async (_event, targetInput) => {
3158
3263
  if (!agent)
3159
3264
  return { enabled: false, occupied: false, runtimeKey: '' };
@@ -3691,6 +3796,18 @@ else {
3691
3796
  ? await ensureWslConversationPool().checkpoint(target)
3692
3797
  : await ensureElectronUtilityPool().checkpoint(target);
3693
3798
  });
3799
+ electron_1.ipcMain.handle('agent:compressContext', async (_event, request) => {
3800
+ if (!agent)
3801
+ throw new Error('Agent not initialized');
3802
+ const target = conversationRuntimeTarget(request);
3803
+ const options = {
3804
+ keepRecent: Number.isFinite(Number(request?.keepRecent)) ? Math.floor(Number(request.keepRecent)) : undefined,
3805
+ force: request?.force !== false,
3806
+ };
3807
+ return wslBackendEnabled()
3808
+ ? await ensureWslConversationPool().contextCompress(target, options)
3809
+ : await ensureElectronUtilityPool().contextCompress(target, options);
3810
+ });
3694
3811
  electron_1.ipcMain.handle('agent:rateAutoRoute', async (_event, request) => {
3695
3812
  if (!agent)
3696
3813
  return { ok: false, reason: 'no_active_auto_route' };
@@ -3847,9 +3964,14 @@ else {
3847
3964
  mutatingRuntimeKeys.add(normalized.runtimeKey);
3848
3965
  try {
3849
3966
  // Archive is a destructive lifecycle command. It intentionally
3850
- // bypasses the normal mutation/active-prompt guard and hard-stops
3851
- // any resident runtime before touching conversation persistence.
3852
- await forceStopTargetRuntime(normalized);
3967
+ // bypasses the normal mutation/active-prompt guard. Cancel the
3968
+ // conversation-local Flow synchronously, then start the resident
3969
+ // runtime hard-stop in the background so a stuck child cannot hold
3970
+ // the archive click hostage.
3971
+ interruptActiveFlowForArchive(normalized);
3972
+ void forceStopTargetRuntime(normalized).catch(error => {
3973
+ console.error('[Newmark] archive runtime force-stop failed:', error instanceof Error ? error.message : String(error));
3974
+ });
3853
3975
  const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
3854
3976
  const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
3855
3977
  const ownsTargetWorkspace = !!normalized.workspace
@@ -3861,6 +3983,7 @@ else {
3861
3983
  // latest locked state snapshot, so rapid clicks do not serialize on
3862
3984
  // large Markdown bodies or lose a sibling deletion.
3863
3985
  const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
3986
+ archiveOwner.clearStoredFlowSuspension(normalized.conversationId);
3864
3987
  const archived = await archiveOwner.archiveConversationAsync(normalized.conversationId);
3865
3988
  if (!archived)
3866
3989
  return { ok: false, error: 'Conversation archive could not be written.' };
@@ -4205,6 +4328,20 @@ else {
4205
4328
  }
4206
4329
  return { error: 'Agent not initialized' };
4207
4330
  });
4331
+ electron_1.ipcMain.handle('app:openWebUrl', async (_event, rawUrl) => {
4332
+ try {
4333
+ const target = new URL(String(rawUrl || ''));
4334
+ const allowedHosts = new Set(['github.com', 'www.github.com', 'npmjs.com', 'www.npmjs.com']);
4335
+ if (target.protocol !== 'https:' || !allowedHosts.has(target.hostname.toLowerCase()) || !!target.username || !!target.password || (!!target.port && target.port !== '443')) {
4336
+ return { ok: false, error: 'Only approved HTTPS documentation hosts can be opened.' };
4337
+ }
4338
+ await electron_1.shell.openExternal(target.toString());
4339
+ return { ok: true };
4340
+ }
4341
+ catch (error) {
4342
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
4343
+ }
4344
+ });
4208
4345
  electron_1.ipcMain.handle('agent:selectWorkspace', async (_event, id) => {
4209
4346
  if (agent) {
4210
4347
  const requested = String(id || '').trim();
@@ -4502,13 +4639,25 @@ else {
4502
4639
  const controller = new AbortController();
4503
4640
  editorCompletionControllers.set(ownerId, controller);
4504
4641
  try {
4505
- return await (agent?.editorModelRequest({ ...request, completion: true, preferCopilot: true }, controller.signal) || { ok: false, text: '', error: 'Agent not initialized' });
4642
+ const requestId = String(request.requestId || '');
4643
+ const onTextDelta = requestId
4644
+ ? (text) => {
4645
+ if (!controller.signal.aborted && !event.sender.isDestroyed())
4646
+ event.sender.send('agent:editorCompletionDelta', { requestId, text });
4647
+ }
4648
+ : undefined;
4649
+ return await (agent?.editorModelRequest({ ...request, completion: true, preferCopilot: true, onTextDelta }, controller.signal) || { ok: false, text: '', error: 'Agent not initialized' });
4506
4650
  }
4507
4651
  finally {
4508
4652
  if (editorCompletionControllers.get(ownerId) === controller)
4509
4653
  editorCompletionControllers.delete(ownerId);
4510
4654
  }
4511
4655
  });
4656
+ electron_1.ipcMain.handle('agent:editorCompleteCancel', async (event) => {
4657
+ const ownerId = event.sender.id;
4658
+ editorCompletionControllers.get(ownerId)?.abort(new Error('Editor completion cancelled'));
4659
+ return { ok: true };
4660
+ });
4512
4661
  electron_1.ipcMain.handle('agent:editorAssist', async (_event, request) => {
4513
4662
  return agent?.editorModelRequest({ ...request, completion: false }) || { ok: false, text: '', error: 'Agent not initialized' };
4514
4663
  });
@@ -4605,6 +4754,7 @@ else {
4605
4754
  })));
4606
4755
  return { servers: mcpManager.list(), discovered };
4607
4756
  });
4757
+ electron_1.ipcMain.handle('dsh:discover', async () => (0, dshCompatibility_1.discoverDshCompatibility)(root));
4608
4758
  electron_1.ipcMain.handle('mcp:upsert', async (_event, input) => {
4609
4759
  if (!mcpManager)
4610
4760
  return { ok: false, error: 'MCP manager is unavailable.' };
@@ -4619,12 +4769,14 @@ else {
4619
4769
  electron_1.ipcMain.handle('mcp:setEnabled', async (_event, id, enabled) => {
4620
4770
  if (!mcpManager)
4621
4771
  return { ok: false, error: 'MCP manager is unavailable.' };
4622
- return { ok: mcpManager.setEnabled(id, enabled), servers: mcpManager.list() };
4772
+ const ok = mcpManager.setEnabled(id, enabled);
4773
+ return { ok, error: ok ? undefined : 'MCP server was not found.', servers: mcpManager.list() };
4623
4774
  });
4624
4775
  electron_1.ipcMain.handle('mcp:remove', async (_event, id) => {
4625
4776
  if (!mcpManager)
4626
4777
  return { ok: false, error: 'MCP manager is unavailable.' };
4627
- return { ok: mcpManager.remove(id), servers: mcpManager.list() };
4778
+ const ok = mcpManager.remove(id);
4779
+ return { ok, error: ok ? undefined : 'MCP server was not found.', servers: mcpManager.list() };
4628
4780
  });
4629
4781
  electron_1.ipcMain.handle('memoryLab:read', async (_event, selector) => {
4630
4782
  if (!agent)
package/dist/preload.js CHANGED
@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('api', {
12
12
  sendMessage: (message, target) => ipcRenderer.invoke('agent:send', message, target),
13
13
  enqueueGuide: (envelope) => ipcRenderer.invoke('agent:enqueueGuide', envelope),
14
14
  checkpointConversation: (request) => ipcRenderer.invoke('agent:checkpointConversation', request),
15
+ compressContext: (request) => ipcRenderer.invoke('agent:compressContext', request),
15
16
  rateAutoRoute: (request) => ipcRenderer.invoke('agent:rateAutoRoute', request),
16
17
  stopConversation: (request) => ipcRenderer.invoke('agent:stopConversation', request),
17
18
  setWorkRunExpanded: (request) => ipcRenderer.invoke('agent:setWorkRunExpanded', request),
@@ -32,12 +33,16 @@ contextBridge.exposeInMainWorld('api', {
32
33
  getConversationPlan: (conversationId) => ipcRenderer.invoke('agent:getConversationPlan', conversationId),
33
34
  updateConversationPlan: (plan, conversationId) => ipcRenderer.invoke('agent:updateConversationPlan', plan, conversationId),
34
35
  setConversationPinned: (id, pinned) => ipcRenderer.invoke('agent:setConversationPinned', id, pinned),
36
+ setConversationBranchCommunication: (target, enabled) => ipcRenderer.invoke('agent:setConversationBranchCommunication', target, enabled),
35
37
  renameConversation: (id, title) => ipcRenderer.invoke('agent:renameConversation', id, title),
36
38
  reorderConversations: (ids) => ipcRenderer.invoke('agent:reorderConversations', ids),
37
39
  browserRegisterGuest: (guestContentsId, target) => ipcRenderer.invoke('browser:registerGuest', guestContentsId, target),
38
40
  onBrowserEnsureGuest: (callback) => {
39
41
  ipcRenderer.on('browser:ensureGuest', (_event, target) => callback(target));
40
42
  },
43
+ onKeyboardCommand: (callback) => {
44
+ ipcRenderer.on('keyboard:command', (_event, payload) => callback(payload));
45
+ },
41
46
  browserControl: (request) => ipcRenderer.invoke('browser:control', request),
42
47
  computerUseState: (target) => ipcRenderer.invoke('agent:computerUseState', target),
43
48
  setComputerUseEnabled: (target, enabled) => ipcRenderer.invoke('agent:setComputerUseEnabled', target, enabled),
@@ -72,6 +77,10 @@ contextBridge.exposeInMainWorld('api', {
72
77
  readWorkspacePrompt: () => ipcRenderer.invoke('workspace:readPrompt'),
73
78
  saveWorkspacePrompt: (content) => ipcRenderer.invoke('workspace:savePrompt', content),
74
79
  editorComplete: (request) => ipcRenderer.invoke('agent:editorComplete', request),
80
+ editorCompleteCancel: () => ipcRenderer.invoke('agent:editorCompleteCancel'),
81
+ onEditorCompletionDelta: (callback) => {
82
+ ipcRenderer.on('agent:editorCompletionDelta', (_event, payload) => callback(payload));
83
+ },
75
84
  editorAssist: (request) => ipcRenderer.invoke('agent:editorAssist', request),
76
85
  filePathForFile: (file) => {
77
86
  try {
@@ -85,6 +94,7 @@ contextBridge.exposeInMainWorld('api', {
85
94
  selectFolder: () => ipcRenderer.invoke('dialog:selectFolder'),
86
95
  executeBash: (cmd, shell, cwd) => ipcRenderer.invoke('agent:executeBash', cmd, shell, cwd),
87
96
  openExternal: (path) => ipcRenderer.invoke('agent:openExternal', path),
97
+ openWebUrl: (url) => ipcRenderer.invoke('app:openWebUrl', url),
88
98
  selectWorkspace: (id) => ipcRenderer.invoke('agent:selectWorkspace', id),
89
99
  createWorkspace: (name) => ipcRenderer.invoke('agent:createWorkspace', name),
90
100
  createExternalWorkspace: (name, dirPath) => ipcRenderer.invoke('agent:createExternalWorkspace', name, dirPath),
@@ -113,6 +123,7 @@ contextBridge.exposeInMainWorld('api', {
113
123
  removeSkill: (name) => ipcRenderer.invoke('skills:remove', name),
114
124
  refreshSkills: () => ipcRenderer.invoke('skills:refresh'),
115
125
  listMcpServers: () => ipcRenderer.invoke('mcp:list'),
126
+ discoverDshCompatibility: () => ipcRenderer.invoke('dsh:discover'),
116
127
  upsertMcpServer: (input) => ipcRenderer.invoke('mcp:upsert', input),
117
128
  setMcpServerEnabled: (id, enabled) => ipcRenderer.invoke('mcp:setEnabled', id, enabled),
118
129
  removeMcpServer: (id) => ipcRenderer.invoke('mcp:remove', id),
@@ -48,6 +48,10 @@ class ChatCompletionsAdapter {
48
48
  };
49
49
  if (request.reasoningEffort)
50
50
  body.reasoning_effort = request.reasoningEffort;
51
+ // 会话标识透传:仅当上层(支持 session_id 语义的 provider)显式填充时写进
52
+ // body,否则省略,避免严格 API 拒绝未知字段。
53
+ if (request.sessionId)
54
+ body.session_id = request.sessionId;
51
55
  const base = request.baseUrl.replace(/\/+$/, '');
52
56
  return {
53
57
  url: `${base}/chat/completions`,
@@ -90,7 +94,10 @@ class ChatCompletionsAdapter {
90
94
  }
91
95
  const decoder = new TextDecoder();
92
96
  let buffer = '';
93
- let currentToolCall = null;
97
+ const toolCalls = new Map();
98
+ const toolCallOrder = [];
99
+ let syntheticToolIndex = 0;
100
+ let lastToolIndex = 0;
94
101
  let contentPolicyBlocked = false;
95
102
  let emittedContent = false;
96
103
  let emittedTool = false;
@@ -137,32 +144,51 @@ class ChatCompletionsAdapter {
137
144
  emittedContent = true;
138
145
  yield { type: 'text.delta', delta: textDelta };
139
146
  }
140
- const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
141
- for (const raw of toolCalls) {
147
+ const deltaToolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
148
+ for (const raw of deltaToolCalls) {
142
149
  const tc = raw;
143
150
  const fn = tc.function && typeof tc.function === 'object' ? tc.function : {};
144
- if (tc.id) {
145
- if (currentToolCall) {
146
- emittedTool = true;
147
- yield { type: 'tool_call.completed', id: currentToolCall.id, name: currentToolCall.name, arguments: currentToolCall.arguments };
148
- }
151
+ const rawIndex = Number(tc.index);
152
+ const index = Number.isInteger(rawIndex) && rawIndex >= 0
153
+ ? rawIndex
154
+ : (tc.id ? syntheticToolIndex++ : lastToolIndex);
155
+ lastToolIndex = index;
156
+ let currentToolCall = toolCalls.get(index);
157
+ if (!currentToolCall && tc.id) {
149
158
  currentToolCall = {
150
159
  id: String(tc.id || ''),
151
160
  name: (0, chat_messages_1.openAIToolName)(String(fn.name || '')),
152
- arguments: String(fn.arguments || ''),
161
+ argumentParts: [],
153
162
  };
163
+ toolCalls.set(index, currentToolCall);
164
+ toolCallOrder.push(index);
154
165
  yield { type: 'tool_call.started', id: currentToolCall.id, name: currentToolCall.name };
155
166
  }
156
- else if (fn.arguments && currentToolCall) {
157
- currentToolCall.arguments += String(fn.arguments);
158
- yield { type: 'tool_call.arguments.delta', id: currentToolCall.id, delta: String(fn.arguments) };
167
+ if (currentToolCall && fn.name && !currentToolCall.name)
168
+ currentToolCall.name = (0, chat_messages_1.openAIToolName)(String(fn.name));
169
+ if (currentToolCall && fn.arguments !== undefined && fn.arguments !== null) {
170
+ const argumentDelta = String(fn.arguments);
171
+ if (argumentDelta) {
172
+ currentToolCall.argumentParts.push(argumentDelta);
173
+ yield { type: 'tool_call.arguments.delta', id: currentToolCall.id, delta: argumentDelta };
174
+ }
159
175
  }
160
176
  }
161
177
  }
162
178
  }
163
- if (currentToolCall && currentToolCall.arguments) {
164
- emittedTool = true;
165
- yield { type: 'tool_call.completed', id: currentToolCall.id, name: currentToolCall.name, arguments: currentToolCall.arguments };
179
+ if (toolCallOrder.length) {
180
+ for (const index of toolCallOrder) {
181
+ const currentToolCall = toolCalls.get(index);
182
+ if (!currentToolCall)
183
+ continue;
184
+ emittedTool = true;
185
+ yield {
186
+ type: 'tool_call.completed',
187
+ id: currentToolCall.id,
188
+ name: currentToolCall.name,
189
+ arguments: currentToolCall.argumentParts.join(''),
190
+ };
191
+ }
166
192
  }
167
193
  else if (!emittedContent && !emittedTool && contentPolicyBlocked) {
168
194
  yield { type: 'response.failed', error: '[Error] Content policy refusal (content_filter).' };
@@ -55,6 +55,9 @@ export interface NormalizedAgentRequest {
55
55
  reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
56
56
  apiKey: string;
57
57
  baseUrl: string;
58
+ /** 可选的会话标识。仅当目标 provider 显式支持 session_id 语义的上下文缓存
59
+ * 时才由上层填充;adapter 在存在时透传,否则省略该字段(避免严格 API 拒绝未知字段)。 */
60
+ sessionId?: string;
58
61
  }
59
62
  export interface TokenEstimate {
60
63
  inputTokens: number;
@@ -1,4 +1,4 @@
1
- import { ToolDescriptor, ToolIdempotency, RiskLevel } from '../../context/domain/types';
1
+ import { ToolDescriptor, ToolIdempotency, RiskLevel, ToolExecuteFn, ToolConcurrencySafeFn, ToolRenderFn, ToolPresentationMetaFn, ToolFinalizeContentFn, ToolPresentCallFn, ToolPresentResultFn } from '../../context/domain/types';
2
2
  export interface ToolDescriptorInput {
3
3
  toolId: string;
4
4
  capabilityId: string;
@@ -15,6 +15,18 @@ export interface ToolDescriptorInput {
15
15
  supportedScopes?: string[];
16
16
  cacheGroup?: string;
17
17
  implementationHash?: string;
18
+ /** DSH ToolDefinition.execute:命令式功能实现引用(cordis 核功能承载)。 */
19
+ execute?: ToolExecuteFn;
20
+ /** DSH ToolDefinition.isConcurrencySafe:运行时并发分类。 */
21
+ isConcurrencySafe?: ToolConcurrencySafeFn;
22
+ /** DSH ToolOutputDefinition.render / presentationMeta。 */
23
+ render?: ToolRenderFn;
24
+ presentationMeta?: ToolPresentationMetaFn;
25
+ /** DSH ToolDefinition.finalizeContent / timeoutMs / presentCall / presentResult。 */
26
+ finalizeContent?: ToolFinalizeContentFn;
27
+ timeoutMs?: number;
28
+ presentCall?: ToolPresentCallFn;
29
+ presentResult?: ToolPresentResultFn;
18
30
  }
19
31
  /**
20
32
  * Tool Registry: the authoritative set of ToolDescriptors. Schemas are
@@ -28,6 +28,14 @@ class ToolRegistry {
28
28
  implementationHash: input.implementationHash,
29
29
  cacheGroup: input.cacheGroup || `${input.namespace}.${input.name}`,
30
30
  enabled: true,
31
+ execute: input.execute,
32
+ isConcurrencySafe: input.isConcurrencySafe,
33
+ render: input.render,
34
+ presentationMeta: input.presentationMeta,
35
+ finalizeContent: input.finalizeContent,
36
+ timeoutMs: input.timeoutMs,
37
+ presentCall: input.presentCall,
38
+ presentResult: input.presentResult,
31
39
  };
32
40
  this.tools.set(input.toolId, descriptor);
33
41
  return descriptor;
@@ -60,15 +60,28 @@ function inferRiskLevel(name, description, annotations) {
60
60
  return 'external';
61
61
  if (READ_TOOL_PATTERN.test(name))
62
62
  return 'read';
63
+ // DSH 读工具命名惯例(get_/list_/query_/inspect_/read_ 前缀):这些动词本质只读,
64
+ // 避免被启发式误判为 write(例如 DSH 的 get_goal / cordis_inspect_list)。
65
+ if (/^(get_|list_|query_|inspect_|read_)/.test(name) && !/_(create|update|set|write|delete|remove|run|execute|send|save|push|edit|toggle|define|stop|start)$/.test(name))
66
+ return 'read';
63
67
  return 'write';
64
68
  }
65
69
  function inferIdempotency(name) {
66
- if (/^(bash|computer_use|browser_use|run|exec|execute|task)$/.test(name))
70
+ // shell 命令工具每次执行都可能改变外部状态,本质非幂等(DSH pwsh/bash/terminal 等)。
71
+ if (/^(bash|pwsh|powershell|cmd|shell|terminal|computer_use|browser_use|run|exec|execute|task)$/.test(name))
67
72
  return 'non_idempotent';
68
73
  if (/^(write|edit|append|send|save|create|update|set|put|register|patch)/.test(name))
69
74
  return 'conditionally_idempotent';
70
75
  return undefined;
71
76
  }
77
+ /** 从真实 tool description 提取简洁 shortDescription(首句或截断),保 cordis 核可读。 */
78
+ function compactDescription(description, fallback) {
79
+ const clean = String(description || '').replace(/\s+/g, ' ').trim();
80
+ if (!clean)
81
+ return fallback;
82
+ const firstSentence = clean.split(/(?<=[.!?])\s+/)[0] || clean;
83
+ return firstSentence.slice(0, 120);
84
+ }
72
85
  function resolveDefinition(definition) {
73
86
  if (!definition || typeof definition !== 'object')
74
87
  return null;
@@ -82,11 +95,35 @@ function resolveDefinition(definition) {
82
95
  };
83
96
  }
84
97
  if (typeof record.name === 'string') {
98
+ // 兼容两种字段名:Newmark 的 SeededToolDefinition 用 inputSchema,
99
+ // DSH 的 ToolSchema 用 parameters(dsh-llm ToolSchema:{name, description, parameters})。
100
+ const rawParameters = record.inputSchema ?? record.parameters;
101
+ const rawExecute = record.execute;
102
+ const rawConcurrencySafe = record.isConcurrencySafe;
103
+ // DSH ToolOutputDefinition 是嵌套对象 {schema, render, presentationMeta};
104
+ // outputSchema 从 output.schema 提取。
105
+ const rawOutput = record.output;
106
+ const outputSchema = record.outputSchema ?? rawOutput?.schema;
107
+ const render = rawOutput?.render;
108
+ const presentationMeta = rawOutput?.presentationMeta;
109
+ const finalizeContent = record.finalizeContent;
110
+ const timeoutMs = record.timeoutMs;
111
+ const presentCall = record.presentCall;
112
+ const presentResult = record.presentResult;
85
113
  return {
86
114
  name: record.name,
87
115
  description: typeof record.description === 'string' ? record.description : '',
88
- parameters: record.inputSchema,
116
+ parameters: rawParameters,
117
+ outputSchema,
89
118
  annotations: record.annotations,
119
+ execute: typeof rawExecute === 'function' ? rawExecute : undefined,
120
+ isConcurrencySafe: typeof rawConcurrencySafe === 'function' ? rawConcurrencySafe : undefined,
121
+ render: typeof render === 'function' ? render : undefined,
122
+ presentationMeta: typeof presentationMeta === 'function' ? presentationMeta : undefined,
123
+ finalizeContent: typeof finalizeContent === 'function' ? finalizeContent : undefined,
124
+ timeoutMs: typeof timeoutMs === 'number' ? timeoutMs : undefined,
125
+ presentCall: typeof presentCall === 'function' ? presentCall : undefined,
126
+ presentResult: typeof presentResult === 'function' ? presentResult : undefined,
90
127
  };
91
128
  }
92
129
  return null;
@@ -135,7 +172,7 @@ function seedToolchainFromDefinitions(definitions, options) {
135
172
  if (riskLevel === 'destructive' || (riskLevel === 'external' && entry.input.riskLevel !== 'destructive')) {
136
173
  entry.input.riskLevel = riskLevel;
137
174
  }
138
- entry.resolved.push({ name: definition.name, riskLevel, parameters: definition.parameters });
175
+ entry.resolved.push({ ...definition, riskLevel, domain });
139
176
  }
140
177
  for (const [domain, entry] of byDomain) {
141
178
  const requiredPermissions = entry.input.riskLevel === 'destructive'
@@ -166,13 +203,22 @@ function seedToolchainFromDefinitions(definitions, options) {
166
203
  namespace,
167
204
  name: tool.name,
168
205
  version,
169
- shortDescription: tool.name,
170
- fullDescription: `${tool.name} (${domain})`,
206
+ shortDescription: compactDescription(tool.description, tool.name),
207
+ fullDescription: tool.description && tool.description.trim() ? tool.description : `${tool.name} (${domain})`,
171
208
  inputSchema: tool.parameters ?? { type: 'object', properties: {}, required: [] },
209
+ outputSchema: tool.outputSchema,
172
210
  riskLevel: tool.riskLevel,
173
211
  idempotency,
174
212
  requiredPermissions: required,
175
213
  implementationHash: (0, deterministic_1.sha256)(tool.name),
214
+ execute: tool.execute,
215
+ isConcurrencySafe: tool.isConcurrencySafe,
216
+ render: tool.render,
217
+ presentationMeta: tool.presentationMeta,
218
+ finalizeContent: tool.finalizeContent,
219
+ timeoutMs: tool.timeoutMs,
220
+ presentCall: tool.presentCall,
221
+ presentResult: tool.presentResult,
176
222
  };
177
223
  core.registry.register(input);
178
224
  toolIds.push(tool.name);