nextclaw 0.8.62 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/cli/index.js +358 -67
  2. package/package.json +4 -4
  3. package/ui-dist/assets/ChannelsList-CkCpHSto.js +1 -0
  4. package/ui-dist/assets/ChatPage-DM4XNsrW.js +32 -0
  5. package/ui-dist/assets/DocBrowser-B5Aqiz6W.js +1 -0
  6. package/ui-dist/assets/MarketplacePage-BIi0bBdW.js +49 -0
  7. package/ui-dist/assets/ModelConfig-BTFiEAxQ.js +1 -0
  8. package/ui-dist/assets/{ProvidersList-BXHpjVtO.js → ProvidersList-cdk1d-G_.js} +1 -1
  9. package/ui-dist/assets/RuntimeConfig-CFqFsXmR.js +1 -0
  10. package/ui-dist/assets/{SecretsConfig-KkgMzdt1.js → SecretsConfig-CIKasCek.js} +2 -2
  11. package/ui-dist/assets/SessionsConfig-mnCLFtbo.js +2 -0
  12. package/ui-dist/assets/{card-D7NY0Szf.js → card-C1BUfR85.js} +1 -1
  13. package/ui-dist/assets/index-Dxas8MJ9.js +2 -0
  14. package/ui-dist/assets/index-P4YzN9iS.css +1 -0
  15. package/ui-dist/assets/{label-Ojs7Al6B.js → label-CwWfYbuj.js} +1 -1
  16. package/ui-dist/assets/{logos-B1qBsCSi.js → logos-DDyjHSEU.js} +1 -1
  17. package/ui-dist/assets/{page-layout-CUMMO0nN.js → page-layout-DKTRKcHL.js} +1 -1
  18. package/ui-dist/assets/provider-models-y4mUDcGF.js +1 -0
  19. package/ui-dist/assets/{switch-BdhS_16-.js → switch-Bi3yeYiC.js} +1 -1
  20. package/ui-dist/assets/{tabs-custom-D261E5EA.js → tabs-custom-HZFNZrc0.js} +1 -1
  21. package/ui-dist/assets/useConfig-CgzVQTZl.js +6 -0
  22. package/ui-dist/assets/{useConfirmDialog-BUKGHDL6.js → useConfirmDialog-DwD21HlD.js} +2 -2
  23. package/ui-dist/assets/{vendor-Dh04PGww.js → vendor-Ylg6Wdt_.js} +84 -69
  24. package/ui-dist/index.html +3 -3
  25. package/ui-dist/assets/ChannelsList-C8cguFLc.js +0 -1
  26. package/ui-dist/assets/ChatPage-BkHWNUNR.js +0 -32
  27. package/ui-dist/assets/CronConfig-D-ESQlvk.js +0 -1
  28. package/ui-dist/assets/DocBrowser-B9ZD6pAk.js +0 -1
  29. package/ui-dist/assets/MarketplacePage-Ds_l9KTF.js +0 -49
  30. package/ui-dist/assets/ModelConfig-N1tbLv9b.js +0 -1
  31. package/ui-dist/assets/RuntimeConfig-KsKfkjgv.js +0 -1
  32. package/ui-dist/assets/SessionsConfig-CWBp8IPf.js +0 -2
  33. package/ui-dist/assets/index-BRBYYgR_.js +0 -2
  34. package/ui-dist/assets/index-C5cdRzpO.css +0 -1
  35. package/ui-dist/assets/useConfig-txxbxXnT.js +0 -6
package/dist/cli/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  getWorkspacePath as getWorkspacePath6,
15
15
  expandHome as expandHome2,
16
16
  MessageBus as MessageBus2,
17
- AgentLoop as AgentLoop2,
17
+ AgentLoop,
18
18
  ProviderManager as ProviderManager2,
19
19
  resolveConfigSecrets as resolveConfigSecrets3,
20
20
  APP_NAME as APP_NAME4,
@@ -598,6 +598,7 @@ function loadPluginRegistry(config2, workspaceDir) {
598
598
  ],
599
599
  reservedChannelIds: [],
600
600
  reservedProviderIds: PROVIDERS.map((provider) => provider.name),
601
+ reservedEngineKinds: ["native"],
601
602
  logger: {
602
603
  info: (message) => console.log(message),
603
604
  warn: (message) => console.warn(message),
@@ -620,6 +621,12 @@ function toExtensionRegistry(pluginRegistry) {
620
621
  channel: channel.channel,
621
622
  source: channel.source
622
623
  })),
624
+ engines: pluginRegistry.engines.map((engine) => ({
625
+ extensionId: engine.pluginId,
626
+ kind: engine.kind,
627
+ factory: engine.factory,
628
+ source: engine.source
629
+ })),
623
630
  diagnostics: pluginRegistry.diagnostics.map((diag) => ({
624
631
  level: diag.level,
625
632
  message: diag.message,
@@ -685,7 +692,8 @@ var PluginCommands = class {
685
692
  config: config2,
686
693
  workspaceDir,
687
694
  reservedChannelIds: [],
688
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
695
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name),
696
+ reservedEngineKinds: ["native"]
689
697
  });
690
698
  const list = opts.enabled ? report.plugins.filter((plugin) => plugin.status === "loaded") : report.plugins;
691
699
  if (opts.json) {
@@ -729,6 +737,9 @@ var PluginCommands = class {
729
737
  if (plugin.providerIds.length > 0) {
730
738
  console.log(` providers: ${plugin.providerIds.join(", ")}`);
731
739
  }
740
+ if (plugin.engineKinds.length > 0) {
741
+ console.log(` engines: ${plugin.engineKinds.join(", ")}`);
742
+ }
732
743
  if (plugin.error) {
733
744
  console.log(` error: ${plugin.error}`);
734
745
  }
@@ -742,7 +753,8 @@ var PluginCommands = class {
742
753
  config: config2,
743
754
  workspaceDir,
744
755
  reservedChannelIds: [],
745
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
756
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name),
757
+ reservedEngineKinds: ["native"]
746
758
  });
747
759
  const plugin = report.plugins.find((entry) => entry.id === id || entry.name === id);
748
760
  if (!plugin) {
@@ -778,6 +790,9 @@ var PluginCommands = class {
778
790
  if (plugin.providerIds.length > 0) {
779
791
  lines.push(`Providers: ${plugin.providerIds.join(", ")}`);
780
792
  }
793
+ if (plugin.engineKinds.length > 0) {
794
+ lines.push(`Engines: ${plugin.engineKinds.join(", ")}`);
795
+ }
781
796
  if (plugin.error) {
782
797
  lines.push(`Error: ${plugin.error}`);
783
798
  }
@@ -823,7 +838,8 @@ var PluginCommands = class {
823
838
  config: config2,
824
839
  workspaceDir,
825
840
  reservedChannelIds: [],
826
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
841
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name),
842
+ reservedEngineKinds: ["native"]
827
843
  });
828
844
  const keepFiles = Boolean(opts.keepFiles || opts.keepConfig);
829
845
  if (opts.keepConfig) {
@@ -1005,7 +1021,8 @@ var PluginCommands = class {
1005
1021
  config: config2,
1006
1022
  workspaceDir,
1007
1023
  reservedChannelIds: [],
1008
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1024
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name),
1025
+ reservedEngineKinds: ["native"]
1009
1026
  });
1010
1027
  const pluginErrors = report.plugins.filter((plugin) => plugin.status === "error");
1011
1028
  const diagnostics = report.diagnostics.filter((diag) => diag.level === "error");
@@ -2230,6 +2247,8 @@ import { startUiServer } from "@nextclaw/server";
2230
2247
  import { closeSync, cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, openSync, rmSync as rmSync3 } from "fs";
2231
2248
  import { dirname, isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve7 } from "path";
2232
2249
  import { spawn as spawn2 } from "child_process";
2250
+ import { request as httpRequest } from "http";
2251
+ import { request as httpsRequest } from "https";
2233
2252
  import { fileURLToPath as fileURLToPath2 } from "url";
2234
2253
  import chokidar from "chokidar";
2235
2254
 
@@ -2676,7 +2695,7 @@ var MissingProvider = class extends LLMProvider {
2676
2695
  setDefaultModel(model) {
2677
2696
  this.defaultModel = model;
2678
2697
  }
2679
- async chat() {
2698
+ async chat(_params) {
2680
2699
  throw new Error("No API key configured yet. Configure provider credentials in UI and retry.");
2681
2700
  }
2682
2701
  getDefaultModel() {
@@ -2686,7 +2705,7 @@ var MissingProvider = class extends LLMProvider {
2686
2705
 
2687
2706
  // src/cli/commands/agent-runtime-pool.ts
2688
2707
  import {
2689
- AgentLoop,
2708
+ NativeAgentEngine,
2690
2709
  AgentRouteResolver,
2691
2710
  getWorkspacePath as getWorkspacePath4,
2692
2711
  parseAgentScopedSessionKey
@@ -2695,6 +2714,19 @@ function normalizeAgentId(value) {
2695
2714
  const text = (value ?? "").trim().toLowerCase();
2696
2715
  return text || "main";
2697
2716
  }
2717
+ function normalizeEngineKind(value) {
2718
+ if (typeof value !== "string") {
2719
+ return "native";
2720
+ }
2721
+ const kind = value.trim().toLowerCase();
2722
+ return kind || "native";
2723
+ }
2724
+ function toRecord(value) {
2725
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2726
+ return void 0;
2727
+ }
2728
+ return value;
2729
+ }
2698
2730
  function resolveAgentProfiles(config2) {
2699
2731
  const defaults = config2.agents.defaults;
2700
2732
  const listed = Array.isArray(config2.agents.list) ? config2.agents.list.map((entry) => ({
@@ -2702,6 +2734,8 @@ function resolveAgentProfiles(config2) {
2702
2734
  default: entry.default,
2703
2735
  workspace: entry.workspace,
2704
2736
  model: entry.model,
2737
+ engine: entry.engine,
2738
+ engineConfig: toRecord(entry.engineConfig),
2705
2739
  maxToolIterations: entry.maxToolIterations,
2706
2740
  contextTokens: entry.contextTokens
2707
2741
  })).filter((entry) => Boolean(entry.id)) : [];
@@ -2720,6 +2754,8 @@ function resolveAgentProfiles(config2) {
2720
2754
  id: entry.id,
2721
2755
  workspace: getWorkspacePath4(entry.workspace ?? defaults.workspace),
2722
2756
  model: entry.model ?? defaults.model,
2757
+ engine: normalizeEngineKind(entry.engine ?? defaults.engine),
2758
+ engineConfig: entry.engineConfig ?? toRecord(defaults.engineConfig),
2723
2759
  maxIterations: entry.maxToolIterations ?? defaults.maxToolIterations,
2724
2760
  contextTokens: entry.contextTokens ?? defaults.contextTokens
2725
2761
  }));
@@ -2751,32 +2787,48 @@ var GatewayAgentRuntimePool = class {
2751
2787
  this.rebuild(this.options.config);
2752
2788
  }
2753
2789
  async processDirect(params) {
2754
- const message = {
2755
- channel: params.channel ?? "cli",
2756
- senderId: "user",
2757
- chatId: params.chatId ?? "direct",
2790
+ const { message, route } = this.resolveDirectRoute({
2758
2791
  content: params.content,
2759
- timestamp: /* @__PURE__ */ new Date(),
2760
- attachments: [],
2761
- metadata: params.metadata ?? {}
2762
- };
2763
- const forcedAgentId = this.readString(params.agentId) ?? parseAgentScopedSessionKey(params.sessionKey)?.agentId ?? void 0;
2764
- const route = this.routeResolver.resolveInbound({
2765
- message,
2766
- forcedAgentId,
2767
- sessionKeyOverride: params.sessionKey
2792
+ sessionKey: params.sessionKey,
2793
+ channel: params.channel,
2794
+ chatId: params.chatId,
2795
+ metadata: params.metadata,
2796
+ agentId: params.agentId
2768
2797
  });
2769
2798
  const runtime2 = this.resolveRuntime(route.agentId);
2770
- return runtime2.loop.processDirect({
2799
+ return runtime2.engine.processDirect({
2771
2800
  content: params.content,
2772
2801
  sessionKey: route.sessionKey,
2773
2802
  channel: message.channel,
2774
2803
  chatId: message.chatId,
2775
2804
  metadata: message.metadata,
2805
+ abortSignal: params.abortSignal,
2776
2806
  onAssistantDelta: params.onAssistantDelta,
2777
2807
  onSessionEvent: params.onSessionEvent
2778
2808
  });
2779
2809
  }
2810
+ supportsTurnAbort(params) {
2811
+ const { route } = this.resolveDirectRoute({
2812
+ content: "",
2813
+ sessionKey: params.sessionKey,
2814
+ channel: params.channel,
2815
+ chatId: params.chatId,
2816
+ metadata: params.metadata,
2817
+ agentId: params.agentId
2818
+ });
2819
+ const runtime2 = this.resolveRuntime(route.agentId);
2820
+ if (runtime2.engine.kind !== "native") {
2821
+ return {
2822
+ supported: false,
2823
+ agentId: route.agentId,
2824
+ reason: `engine "${runtime2.engine.kind}" does not support server-side stop yet`
2825
+ };
2826
+ }
2827
+ return {
2828
+ supported: true,
2829
+ agentId: route.agentId
2830
+ };
2831
+ }
2780
2832
  async run() {
2781
2833
  this.running = true;
2782
2834
  while (this.running) {
@@ -2790,7 +2842,7 @@ var GatewayAgentRuntimePool = class {
2790
2842
  sessionKeyOverride: explicitSessionKey
2791
2843
  });
2792
2844
  const runtime2 = this.resolveRuntime(route.agentId);
2793
- await runtime2.loop.handleInbound({
2845
+ await runtime2.engine.handleInbound({
2794
2846
  message,
2795
2847
  sessionKey: route.sessionKey,
2796
2848
  publishResponse: true
@@ -2813,6 +2865,27 @@ var GatewayAgentRuntimePool = class {
2813
2865
  const trimmed = value.trim();
2814
2866
  return trimmed || void 0;
2815
2867
  }
2868
+ resolveDirectRoute(params) {
2869
+ const message = {
2870
+ channel: params.channel ?? "cli",
2871
+ senderId: "user",
2872
+ chatId: params.chatId ?? "direct",
2873
+ content: params.content,
2874
+ timestamp: /* @__PURE__ */ new Date(),
2875
+ attachments: [],
2876
+ metadata: params.metadata ?? {}
2877
+ };
2878
+ const forcedAgentId = this.readString(params.agentId) ?? parseAgentScopedSessionKey(params.sessionKey)?.agentId ?? void 0;
2879
+ const route = this.routeResolver.resolveInbound({
2880
+ message,
2881
+ forcedAgentId,
2882
+ sessionKeyOverride: params.sessionKey
2883
+ });
2884
+ return {
2885
+ message,
2886
+ route
2887
+ };
2888
+ }
2816
2889
  resolveRuntime(agentId) {
2817
2890
  const normalized = normalizeAgentId(agentId);
2818
2891
  const runtime2 = this.runtimes.get(normalized);
@@ -2825,34 +2898,82 @@ var GatewayAgentRuntimePool = class {
2825
2898
  }
2826
2899
  throw new Error("No agent runtime available");
2827
2900
  }
2901
+ createNativeEngineFactory() {
2902
+ return (context) => new NativeAgentEngine({
2903
+ bus: context.bus,
2904
+ providerManager: context.providerManager,
2905
+ workspace: context.workspace,
2906
+ model: context.model,
2907
+ maxIterations: context.maxIterations,
2908
+ contextTokens: context.contextTokens,
2909
+ braveApiKey: context.braveApiKey,
2910
+ execConfig: context.execConfig,
2911
+ cronService: context.cronService,
2912
+ restrictToWorkspace: context.restrictToWorkspace,
2913
+ sessionManager: context.sessionManager,
2914
+ contextConfig: context.contextConfig,
2915
+ gatewayController: context.gatewayController,
2916
+ config: context.config,
2917
+ extensionRegistry: context.extensionRegistry,
2918
+ resolveMessageToolHints: context.resolveMessageToolHints,
2919
+ agentId: context.agentId
2920
+ });
2921
+ }
2922
+ resolveEngineFactory(kind) {
2923
+ if (kind === "native") {
2924
+ return this.createNativeEngineFactory();
2925
+ }
2926
+ const registrations = this.options.extensionRegistry?.engines ?? [];
2927
+ const matched = registrations.find((entry) => normalizeEngineKind(entry.kind) === kind);
2928
+ if (matched) {
2929
+ return matched.factory;
2930
+ }
2931
+ console.warn(`[engine] unknown engine "${kind}", fallback to "native"`);
2932
+ return this.createNativeEngineFactory();
2933
+ }
2934
+ createEngine(profile, config2) {
2935
+ const kind = normalizeEngineKind(profile.engine);
2936
+ const factory = this.resolveEngineFactory(kind);
2937
+ const context = {
2938
+ agentId: profile.id,
2939
+ workspace: profile.workspace,
2940
+ model: profile.model,
2941
+ maxIterations: profile.maxIterations,
2942
+ contextTokens: profile.contextTokens,
2943
+ engineConfig: profile.engineConfig,
2944
+ bus: this.options.bus,
2945
+ providerManager: this.options.providerManager,
2946
+ sessionManager: this.options.sessionManager,
2947
+ cronService: this.options.cronService,
2948
+ restrictToWorkspace: this.options.restrictToWorkspace,
2949
+ braveApiKey: this.options.braveApiKey,
2950
+ execConfig: this.options.execConfig,
2951
+ contextConfig: this.options.contextConfig,
2952
+ gatewayController: this.options.gatewayController,
2953
+ config: config2,
2954
+ extensionRegistry: this.options.extensionRegistry,
2955
+ resolveMessageToolHints: this.options.resolveMessageToolHints
2956
+ };
2957
+ try {
2958
+ return factory(context);
2959
+ } catch (error) {
2960
+ if (kind === "native") {
2961
+ throw error;
2962
+ }
2963
+ console.warn(`[engine] failed to create "${kind}" for agent "${profile.id}": ${String(error)}`);
2964
+ return this.createNativeEngineFactory()(context);
2965
+ }
2966
+ }
2828
2967
  rebuild(config2) {
2829
2968
  const profiles = resolveAgentProfiles(config2);
2830
2969
  const configuredDefault = this.readString(config2.agents.list.find((entry) => entry.default)?.id);
2831
2970
  this.defaultAgentId = configuredDefault ?? profiles[0]?.id ?? "main";
2832
2971
  const nextRuntimes = /* @__PURE__ */ new Map();
2833
2972
  for (const profile of profiles) {
2834
- const loop = new AgentLoop({
2835
- bus: this.options.bus,
2836
- providerManager: this.options.providerManager,
2837
- workspace: profile.workspace,
2838
- model: profile.model,
2839
- maxIterations: profile.maxIterations,
2840
- contextTokens: profile.contextTokens,
2841
- braveApiKey: this.options.braveApiKey,
2842
- execConfig: this.options.execConfig,
2843
- cronService: this.options.cronService,
2844
- restrictToWorkspace: this.options.restrictToWorkspace,
2845
- sessionManager: this.options.sessionManager,
2846
- contextConfig: this.options.contextConfig,
2847
- gatewayController: this.options.gatewayController,
2848
- config: config2,
2849
- extensionRegistry: this.options.extensionRegistry,
2850
- resolveMessageToolHints: this.options.resolveMessageToolHints,
2851
- agentId: profile.id
2852
- });
2973
+ const engine = this.createEngine(profile, config2);
2853
2974
  nextRuntimes.set(profile.id, {
2854
2975
  id: profile.id,
2855
- loop
2976
+ engine
2856
2977
  });
2857
2978
  }
2858
2979
  this.runtimes = nextRuntimes;
@@ -2887,6 +3008,21 @@ function createSkillsLoader(workspace) {
2887
3008
  }
2888
3009
  return new ctor(workspace);
2889
3010
  }
3011
+ function isAbortError(error) {
3012
+ if (error instanceof DOMException && error.name === "AbortError") {
3013
+ return true;
3014
+ }
3015
+ if (error instanceof Error) {
3016
+ if (error.name === "AbortError") {
3017
+ return true;
3018
+ }
3019
+ const message = error.message.toLowerCase();
3020
+ if (message.includes("aborted") || message.includes("abort")) {
3021
+ return true;
3022
+ }
3023
+ }
3024
+ return false;
3025
+ }
2890
3026
  var ServiceCommands = class {
2891
3027
  constructor(deps) {
2892
3028
  this.deps = deps;
@@ -3279,7 +3415,7 @@ var ServiceCommands = class {
3279
3415
  }
3280
3416
  await this.printPublicUiUrls(parsedUi.host, parsedUi.port);
3281
3417
  console.log(`Logs: ${existing.logPath}`);
3282
- console.log(`Stop: ${APP_NAME2} stop`);
3418
+ this.printServiceControlHints();
3283
3419
  return;
3284
3420
  }
3285
3421
  if (existing) {
@@ -3306,12 +3442,20 @@ var ServiceCommands = class {
3306
3442
  return;
3307
3443
  }
3308
3444
  const healthUrl = `${apiUrl}/health`;
3309
- const started = await this.waitForBackgroundServiceReady({
3445
+ let readiness = await this.waitForBackgroundServiceReady({
3310
3446
  pid: child.pid,
3311
3447
  healthUrl,
3312
3448
  timeoutMs: 8e3
3313
3449
  });
3314
- if (!started) {
3450
+ if (!readiness.ready && process.platform === "win32" && isProcessRunning(child.pid)) {
3451
+ console.warn("Warning: Background service is still running but not ready after 8s; waiting up to 20s more on Windows.");
3452
+ readiness = await this.waitForBackgroundServiceReady({
3453
+ pid: child.pid,
3454
+ healthUrl,
3455
+ timeoutMs: 2e4
3456
+ });
3457
+ }
3458
+ if (!readiness.ready) {
3315
3459
  if (isProcessRunning(child.pid)) {
3316
3460
  try {
3317
3461
  process.kill(child.pid, "SIGTERM");
@@ -3320,7 +3464,8 @@ var ServiceCommands = class {
3320
3464
  }
3321
3465
  }
3322
3466
  clearServiceState();
3323
- console.error(`Error: Failed to start background service. Check logs: ${logPath}`);
3467
+ const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
3468
+ console.error(`Error: Failed to start background service. Check logs: ${logPath}.${hint}`);
3324
3469
  return;
3325
3470
  }
3326
3471
  child.unref();
@@ -3339,7 +3484,7 @@ var ServiceCommands = class {
3339
3484
  console.log(`API: ${apiUrl}`);
3340
3485
  await this.printPublicUiUrls(uiConfig.host, uiConfig.port);
3341
3486
  console.log(`Logs: ${logPath}`);
3342
- console.log(`Stop: ${APP_NAME2} stop`);
3487
+ this.printServiceControlHints();
3343
3488
  if (options.open) {
3344
3489
  openBrowser(uiUrl);
3345
3490
  }
@@ -3377,31 +3522,80 @@ var ServiceCommands = class {
3377
3522
  }
3378
3523
  async waitForBackgroundServiceReady(params) {
3379
3524
  const startedAt = Date.now();
3525
+ let lastProbeError = null;
3380
3526
  while (Date.now() - startedAt < params.timeoutMs) {
3381
3527
  if (!isProcessRunning(params.pid)) {
3382
- return false;
3528
+ return { ready: false, lastProbeError };
3383
3529
  }
3384
- try {
3385
- const response = await fetch(params.healthUrl, { method: "GET" });
3386
- if (!response.ok) {
3387
- await new Promise((resolve10) => setTimeout(resolve10, 200));
3388
- continue;
3389
- }
3390
- const payload = await response.json();
3391
- const healthy = payload?.ok === true && payload?.data?.status === "ok";
3392
- if (!healthy) {
3393
- await new Promise((resolve10) => setTimeout(resolve10, 200));
3394
- continue;
3395
- }
3396
- await new Promise((resolve10) => setTimeout(resolve10, 300));
3397
- if (isProcessRunning(params.pid)) {
3398
- return true;
3399
- }
3400
- } catch {
3530
+ const probe = await this.probeHealthEndpoint(params.healthUrl);
3531
+ if (!probe.healthy) {
3532
+ lastProbeError = probe.error;
3533
+ await new Promise((resolve10) => setTimeout(resolve10, 200));
3534
+ continue;
3535
+ }
3536
+ await new Promise((resolve10) => setTimeout(resolve10, 300));
3537
+ if (isProcessRunning(params.pid)) {
3538
+ return { ready: true, lastProbeError: null };
3401
3539
  }
3402
3540
  await new Promise((resolve10) => setTimeout(resolve10, 200));
3403
3541
  }
3404
- return false;
3542
+ return { ready: false, lastProbeError };
3543
+ }
3544
+ async probeHealthEndpoint(healthUrl) {
3545
+ let parsed;
3546
+ try {
3547
+ parsed = new URL(healthUrl);
3548
+ } catch {
3549
+ return { healthy: false, error: "invalid health URL" };
3550
+ }
3551
+ const requestImpl = parsed.protocol === "https:" ? httpsRequest : httpRequest;
3552
+ return new Promise((resolve10) => {
3553
+ const req = requestImpl(
3554
+ {
3555
+ protocol: parsed.protocol,
3556
+ hostname: parsed.hostname,
3557
+ port: parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80,
3558
+ method: "GET",
3559
+ path: `${parsed.pathname}${parsed.search}`,
3560
+ timeout: 1e3,
3561
+ headers: { Accept: "application/json" }
3562
+ },
3563
+ (res) => {
3564
+ const chunks = [];
3565
+ res.on("data", (chunk) => {
3566
+ if (typeof chunk === "string") {
3567
+ chunks.push(Buffer.from(chunk));
3568
+ return;
3569
+ }
3570
+ chunks.push(chunk);
3571
+ });
3572
+ res.on("end", () => {
3573
+ if ((res.statusCode ?? 0) < 200 || (res.statusCode ?? 0) >= 300) {
3574
+ resolve10({ healthy: false, error: `http ${res.statusCode ?? "unknown"}` });
3575
+ return;
3576
+ }
3577
+ try {
3578
+ const payload = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
3579
+ const healthy = payload?.ok === true && payload?.data?.status === "ok";
3580
+ if (!healthy) {
3581
+ resolve10({ healthy: false, error: "health payload not ok" });
3582
+ return;
3583
+ }
3584
+ resolve10({ healthy: true, error: null });
3585
+ } catch {
3586
+ resolve10({ healthy: false, error: "invalid health JSON response" });
3587
+ }
3588
+ });
3589
+ }
3590
+ );
3591
+ req.on("timeout", () => {
3592
+ req.destroy(new Error("probe timeout"));
3593
+ });
3594
+ req.on("error", (error) => {
3595
+ resolve10({ healthy: false, error: error.message || String(error) });
3596
+ });
3597
+ req.end();
3598
+ });
3405
3599
  }
3406
3600
  createMissingProvider(config2) {
3407
3601
  return this.makeMissingProvider(config2);
@@ -3449,10 +3643,23 @@ var ServiceCommands = class {
3449
3643
  console.log(`Public UI (if firewall/NAT allows): ${publicBase}`);
3450
3644
  console.log(`Public API (if firewall/NAT allows): ${publicBase}/api`);
3451
3645
  }
3646
+ printServiceControlHints() {
3647
+ console.log("Service controls:");
3648
+ console.log(` - Check status: ${APP_NAME2} status`);
3649
+ console.log(` - If you need to stop the service, run: ${APP_NAME2} stop`);
3650
+ }
3452
3651
  startUiIfEnabled(uiConfig, uiStaticDir, cronService, runtimePool) {
3453
3652
  if (!uiConfig.enabled) {
3454
3653
  return;
3455
3654
  }
3655
+ const activeTurnRuns = /* @__PURE__ */ new Map();
3656
+ const resolveStopCapability = (params) => runtimePool.supportsTurnAbort({
3657
+ sessionKey: params.sessionKey,
3658
+ agentId: params.agentId,
3659
+ channel: params.channel,
3660
+ chatId: params.chatId,
3661
+ metadata: params.metadata
3662
+ });
3456
3663
  const resolveChatTurnParams = (params) => {
3457
3664
  const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim().length > 0 ? params.sessionKey.trim() : `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
3458
3665
  const inferredAgentId = typeof params.agentId === "string" && params.agentId.trim().length > 0 ? params.agentId.trim() : parseAgentScopedSessionKey2(sessionKey)?.agentId;
@@ -3461,7 +3668,9 @@ var ServiceCommands = class {
3461
3668
  if (model) {
3462
3669
  metadata.model = model;
3463
3670
  }
3671
+ const runId = typeof params.runId === "string" && params.runId.trim().length > 0 ? params.runId.trim() : void 0;
3464
3672
  return {
3673
+ runId,
3465
3674
  sessionKey,
3466
3675
  inferredAgentId,
3467
3676
  model,
@@ -3494,6 +3703,20 @@ var ServiceCommands = class {
3494
3703
  }
3495
3704
  },
3496
3705
  chatRuntime: {
3706
+ getCapabilities: async (params) => {
3707
+ const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim().length > 0 ? params.sessionKey.trim() : `ui:capability:${Date.now().toString(36)}`;
3708
+ const capability = resolveStopCapability({
3709
+ sessionKey,
3710
+ agentId: typeof params.agentId === "string" ? params.agentId : void 0,
3711
+ channel: "ui",
3712
+ chatId: "web-ui",
3713
+ metadata: {}
3714
+ });
3715
+ return {
3716
+ stopSupported: capability.supported,
3717
+ ...capability.reason ? { stopReason: capability.reason } : {}
3718
+ };
3719
+ },
3497
3720
  processTurn: async (params) => {
3498
3721
  const resolved = resolveChatTurnParams(params);
3499
3722
  const reply = await runtimePool.processDirect({
@@ -3511,9 +3734,60 @@ var ServiceCommands = class {
3511
3734
  model: resolved.model
3512
3735
  });
3513
3736
  },
3737
+ stopTurn: async (params) => {
3738
+ const runId = typeof params.runId === "string" ? params.runId.trim() : "";
3739
+ if (!runId) {
3740
+ return {
3741
+ stopped: false,
3742
+ runId: "",
3743
+ reason: "runId is required"
3744
+ };
3745
+ }
3746
+ const active = activeTurnRuns.get(runId);
3747
+ if (!active) {
3748
+ return {
3749
+ stopped: false,
3750
+ runId,
3751
+ ...typeof params.sessionKey === "string" && params.sessionKey.trim().length > 0 ? { sessionKey: params.sessionKey.trim() } : {},
3752
+ reason: "run not found or already completed"
3753
+ };
3754
+ }
3755
+ const requestedSessionKey = typeof params.sessionKey === "string" ? params.sessionKey.trim() : "";
3756
+ if (requestedSessionKey && requestedSessionKey !== active.sessionKey) {
3757
+ return {
3758
+ stopped: false,
3759
+ runId,
3760
+ sessionKey: active.sessionKey,
3761
+ reason: "session key mismatch"
3762
+ };
3763
+ }
3764
+ active.controller.abort(new Error("chat turn stopped by user"));
3765
+ return {
3766
+ stopped: true,
3767
+ runId,
3768
+ sessionKey: active.sessionKey
3769
+ };
3770
+ },
3514
3771
  processTurnStream: async function* (params) {
3515
3772
  const resolved = resolveChatTurnParams(params);
3773
+ const runId = resolved.runId ?? `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
3774
+ const stopCapability = resolveStopCapability({
3775
+ sessionKey: resolved.sessionKey,
3776
+ agentId: resolved.inferredAgentId,
3777
+ channel: resolved.channel,
3778
+ chatId: resolved.chatId,
3779
+ metadata: resolved.metadata
3780
+ });
3781
+ const controller = stopCapability.supported ? new AbortController() : null;
3782
+ if (controller) {
3783
+ activeTurnRuns.set(runId, {
3784
+ controller,
3785
+ sessionKey: resolved.sessionKey,
3786
+ ...resolved.inferredAgentId ? { agentId: resolved.inferredAgentId } : {}
3787
+ });
3788
+ }
3516
3789
  const queue = [];
3790
+ const assistantDeltaParts = [];
3517
3791
  let waiter = null;
3518
3792
  const push = (event) => {
3519
3793
  queue.push(event);
@@ -3528,10 +3802,12 @@ var ServiceCommands = class {
3528
3802
  chatId: resolved.chatId,
3529
3803
  agentId: resolved.inferredAgentId,
3530
3804
  metadata: resolved.metadata,
3805
+ ...controller ? { abortSignal: controller.signal } : {},
3531
3806
  onAssistantDelta: (delta) => {
3532
3807
  if (typeof delta !== "string" || delta.length === 0) {
3533
3808
  return;
3534
3809
  }
3810
+ assistantDeltaParts.push(delta);
3535
3811
  push({ type: "delta", delta });
3536
3812
  },
3537
3813
  onSessionEvent: (event) => {
@@ -3567,7 +3843,22 @@ var ServiceCommands = class {
3567
3843
  })
3568
3844
  });
3569
3845
  }).catch((error) => {
3846
+ if ((controller?.signal.aborted ?? false) || isAbortError(error)) {
3847
+ const partialReply = assistantDeltaParts.join("");
3848
+ push({
3849
+ type: "final",
3850
+ result: buildTurnResult({
3851
+ reply: partialReply,
3852
+ sessionKey: resolved.sessionKey,
3853
+ inferredAgentId: resolved.inferredAgentId,
3854
+ model: resolved.model
3855
+ })
3856
+ });
3857
+ return;
3858
+ }
3570
3859
  push({ type: "error", error: String(error) });
3860
+ }).finally(() => {
3861
+ activeTurnRuns.delete(runId);
3571
3862
  });
3572
3863
  while (true) {
3573
3864
  if (queue.length === 0) {
@@ -4455,7 +4746,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
4455
4746
  defaultProvider: provider,
4456
4747
  config: config2
4457
4748
  });
4458
- const agentLoop = new AgentLoop2({
4749
+ const agentLoop = new AgentLoop({
4459
4750
  bus,
4460
4751
  providerManager,
4461
4752
  workspace,