@schlenkr/ragents 0.1.8 → 0.1.9

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 (174) hide show
  1. package/apps/server/src/config-definition.ts +24 -2
  2. package/apps/server/src/config.ts +0 -6
  3. package/apps/server/src/host-api.json +3 -10
  4. package/apps/server/src/host-api.ts +6 -7
  5. package/apps/server/src/plugin-build/build.ts +28 -5
  6. package/apps/server/src/plugin-build/plugin-description.ts +1 -1
  7. package/apps/server/src/plugin-build/typecheck.ts +2 -2
  8. package/apps/server/src/plugin-support/language-server/plugin.ts +8 -7
  9. package/apps/server/src/plugin-support/language-server/tools.ts +7 -6
  10. package/apps/server/src/plugin-support/model-aliases.ts +55 -25
  11. package/apps/server/src/plugin-support/product-relay.ts +10 -2
  12. package/apps/server/src/plugin-support/workspace-sandbox-host.ts +18 -9
  13. package/apps/server/src/profile/bundle-manifest.ts +7 -3
  14. package/apps/server/src/profile/compose.ts +6 -0
  15. package/apps/server/src/profile/plugin-discovery.ts +6 -1
  16. package/apps/server/src/profile/provisioning.ts +6 -5
  17. package/apps/server/src/ragents/engine.ts +0 -1
  18. package/apps/server/src/ragents/host-services.ts +4 -0
  19. package/apps/server/src/ragents/session.ts +1 -1
  20. package/apps/server/src/server.ts +1 -0
  21. package/apps/server/tsconfig.json +2 -0
  22. package/apps/web/dist/help/guide-clients.html +14 -6
  23. package/apps/web/dist/help/guide-clients.md +14 -6
  24. package/apps/web/dist/help/guide-distributed.html +12 -4
  25. package/apps/web/dist/help/guide-distributed.md +12 -4
  26. package/apps/web/dist/help/guide-getting-started.html +12 -8
  27. package/apps/web/dist/help/guide-getting-started.md +12 -8
  28. package/apps/web/dist/help/guide-plugins.html +6 -2
  29. package/apps/web/dist/help/guide-plugins.md +6 -2
  30. package/apps/web/dist/help/guide-runtime.html +2 -1
  31. package/apps/web/dist/help/guide-runtime.md +2 -1
  32. package/apps/web/dist/help/index.html +3328 -1135
  33. package/apps/web/dist/host-web.json +3 -3
  34. package/bundles/ragents.activity/ragents-bundle.json +3 -3
  35. package/bundles/ragents.actor-programs/ragents-bundle.json +3 -3
  36. package/bundles/ragents.ask/ragents-bundle.json +4 -4
  37. package/bundles/ragents.ask/server/index.js +42 -11
  38. package/bundles/ragents.ask/server/index.js.map +3 -3
  39. package/bundles/ragents.ask/web/chunks/chunk-WFN4374U.js +2 -0
  40. package/bundles/ragents.ask/web/chunks/chunk-WFN4374U.js.map +7 -0
  41. package/bundles/ragents.ask/web/classes.json +1 -1
  42. package/bundles/ragents.ask/web/exports/ask-payload.js +1 -1
  43. package/bundles/ragents.ask/web/index.js +1 -1
  44. package/bundles/ragents.ask/web/index.js.map +3 -3
  45. package/bundles/ragents.browser/executor/index.mjs +357 -0
  46. package/bundles/ragents.browser/executor/index.mjs.map +7 -0
  47. package/bundles/ragents.browser/ragents-bundle.json +5 -8
  48. package/bundles/ragents.browser/server/chunks/{chunk-2GKLLCBE.js → chunk-CWLX4VA5.js} +1 -1
  49. package/bundles/ragents.browser/server/chunks/chunk-CWLX4VA5.js.map +7 -0
  50. package/bundles/ragents.browser/server/exports/server/contract.js +1 -1
  51. package/bundles/ragents.browser/server/index.js +20 -9
  52. package/bundles/ragents.browser/server/index.js.map +4 -4
  53. package/bundles/ragents.documents/ragents-bundle.json +3 -3
  54. package/bundles/ragents.lsp-fsharp/executor/index.mjs +68 -0
  55. package/bundles/ragents.lsp-fsharp/executor/index.mjs.map +7 -0
  56. package/bundles/ragents.lsp-fsharp/ragents-bundle.json +5 -9
  57. package/bundles/ragents.lsp-fsharp/server/index.js +21 -6
  58. package/bundles/ragents.lsp-fsharp/server/index.js.map +4 -4
  59. package/bundles/ragents.lsp-roslyn/executor/index.mjs +68 -0
  60. package/bundles/ragents.lsp-roslyn/executor/index.mjs.map +7 -0
  61. package/bundles/ragents.lsp-roslyn/ragents-bundle.json +6 -8
  62. package/bundles/ragents.lsp-roslyn/server/index.js +54 -15
  63. package/bundles/ragents.lsp-roslyn/server/index.js.map +4 -4
  64. package/bundles/ragents.lsp-typescript/executor/index.mjs +49 -0
  65. package/bundles/ragents.lsp-typescript/executor/index.mjs.map +7 -0
  66. package/bundles/ragents.lsp-typescript/ragents-bundle.json +5 -7
  67. package/bundles/ragents.lsp-typescript/server/index.js +22 -2
  68. package/bundles/ragents.lsp-typescript/server/index.js.map +3 -3
  69. package/bundles/ragents.model-relay/ragents-bundle.json +4 -4
  70. package/bundles/ragents.model-relay/server/index.js +3 -2
  71. package/bundles/ragents.model-relay/server/index.js.map +2 -2
  72. package/bundles/ragents.orchestration/ragents-bundle.json +3 -3
  73. package/bundles/ragents.overseer/ragents-bundle.json +3 -3
  74. package/bundles/ragents.processes/ragents-bundle.json +3 -3
  75. package/bundles/ragents.product/ragents-bundle.json +3 -3
  76. package/bundles/ragents.profile-distribution/ragents-bundle.json +4 -4
  77. package/bundles/ragents.profile-distribution/server/index.js +1 -1
  78. package/bundles/ragents.profile-distribution/server/index.js.map +2 -2
  79. package/bundles/ragents.reference/ragents-bundle.json +3 -3
  80. package/bundles/ragents.todo/ragents-bundle.json +3 -3
  81. package/bundles/ragents.transcript/ragents-bundle.json +3 -3
  82. package/bundles/ragents.watch/ragents-bundle.json +3 -3
  83. package/bundles/ragents.workspace/ragents-bundle.json +11 -5
  84. package/bundles/ragents.workspace/server/index.js +101 -28
  85. package/bundles/ragents.workspace/server/index.js.map +3 -3
  86. package/bundles/ragents.workspace/web/classes.json +1 -1
  87. package/bundles/ragents.workspace/web/index.js +1 -1
  88. package/bundles/ragents.workspace/web/index.js.map +3 -3
  89. package/package.json +2 -2
  90. package/packages/agent/dist/core/agent-settings.d.ts +1 -7
  91. package/packages/agent/dist/core/compaction/compaction.d.ts +17 -13
  92. package/packages/agent/dist/core/model-runtime.d.ts +5 -4
  93. package/packages/agent/dist/core/provider-composer.d.ts +2 -1
  94. package/packages/agent/dist/core/tools/bash.d.ts +9 -3
  95. package/packages/agent/dist/index.d.ts +1 -1
  96. package/packages/agent/src/core/agent-settings.ts +1 -4
  97. package/packages/agent/src/core/compaction/compaction.ts +68 -30
  98. package/packages/agent/src/core/model-runtime.ts +12 -5
  99. package/packages/agent/src/core/provider-composer.ts +5 -0
  100. package/packages/agent/src/core/tools/bash.ts +36 -30
  101. package/packages/agent/src/index.ts +3 -0
  102. package/packages/ai/dist/types.d.ts +11 -0
  103. package/packages/ai/src/types.ts +12 -0
  104. package/packages/ragents/src/domain/event-semantics.ts +3 -0
  105. package/packages/ragents/src/domain/event-validation.ts +15 -2
  106. package/packages/ragents/src/domain/events.ts +3 -0
  107. package/packages/ragents/src/domain/model.ts +2 -0
  108. package/packages/ragents/src/domain/projection.ts +1 -0
  109. package/packages/ragents/src/drivers/agent-runtime.ts +1 -1
  110. package/packages/ragents/src/drivers/agent-turn.ts +8 -6
  111. package/packages/ragents/src/runtime/decisions/inputs.ts +6 -2
  112. package/packages/ragents/src/runtime/journal-storage.ts +5 -4
  113. package/packages/workspace-executor/package.json +0 -1
  114. package/packages/workspace-executor/src/bash-launch.ts +56 -10
  115. package/packages/workspace-executor/src/context.ts +4 -0
  116. package/packages/workspace-executor/src/contributions.ts +150 -0
  117. package/packages/workspace-executor/src/executor.ts +1 -1
  118. package/packages/workspace-executor/src/index.ts +16 -19
  119. package/packages/workspace-executor/src/language-server/host.ts +3 -0
  120. package/packages/workspace-executor/src/language-server/module.ts +12 -2
  121. package/packages/workspace-executor/src/language-server/roots.ts +0 -5
  122. package/packages/workspace-executor/src/modules.ts +7 -6
  123. package/packages/workspace-executor/src/sandbox-tools.ts +3 -9
  124. package/packages/workspace-executor/src/shell-platform.ts +14 -3
  125. package/packages/workspace-executor/src/tools.ts +0 -3
  126. package/plugins/ragents.ask/ask-payload.ts +9 -0
  127. package/plugins/ragents.ask/server/ask-service.ts +53 -13
  128. package/plugins/ragents.ask/web/AskActionView.tsx +3 -2
  129. package/plugins/ragents.browser/executor/index.ts +5 -0
  130. package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/module.ts +25 -31
  131. package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/playwright.ts +9 -23
  132. package/plugins/ragents.browser/provision.ts +1 -1
  133. package/plugins/ragents.browser/server/browser.ts +3 -3
  134. package/plugins/ragents.browser/server/contract.ts +2 -2
  135. package/plugins/ragents.browser/server/index.ts +1 -1
  136. package/plugins/ragents.lsp-fsharp/executor.ts +80 -0
  137. package/plugins/ragents.lsp-fsharp/provision.ts +1 -1
  138. package/plugins/ragents.lsp-fsharp/server/index.ts +2 -2
  139. package/plugins/ragents.lsp-roslyn/executor.ts +64 -0
  140. package/plugins/ragents.lsp-roslyn/provision.ts +1 -1
  141. package/plugins/ragents.lsp-roslyn/server/index.ts +28 -5
  142. package/plugins/ragents.lsp-roslyn/server/solution-on-start.ts +18 -6
  143. package/plugins/ragents.lsp-typescript/executor.ts +50 -0
  144. package/plugins/ragents.lsp-typescript/server/index.ts +2 -2
  145. package/plugins/ragents.model-relay/server/config.ts +1 -1
  146. package/plugins/ragents.model-relay/server/relay.ts +4 -2
  147. package/plugins/ragents.profile-distribution/server/archive.ts +2 -1
  148. package/plugins/ragents.workspace/client/workspace-client.ts +111 -28
  149. package/plugins/ragents.workspace/contract.ts +33 -2
  150. package/plugins/ragents.workspace/server/binding.ts +10 -4
  151. package/plugins/ragents.workspace/server/clients.ts +56 -9
  152. package/plugins/ragents.workspace/server/config.ts +9 -0
  153. package/plugins/ragents.workspace/server/index.ts +13 -4
  154. package/plugins/ragents.workspace/server/runtime.ts +10 -0
  155. package/plugins/ragents.workspace/server/shell-platform.ts +6 -5
  156. package/plugins/ragents.workspace/web/WorkspaceBinding.tsx +1 -1
  157. package/ragents.config.core.ts +4 -1
  158. package/ragents.config.showcase.ts +4 -1
  159. package/scripts/agent/journal.ts +3 -1
  160. package/scripts/package/build-package.ts +3 -1
  161. package/scripts/provision/run-provision.ts +2 -2
  162. package/scripts/workspace-client/run-workspace-client.ts +7 -4
  163. package/apps/web/dist/help/screenshots/release-checklist-vscode.png +0 -0
  164. package/apps/web/dist/help/screenshots/release-checklist-web.png +0 -0
  165. package/bundles/ragents.ask/web/chunks/chunk-HGFDGTNC.js +0 -2
  166. package/bundles/ragents.ask/web/chunks/chunk-HGFDGTNC.js.map +0 -7
  167. package/bundles/ragents.browser/server/chunks/chunk-2GKLLCBE.js.map +0 -7
  168. package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +0 -60
  169. package/packages/workspace-executor/src/language-server/adapters/index.ts +0 -7
  170. package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +0 -56
  171. package/packages/workspace-executor/src/language-server/adapters/typescript.ts +0 -74
  172. package/packages/workspace-executor/src/language-server/solution.ts +0 -13
  173. /package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/contract.ts +0 -0
  174. /package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/pages.ts +0 -0
@@ -520,6 +520,15 @@ export interface ModelCost extends ModelCostRates {
520
520
  /** Request-wide pricing tiers. The highest matching input threshold applies to the full request. */
521
521
  tiers?: ModelCostTier[];
522
522
  }
523
+ /** When an agent compacts its context on a model and how much it keeps, in absolute tokens of that model. */
524
+ export interface ModelCompaction {
525
+ /** Context tokens above which the agent compacts. */
526
+ threshold: number;
527
+ /** Tokens of the newest context the compaction keeps verbatim. */
528
+ keepRecentTokens: number;
529
+ /** Output budget of the summary. */
530
+ summaryTokens: number;
531
+ }
523
532
  export interface Model<TApi extends Api> {
524
533
  id: string;
525
534
  name: string;
@@ -536,6 +545,8 @@ export interface Model<TApi extends Api> {
536
545
  cost: ModelCost;
537
546
  contextWindow: number;
538
547
  maxTokens: number;
548
+ /** The model's own compaction values; without them the agent applies the catalog standard of its context window. */
549
+ compaction?: ModelCompaction;
539
550
  headers?: Record<string, string>;
540
551
  /** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */
541
552
  compat?: TApi extends "openai-completions" ? OpenAICompletionsCompat : TApi extends "openai-responses" | "openai-codex-responses" ? OpenAIResponsesCompat : TApi extends "anthropic-messages" ? AnthropicMessagesCompat : never;
@@ -561,6 +561,16 @@ export interface ModelCost extends ModelCostRates {
561
561
  tiers?: ModelCostTier[];
562
562
  }
563
563
 
564
+ /** When an agent compacts its context on a model and how much it keeps, in absolute tokens of that model. */
565
+ export interface ModelCompaction {
566
+ /** Context tokens above which the agent compacts. */
567
+ threshold: number;
568
+ /** Tokens of the newest context the compaction keeps verbatim. */
569
+ keepRecentTokens: number;
570
+ /** Output budget of the summary. */
571
+ summaryTokens: number;
572
+ }
573
+
564
574
  // Model interface for the unified model system
565
575
  export interface Model<TApi extends Api> {
566
576
  id: string;
@@ -578,6 +588,8 @@ export interface Model<TApi extends Api> {
578
588
  cost: ModelCost;
579
589
  contextWindow: number;
580
590
  maxTokens: number;
591
+ /** The model's own compaction values; without them the agent applies the catalog standard of its context window. */
592
+ compaction?: ModelCompaction;
581
593
  headers?: Record<string, string>;
582
594
  /** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */
583
595
  compat?: TApi extends "openai-completions"
@@ -358,6 +358,9 @@ export const assertEventSemantics = (
358
358
  for (const artifactId of event.payload.artifactIds)
359
359
  assertArtifactRead(state, event.actorId, artifactId);
360
360
 
361
+ if (event.payload.origin === "human" && existingActorOf(state, event.actorId).kind !== "human")
362
+ throw new Error(`Input ${event.payload.inputId} of human origin was not enqueued by a human actor.`);
363
+
361
364
  if (event.payload.subscriptionId) {
362
365
  const subscription = state.subscriptions.get(event.payload.subscriptionId);
363
366
 
@@ -431,9 +431,13 @@ const payloadOf = (type: EventType, value: unknown, path: string) => {
431
431
  "artifactIds",
432
432
  "sourceEventIds",
433
433
  "subscriptionId",
434
- ], ["presentation"]);
434
+ ], ["presentation", "origin"]);
435
435
  if (payload.presentation !== undefined && payload.presentation !== "background")
436
436
  fail(`${path}.presentation`, "must be background");
437
+ if (payload.origin !== undefined && payload.origin !== "human")
438
+ fail(`${path}.origin`, "must be human");
439
+ if (subscriptionInput && Object.hasOwn(payload, "origin"))
440
+ fail(`${path}.origin`, "is not supported for a subscription input");
437
441
  stringOf(payload.inputId, `${path}.inputId`);
438
442
  actorIdOf(payload.actorId, `${path}.actorId`);
439
443
  stringArrayOf(payload.artifactIds, `${path}.artifactIds`);
@@ -545,7 +549,7 @@ const payloadOf = (type: EventType, value: unknown, path: string) => {
545
549
  "model",
546
550
  "readFiles",
547
551
  "modifiedFiles",
548
- ]);
552
+ ], ["threshold"]);
549
553
  stringOf(payload.turnId, `${path}.turnId`);
550
554
  nonEmptyStringOf(payload.summary, `${path}.summary`);
551
555
  nonEmptyStringOf(payload.firstKeptEventId, `${path}.firstKeptEventId`);
@@ -554,6 +558,15 @@ const payloadOf = (type: EventType, value: unknown, path: string) => {
554
558
  nonEmptyStringOf(payload.model, `${path}.model`);
555
559
  stringArrayOf(payload.readFiles, `${path}.readFiles`);
556
560
  stringArrayOf(payload.modifiedFiles, `${path}.modifiedFiles`);
561
+
562
+ if (Object.hasOwn(payload, "threshold")) {
563
+ const threshold = exactObject(payload.threshold, `${path}.threshold`, ["tokens", "source"]);
564
+ if (!Number.isSafeInteger(threshold.tokens))
565
+ fail(`${path}.threshold.tokens`, "must be a safe integer");
566
+ if (threshold.source !== "model" && threshold.source !== "catalog")
567
+ fail(`${path}.threshold.source`, "must be model or catalog");
568
+ }
569
+
557
570
  return;
558
571
  }
559
572
 
@@ -93,6 +93,7 @@ export type EventPayloads = {
93
93
  actorId: ActorId;
94
94
  artifactIds: string[];
95
95
  presentation?: "background";
96
+ origin?: "human";
96
97
  } & (
97
98
  | { subscriptionId: null; sourceEventIds: string[]; content: string }
98
99
  | { subscriptionId: SubscriptionId; sourceEventIds: [string]; content?: never }
@@ -151,6 +152,8 @@ export type EventPayloads = {
151
152
  model: string;
152
153
  readFiles: string[];
153
154
  modifiedFiles: string[];
155
+ /** The compaction threshold of the model and whether it is the model's own or the catalog standard; absent before file format 9. */
156
+ threshold?: { tokens: number; source: "model" | "catalog" };
154
157
  };
155
158
  "model.output.completed": {
156
159
  turnId: TurnId;
@@ -89,6 +89,8 @@ export type ActorInput = {
89
89
  actorId: ActorId;
90
90
  content: string;
91
91
  presentation?: "background";
92
+ /** Set only on a person's own message, because system inputs are enqueued under the owner too. */
93
+ origin?: "human";
92
94
  artifactIds: readonly ArtifactId[];
93
95
  enqueuedBy: ActorId;
94
96
  enqueuedAt: string;
@@ -222,6 +222,7 @@ export function applyEvent(
222
222
  actorId: event.payload.actorId,
223
223
  content: event.payload.subscriptionId === null ? event.payload.content : JSON.stringify(source),
224
224
  ...(event.payload.presentation ? { presentation: event.payload.presentation } : {}),
225
+ ...(event.payload.origin ? { origin: event.payload.origin } : {}),
225
226
  artifactIds: event.payload.artifactIds,
226
227
  enqueuedBy: event.actorId,
227
228
  enqueuedAt: event.occurredAt,
@@ -36,7 +36,7 @@ export type AgentRuntimeManagerOptions = {
36
36
  context: AgentRuntimeContext,
37
37
  signal: AbortSignal,
38
38
  ) => readonly AgentHook[] | Promise<readonly AgentHook[]>;
39
- /** Compaction, retry and provider request limits; unset values keep the defaults. */
39
+ /** Retry and provider request limits; unset values keep the defaults. Compaction comes from the model. */
40
40
  settings?: AgentSettingsInput;
41
41
  turnAbortTimeoutMs?: number;
42
42
  onDiagnostic?: (diagnostic: AgentRuntimeDiagnostic) => void;
@@ -10,6 +10,7 @@ import {
10
10
  type AgentTool,
11
11
  calculateContextTokens,
12
12
  compact,
13
+ compactionOf,
13
14
  contextMessages,
14
15
  convertToLlm,
15
16
  EMPTY_RESPONSE_NUDGE,
@@ -626,12 +627,11 @@ export class AgentTurn {
626
627
  return true;
627
628
  }
628
629
 
629
- /** After a context overflow compacts and retries once; over the threshold compacts without a retry. */
630
+ /** After a context overflow compacts and retries once; over the threshold of the turn's model compacts without a retry. */
630
631
  async #checkCompaction(assistant: AssistantMessage | undefined, skipAborted = true): Promise<boolean> {
631
- const settings = this.#options.settings.compaction;
632
632
  const { model } = this.#options;
633
633
 
634
- if (!assistant || !settings.enabled || (skipAborted && assistant.stopReason === "aborted"))
634
+ if (!assistant || (skipAborted && assistant.stopReason === "aborted"))
635
635
  return false;
636
636
 
637
637
  const contextWindow = model.contextWindow ?? 0;
@@ -672,12 +672,13 @@ export class AgentTurn {
672
672
  contextTokens = estimate.tokens;
673
673
  }
674
674
 
675
- return shouldCompact(contextTokens, contextWindow, settings) ? this.#compact(false) : false;
675
+ return shouldCompact(contextTokens, compactionOf(model).values) ? this.#compact(false) : false;
676
676
  }
677
677
 
678
678
  async #compact(retry: boolean): Promise<boolean> {
679
- const { model, modelRuntime, thinkingLevel, settings } = this.#options;
680
- const preparation = prepareCompaction([...this.#request.modelContext().entries], settings.compaction);
679
+ const { model, modelRuntime, thinkingLevel } = this.#options;
680
+ const compaction = compactionOf(model);
681
+ const preparation = prepareCompaction([...this.#request.modelContext().entries], compaction.values);
681
682
 
682
683
  if (!preparation)
683
684
  return false;
@@ -715,6 +716,7 @@ export class AgentTurn {
715
716
  model: model.id,
716
717
  readFiles: result.readFiles,
717
718
  modifiedFiles: result.modifiedFiles,
719
+ threshold: { tokens: compaction.values.threshold, source: compaction.source },
718
720
  },
719
721
  });
720
722
  const messages = this.#request.modelContext().messages;
@@ -17,8 +17,8 @@ export type EnqueueInput = {
17
17
  artifactIds?: readonly string[];
18
18
  presentation?: "background";
19
19
  } & (
20
- | { content: string; sourceEventIds?: readonly string[]; subscriptionId?: null }
21
- | { content?: never; sourceEventIds: readonly [string]; subscriptionId: string }
20
+ | { content: string; sourceEventIds?: readonly string[]; subscriptionId?: null; origin?: "human" }
21
+ | { content?: never; sourceEventIds: readonly [string]; subscriptionId: string; origin?: never }
22
22
  );
23
23
 
24
24
  export const enqueueInput =
@@ -43,6 +43,9 @@ export const enqueueInput =
43
43
  assertCapability(caller, "actor.input", { kind: "run" });
44
44
  }
45
45
 
46
+ if (input.origin === "human" && caller.kind !== "human")
47
+ throw new DomainError("input-origin-invalid", "Only a human can enqueue an input of human origin.", 403);
48
+
46
49
  const artifactIds = [...new Set(input.artifactIds ?? [])];
47
50
 
48
51
  for (const artifactId of artifactIds)
@@ -55,6 +58,7 @@ export const enqueueInput =
55
58
  actorId: target.id,
56
59
  artifactIds,
57
60
  ...(input.presentation !== undefined ? { presentation: input.presentation } : {}),
61
+ ...(input.origin !== undefined ? { origin: input.origin } : {}),
58
62
  };
59
63
 
60
64
  if (subscriptionId && firstSourceEventId === undefined)
@@ -8,9 +8,10 @@ import { isRunId } from "../domain/portable-id.ts";
8
8
  import { syncDirectory } from "./durable-fs.ts";
9
9
  import type { CommandRecord, JournalCommand } from "./journal.ts";
10
10
 
11
- /** Rises whenever an older stand would reject newly written lines; the journal reads only this version. */
12
- export const journalStorageVersion = 7;
13
- const readableJournalStorageVersions: readonly unknown[] = [journalStorageVersion];
11
+ /** Rises whenever an older stand would reject newly written lines; formats 7 and 8 stay readable because they only lack the input origin and the compaction threshold. */
12
+ export const journalStorageVersion = 9;
13
+ const oldestReadableJournalStorageVersion = 7;
14
+ const readableJournalStorageVersions: readonly unknown[] = [oldestReadableJournalStorageVersion, 8, journalStorageVersion];
14
15
  export const journalPayloadThresholdBytes = 4096;
15
16
 
16
17
  type PayloadReference = { sha256: string; bytes: number };
@@ -162,7 +163,7 @@ export const parseJournalRecord = (line: string, runDirectory: string, location:
162
163
 
163
164
  if (!readableJournalStorageVersions.includes(record.formatVersion))
164
165
  throw new Error(`${location} has unsupported journal format version ${String(record.formatVersion)}.${
165
- typeof record.formatVersion === "number" && record.formatVersion < journalStorageVersion
166
+ typeof record.formatVersion === "number" && record.formatVersion < oldestReadableJournalStorageVersion
166
167
  ? " Journals before format 7 hold no model context and are not migrated."
167
168
  : ""}`);
168
169
 
@@ -15,7 +15,6 @@
15
15
  "dependencies": {
16
16
  "@ragents/agent": "workspace:*",
17
17
  "typescript": "^5.9.0",
18
- "typescript-language-server": "6.0.0",
19
18
  "vscode-jsonrpc": "^9.0.2"
20
19
  },
21
20
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { getShellConfig } from "@ragents/agent";
4
4
 
@@ -9,6 +9,12 @@ export interface BashLaunch {
9
9
  readonly env: NodeJS.ProcessEnv;
10
10
  }
11
11
 
12
+ /** Was der Executor dem Werkzeug bash mitgibt: die Bash selbst und rg; ohne Angabe gilt jeweils das des Systems. */
13
+ export interface BashPrograms {
14
+ readonly bash: string | undefined;
15
+ readonly rg: string | undefined;
16
+ }
17
+
12
18
  /** Unter Windows sucht RAgents keine Bash; der Executor nennt die mitgebrachte. */
13
19
  const missingWindowsBash = (): Error => new Error(
14
20
  "Unter Windows arbeitet das Werkzeug bash nur mit der Bash, die RAgents mitbringt, und für diesen Executor ist keine genannt. "
@@ -16,17 +22,51 @@ const missingWindowsBash = (): Error => new Error(
16
22
  + "und reicht sie an Arbeitsplatz und lokalen Host weiter; ein Host oder Arbeitsplatz ohne Erweiterung braucht RAGENTS_BASH mit dem Pfad dieser bash.exe.",
17
23
  );
18
24
 
19
- /** Die Windows-Umgebung der Bash: ihr usr/bin vorn im PATH, damit find.exe und sort.exe aus System32 nicht gewinnen; MSYSTEM gehört einer Git-Bash-Anmeldung. */
20
- const windowsBashEnvironment = (env: NodeJS.ProcessEnv, bin: string): NodeJS.ProcessEnv => {
21
- const pathNames = Object.keys(env).filter((name) => name.toUpperCase() === "PATH");
22
- const current = pathNames.map((name) => env[name]).find((value) => value !== undefined && value !== "");
23
- const kept = Object.entries(env).filter(([name]) => !pathNames.includes(name) && name !== "MSYSTEM");
24
- return { ...Object.fromEntries(kept), PATH: current ? `${bin}${path.win32.delimiter}${current}` : bin };
25
+ const missingRipgrep = (rg: string): Error =>
26
+ new Error(`Das rg dieses Executors fehlt: ${rg}. Es gehört zur Erweiterung (dist/rg/<plattform>) oder steht in RAGENTS_RG.`);
27
+
28
+ const pathModule = (platform: NodeJS.Platform): path.PlatformPath => platform === "win32" ? path.win32 : path.posix;
29
+
30
+ /** Die Namen der PATH-Variablen; unter Windows zählt jede Schreibweise (`Path`). */
31
+ const pathNames = (env: NodeJS.ProcessEnv, platform: NodeJS.Platform): readonly string[] =>
32
+ platform === "win32" ? Object.keys(env).filter((name) => name.toUpperCase() === "PATH") : ["PATH"];
33
+
34
+ const currentPath = (env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string | undefined =>
35
+ pathNames(env, platform).map((name) => env[name]).find((value) => value !== undefined && value !== "");
36
+
37
+ /** Die Ordner vorn im PATH, als eine Variable PATH; MSYSTEM gehört unter Windows einer Git-Bash-Anmeldung und fällt weg. */
38
+ const withPathFront = (env: NodeJS.ProcessEnv, directories: readonly string[], platform: NodeJS.Platform): NodeJS.ProcessEnv => {
39
+ if (directories.length === 0) return env;
40
+ const names = pathNames(env, platform);
41
+ const current = currentPath(env, platform);
42
+ const kept = Object.entries(env).filter(([name]) => !names.includes(name) && (platform !== "win32" || name !== "MSYSTEM"));
43
+ const delimiter = pathModule(platform).delimiter;
44
+ return { ...Object.fromEntries(kept), PATH: [...directories, ...(current ? [current] : [])].join(delimiter) };
45
+ };
46
+
47
+ const isExecutableFile = (file: string, platform: NodeJS.Platform): boolean => {
48
+ try {
49
+ if (platform !== "win32") accessSync(file, constants.X_OK);
50
+ return statSync(file).isFile();
51
+ } catch {
52
+ return false;
53
+ }
54
+ };
55
+
56
+ /** Ob die Bash dieses Executors rg findet: das genannte, sonst eines im PATH; ein genanntes, das fehlt, ist ein Fehler. */
57
+ export const ripgrepAvailable = (rg: string | undefined, env: NodeJS.ProcessEnv, platform: NodeJS.Platform = process.platform): boolean => {
58
+ if (rg !== undefined) {
59
+ if (!existsSync(rg)) throw missingRipgrep(rg);
60
+ return true;
61
+ }
62
+ const file = platform === "win32" ? "rg.exe" : "rg";
63
+ return (currentPath(env, platform) ?? "").split(pathModule(platform).delimiter).filter(Boolean)
64
+ .some((directory) => isExecutableFile(path.join(directory, file), platform));
25
65
  };
26
66
 
27
- /** Wie ein Befehl in der Bash dieses Executors startet; die Plattform ist für Prüfungen wählbar. */
67
+ /** Wie ein Befehl in der Bash dieses Executors startet: das Verzeichnis von rg vorn im PATH, unter Windows dahinter das usr/bin der Bash, damit find.exe und sort.exe aus System32 nicht gewinnen. */
28
68
  export const bashLaunch = (
29
- bash: string | undefined,
69
+ { bash, rg }: BashPrograms,
30
70
  command: string,
31
71
  env: NodeJS.ProcessEnv,
32
72
  platform: NodeJS.Platform = process.platform,
@@ -35,11 +75,17 @@ export const bashLaunch = (
35
75
  if (bash !== undefined && !existsSync(bash)) {
36
76
  throw new Error(`Die Bash dieses Executors fehlt: ${bash}. Unter Windows gehört sie zur Erweiterung (dist/bash/<plattform>) oder steht in RAGENTS_BASH.`);
37
77
  }
78
+ if (rg !== undefined && !existsSync(rg)) throw missingRipgrep(rg);
38
79
  const shell = getShellConfig(bash, platform);
39
80
  if (!/bash(\.exe)?$/i.test(shell.shell)) throw new Error(`${shell.shell} ist keine Bash; das Werkzeug heißt bash und der Prompt beschreibt bash`);
81
+ const paths = pathModule(platform);
82
+ const front = [
83
+ ...(rg === undefined ? [] : [paths.dirname(rg)]),
84
+ ...(platform === "win32" ? [paths.dirname(shell.shell)] : []),
85
+ ];
40
86
  return {
41
87
  command: shell.shell,
42
88
  args: [...shell.args, command],
43
- env: platform === "win32" ? windowsBashEnvironment(env, path.win32.dirname(shell.shell)) : env,
89
+ env: withPathFront(env, front, platform),
44
90
  };
45
91
  };
@@ -27,6 +27,8 @@ export interface WorkspaceProcessContext {
27
27
  sandbox?: ProcessSandbox;
28
28
  /** Die Bash, mit der das Werkzeug bash startet; unter Windows Pflicht, sonst gilt ohne Angabe die des Systems. */
29
29
  bash?: string;
30
+ /** Das rg, dessen Ordner das Werkzeug bash vorn im PATH hat; ohne Angabe gilt eines im PATH, falls es eines gibt. */
31
+ rg?: string;
30
32
  }
31
33
 
32
34
  export interface SandboxHomeEnvironment {
@@ -103,6 +105,7 @@ export interface WorkspaceContextOptions {
103
105
  source?: NodeJS.ProcessEnv;
104
106
  baseEnvironment?: BaseEnvironment;
105
107
  bash?: string;
108
+ rg?: string;
106
109
  }
107
110
 
108
111
  const namedEntries = (
@@ -151,5 +154,6 @@ export const workspaceProcessContext = (options: WorkspaceContextOptions): Works
151
154
  ...(options.runOperation === undefined ? {} : { runOperation: options.runOperation }),
152
155
  ...(options.sandbox === undefined ? {} : { sandbox: options.sandbox }),
153
156
  ...(options.bash === undefined ? {} : { bash: options.bash }),
157
+ ...(options.rg === undefined ? {} : { rg: options.rg }),
154
158
  };
155
159
  };
@@ -0,0 +1,150 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { WorkspaceOperationError } from "./errors.js";
8
+ import type { LanguageServerAdapter } from "./language-server/host.js";
9
+ import { resolveRootDirectory, resolveRootFile } from "./language-server/roots.js";
10
+ import type { WorkspaceModuleFactory } from "./module.js";
11
+ import { RUN_MARKER_ENV } from "./run-marker.js";
12
+ import { safeProcessEnvironment } from "./safe-environment.js";
13
+
14
+ /** Wo ein Bundle seinen Beitrag zum Executor trägt: eine in sich geschlossene Datei, die jeder Node-Prozess ohne Host-Auflösung lädt. */
15
+ export const EXECUTOR_CONTRIBUTION_FILE = "executor/index.mjs";
16
+
17
+ /** Was der Executor einer Maschine einem Beitrag über sie sagt; ein Beitrag importiert vom Host nur Typen, alles andere kommt hierüber. */
18
+ export interface WorkspaceExecutorMachine {
19
+ /** Der Werkzeugordner des Plugins auf dieser Maschine, wohin seine Provisionierung lädt. */
20
+ readonly toolsDirectory: string;
21
+ /** Eine Datei aus einem Paket in den node_modules des Hosts dieser Maschine; ohne Host oder Paket scheitert der Aufruf mit Ursache. */
22
+ readonly hostPackageFile: (hostRoot: string | undefined, specifier: string) => string;
23
+ /** Eine Datei mit einer der Endungen im Arbeitsbereich, aufgelöst und geprüft. */
24
+ readonly resolveRootFile: (workspaceRoot: string, requested: string, extensions: readonly string[]) => Promise<string>;
25
+ /** Ein Ordner im Arbeitsbereich, aufgelöst und geprüft. */
26
+ readonly resolveRootDirectory: (workspaceRoot: string, requested: string) => Promise<string>;
27
+ /** Ein fachlicher Fehler einer Operation mit Kennung und Status; beim Aufrufer kommt er als derselbe Fehler an wie einer des Executors. */
28
+ readonly operationError: (code: string, message: string, status: number) => Error;
29
+ /** Die Umgebung eines Prozesses, den ein Beitrag selbst startet: die sichere Auswahl dieser Maschine, ihr HOME und der Marker des Runs für die Prozessanzeige. */
30
+ readonly processEnvironment: (runId: string) => NodeJS.ProcessEnv;
31
+ }
32
+
33
+ /** Was ein Beitrag dem Executor einer Maschine hinzufügt. */
34
+ export interface WorkspaceExecutorParts {
35
+ readonly languageServers?: readonly LanguageServerAdapter[];
36
+ /** Module mit eigenen Operationen; jede Operation gehört genau einem Modul. */
37
+ readonly modules?: readonly WorkspaceModuleFactory[];
38
+ }
39
+
40
+ /** Was ein Plugin-Bundle als `executor` exportiert; es läuft in jedem Executor, im Server wie auf jedem Arbeitsplatz, und berührt beim Bauen weder Platte noch Netz. */
41
+ export type WorkspaceExecutorContribution = (machine: WorkspaceExecutorMachine) => WorkspaceExecutorParts;
42
+
43
+ /** Wessen Beitrag in welchem Stand; Server und Arbeitsplatz verlangen dieselben. */
44
+ export interface ExecutorContributionStand {
45
+ readonly plugin: string;
46
+ /** SHA-256 der Datei des Beitrags. */
47
+ readonly stand: string;
48
+ }
49
+
50
+ export interface LoadedExecutorContribution extends ExecutorContributionStand {
51
+ readonly contribution: WorkspaceExecutorContribution;
52
+ }
53
+
54
+ /** Ein Beitrag, gebaut für diese Maschine. */
55
+ export interface PreparedExecutorContribution extends ExecutorContributionStand {
56
+ readonly parts: WorkspaceExecutorParts;
57
+ }
58
+
59
+ const LANGUAGE_SERVER_ID = /^[a-z][a-z0-9]{0,39}$/;
60
+ const PART_KEYS = new Set(["languageServers", "modules"]);
61
+
62
+ const messageOf = (cause: unknown): string => cause instanceof Error ? cause.message : String(cause);
63
+
64
+ /** Der Paketname eines Bezeichners wie acme-server/lib/cli.mjs oder @scope/name/datei. */
65
+ const packageOf = (specifier: string): string =>
66
+ specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
67
+
68
+ const hostPackageFile = (hostRoot: string | undefined, specifier: string): string => {
69
+ if (!hostRoot) {
70
+ throw new Error(`Auf diesem Rechner ist kein Host bekannt, aus dem sich ${packageOf(specifier)} auflösen ließe. Ein Arbeitsplatz `
71
+ + "bekommt ihn mit der ersten Verbindung zu einem verteilenden Server oder über die Einstellung ragents.hostPath");
72
+ }
73
+ try {
74
+ return createRequire(path.join(hostRoot, "package.json")).resolve(specifier);
75
+ } catch (cause) {
76
+ throw new Error(`${packageOf(specifier)} liegt nicht im Host ${hostRoot}: ${messageOf(cause)}`);
77
+ }
78
+ };
79
+
80
+ export const executorMachine = (toolsDirectory: string): WorkspaceExecutorMachine => ({
81
+ toolsDirectory,
82
+ hostPackageFile,
83
+ resolveRootFile,
84
+ resolveRootDirectory,
85
+ operationError: (code, message, status) => new WorkspaceOperationError(code, message, status),
86
+ processEnvironment: (runId) => ({ ...safeProcessEnvironment(process.env), HOME: homedir(), [RUN_MARKER_ENV]: runId }),
87
+ });
88
+
89
+ /** Lädt die Datei eines Beitrags, mit `expected` nur in diesem Stand; der Stand steht in ihrer Adresse, damit ein neu gebauter Beitrag nie aus dem Modulcache kommt. */
90
+ export const loadExecutorContribution = async (plugin: string, file: string, expected?: string): Promise<LoadedExecutorContribution> => {
91
+ const content = await readFile(file).catch((cause: unknown) => {
92
+ throw new Error(`Der Executor-Beitrag von ${plugin} fehlt unter ${file}: ${messageOf(cause)}`);
93
+ });
94
+ const stand = createHash("sha256").update(content).digest("hex");
95
+ if (expected !== undefined && stand !== expected) {
96
+ throw new Error(`Der Executor-Beitrag von ${plugin} unter ${file} hat den Stand ${stand.slice(0, 12)}, verlangt ist ${expected.slice(0, 12)}`);
97
+ }
98
+ const exported = await import(`${pathToFileURL(file).href}?stand=${stand}`).catch((cause: unknown) => {
99
+ throw new Error(`Der Executor-Beitrag von ${plugin} (${file}) lädt nicht: ${messageOf(cause)}`, { cause });
100
+ }) as Readonly<Record<string, unknown>>;
101
+ const contribution = exported.executor;
102
+ if (typeof contribution !== "function") {
103
+ throw new Error(`Das Bundle ${plugin} exportiert in ${file} keinen gültigen Executor-Beitrag; erwartet wird `
104
+ + "\"export const executor: WorkspaceExecutorContribution\" als Funktion der Maschine");
105
+ }
106
+ return { plugin, stand, contribution: contribution as WorkspaceExecutorContribution };
107
+ };
108
+
109
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
110
+ typeof value === "object" && value !== null && !Array.isArray(value);
111
+
112
+ const isStringList = (value: unknown): value is readonly string[] =>
113
+ Array.isArray(value) && value.every((entry) => typeof entry === "string");
114
+
115
+ /** Was an einem Sprachserver fehlt oder die falsche Form hat; leer, wenn er taugt. */
116
+ const adapterProblems = (value: Readonly<Record<string, unknown>>): readonly string[] => [
117
+ ...typeof value.id === "string" && LANGUAGE_SERVER_ID.test(value.id) ? [] : ["id aus Kleinbuchstaben und Ziffern"],
118
+ ...typeof value.label === "string" ? [] : ["label"],
119
+ ...isRecord(value.languages) && Object.values(value.languages).every((entry) => typeof entry === "string") ? [] : ["languages"],
120
+ ...typeof value.rootDescription === "string" ? [] : ["rootDescription"],
121
+ ...value.solutionExtensions === undefined || isStringList(value.solutionExtensions) ? [] : ["solutionExtensions"],
122
+ ...(["resolveRoot", "rootDirectory", "launch", "open"] as const).filter((name) => typeof value[name] !== "function"),
123
+ ];
124
+
125
+ /** Baut einen geladenen Beitrag für diese Maschine und prüft, was er liefert; eine falsche Form ist ein Fehler mit Ursache. */
126
+ export const prepareExecutorContribution = (loaded: LoadedExecutorContribution, toolsDirectory: string): PreparedExecutorContribution => {
127
+ const invalid = (detail: string): Error => new Error(`Der Executor-Beitrag von ${loaded.plugin} ist ungültig: ${detail}`);
128
+ const parts = ((): unknown => {
129
+ try {
130
+ return loaded.contribution(executorMachine(toolsDirectory));
131
+ } catch (cause) {
132
+ throw invalid(`die Funktion wirft: ${messageOf(cause)}`);
133
+ }
134
+ })();
135
+ if (!isRecord(parts)) throw invalid("die Funktion liefert kein Objekt");
136
+ const unknown = Object.keys(parts).filter((key) => !PART_KEYS.has(key));
137
+ if (unknown.length > 0) throw invalid(`unbekannte Teile ${unknown.join(", ")}; erlaubt sind ${[...PART_KEYS].join(", ")}`);
138
+ const modules = parts.modules;
139
+ if (modules !== undefined && (!Array.isArray(modules) || !modules.every((module) => typeof module === "function"))) {
140
+ throw invalid("modules ist keine Liste von Modulfabriken");
141
+ }
142
+ const servers = parts.languageServers;
143
+ if (servers !== undefined && !Array.isArray(servers)) throw invalid("languageServers ist keine Liste");
144
+ for (const [index, server] of (servers ?? []).entries()) {
145
+ if (!isRecord(server)) throw invalid(`der Sprachserver ${index + 1} ist kein Objekt`);
146
+ const problems = adapterProblems(server);
147
+ if (problems.length > 0) throw invalid(`der Sprachserver ${index + 1} braucht ${problems.join(", ")}`);
148
+ }
149
+ return { plugin: loaded.plugin, stand: loaded.stand, parts: parts as WorkspaceExecutorParts };
150
+ };
@@ -4,7 +4,7 @@ import type { WorkspaceExecutorModule, WorkspaceModuleFactory, WorkspaceOperatio
4
4
  import { NO_ROOTS, type OperationFootprint } from "./paths.js";
5
5
 
6
6
  /** Der Stand des Executors; Server und Arbeitsplatz müssen denselben tragen. */
7
- export const WORKSPACE_EXECUTOR_VERSION = "5";
7
+ export const WORKSPACE_EXECUTOR_VERSION = "7";
8
8
 
9
9
  export interface WorkspaceExecuteOptions {
10
10
  toolCallId?: string;
@@ -18,6 +18,18 @@ export {
18
18
  type WorkspaceModuleFactory,
19
19
  } from "./module.js";
20
20
  export { workspaceExecutorModules, type WorkspaceExecutorModuleOptions } from "./modules.js";
21
+ export {
22
+ EXECUTOR_CONTRIBUTION_FILE,
23
+ executorMachine,
24
+ loadExecutorContribution,
25
+ prepareExecutorContribution,
26
+ type ExecutorContributionStand,
27
+ type LoadedExecutorContribution,
28
+ type PreparedExecutorContribution,
29
+ type WorkspaceExecutorContribution,
30
+ type WorkspaceExecutorMachine,
31
+ type WorkspaceExecutorParts,
32
+ } from "./contributions.js";
21
33
  export { RUN_FOLDER_OPERATIONS, runFolderModule, type RunFolderCreated } from "./run-folders.js";
22
34
  export {
23
35
  FILE_OPERATIONS,
@@ -37,18 +49,6 @@ export {
37
49
  type CommandResult,
38
50
  type CommandRunInput,
39
51
  } from "./commands.js";
40
- export {
41
- BROWSER_EXECUTABLE_VARIABLE,
42
- BROWSER_OPERATIONS,
43
- type BrowserCheck,
44
- type BrowserCheckResult,
45
- type BrowserPageState,
46
- type BrowserSnapshot,
47
- type BrowserStep,
48
- type BrowserTarget,
49
- type BrowserViewport,
50
- } from "./browser/contract.js";
51
- export { browserModule, type BrowserModuleOptions } from "./browser/module.js";
52
52
  export { processGroupExists, runManagedProcess, startManagedService, type ManagedService } from "./managed-process.js";
53
53
  export {
54
54
  containsWorkspacePath,
@@ -81,15 +81,16 @@ export { labelOf, runProcessesFrom } from "./processes/snapshot.js";
81
81
  export { RunProcessTerminator } from "./processes/terminator.js";
82
82
  export { RUN_MARKER_ENV } from "./run-marker.js";
83
83
  export { editorFreeEnvironment, inheritedProcessEnvironment, safeProcessEnvironment } from "./safe-environment.js";
84
- export { bashLaunch, type BashLaunch } from "./bash-launch.js";
84
+ export { bashLaunch, ripgrepAvailable, type BashLaunch, type BashPrograms } from "./bash-launch.js";
85
85
  export { createSandboxTools, sandboxToolsModule, withAnnotation, type SeenFile } from "./sandbox-tools.js";
86
86
  export { type SessionIdent } from "./session-ident.js";
87
- export { shellPlatformText } from "./shell-platform.js";
88
- export { ragentsDataRoot, workspaceDataDirectory } from "./tools.js";
87
+ export { shellPlatformText, type ShellTools } from "./shell-platform.js";
88
+ export { hostDataDirectory, pluginToolsDirectory, ragentsDataRoot, workspaceDataDirectory } from "./tools.js";
89
89
  export {
90
90
  LanguageServerHost,
91
91
  changedWorkspaceFiles,
92
92
  type LanguageServerAdapter,
93
+ type LanguageServerDescription,
93
94
  type LanguageServerHostOptions,
94
95
  } from "./language-server/host.js";
95
96
  export {
@@ -112,7 +113,3 @@ export {
112
113
  } from "./language-server/module.js";
113
114
  export { resolveRootDirectory } from "./language-server/roots.js";
114
115
  export { LanguageServerSession, withTimeout } from "./language-server/session.js";
115
- export { solutionProjects } from "./language-server/solution.js";
116
- export { fsharpAdapter, FSHARP_SERVER_FILE, FSHARP_SERVER_VARIABLE } from "./language-server/adapters/fsharp.js";
117
- export { roslynAdapter, ROSLYN_SERVER_FILE, ROSLYN_SERVER_VARIABLE } from "./language-server/adapters/roslyn.js";
118
- export { typescriptAdapter } from "./language-server/adapters/typescript.js";
@@ -27,6 +27,9 @@ export interface LanguageServerAdapter {
27
27
  open: (session: LanguageServerSession, root: string, timeoutMs: number) => Promise<string>;
28
28
  }
29
29
 
30
+ /** Was der Server über einen Sprachserver wissen muss, um Werkzeuge und Reiter zu beschreiben; gestartet wird er nur im Executor. */
31
+ export type LanguageServerDescription = Pick<LanguageServerAdapter, "id" | "label" | "languages" | "rootDescription" | "solutionExtensions">;
32
+
30
33
  export interface LanguageServerHostOptions {
31
34
  idleMs?: number;
32
35
  openTimeoutMs?: number;
@@ -1,5 +1,5 @@
1
1
  import { WorkspaceOperationError } from "../errors.js";
2
- import type { WorkspaceModuleFactory, WorkspaceOperation } from "../module.js";
2
+ import type { WorkspaceExecutorModule, WorkspaceModuleFactory, WorkspaceModuleHost, WorkspaceOperation } from "../module.js";
3
3
  import { rootsOfFields, type OperationFootprint } from "../paths.js";
4
4
  import { LanguageServerHost, type LanguageServerAdapter, type LanguageServerHostOptions } from "./host.js";
5
5
 
@@ -89,7 +89,17 @@ const footprintsOf = (server: LanguageServerHost): Array<readonly [string, (inpu
89
89
  export const languageServerModule = (
90
90
  adapters: readonly LanguageServerAdapter[],
91
91
  options: LanguageServerHostOptions = {},
92
- ): WorkspaceModuleFactory => (host) => {
92
+ ): WorkspaceModuleFactory => {
93
+ const duplicate = adapters.find((adapter, index) => adapters.findIndex((other) => other.id === adapter.id) !== index);
94
+ if (duplicate) throw new Error(`Der Sprachserver ${duplicate.id} kommt im Executor zweimal vor; seine Operationen gehören genau einem Beitrag`);
95
+ return (host) => languageServersOn(host, adapters, options);
96
+ };
97
+
98
+ const languageServersOn = (
99
+ host: WorkspaceModuleHost,
100
+ adapters: readonly LanguageServerAdapter[],
101
+ options: LanguageServerHostOptions,
102
+ ): WorkspaceExecutorModule => {
93
103
  const servers = adapters.map((adapter) => new LanguageServerHost(adapter, host.contextFor, options));
94
104
  return {
95
105
  operations: Object.fromEntries(servers.flatMap(operationsOf)),