@schlenkr/ragents 0.1.7 → 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 (187) 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/plugin-folder.ts +2 -1
  12. package/apps/server/src/plugin-support/product-relay.ts +10 -2
  13. package/apps/server/src/plugin-support/workspace-sandbox-host.ts +18 -9
  14. package/apps/server/src/profile/bundle-manifest.ts +7 -3
  15. package/apps/server/src/profile/compose.ts +6 -0
  16. package/apps/server/src/profile/plugin-discovery.ts +6 -1
  17. package/apps/server/src/profile/provisioning.ts +6 -5
  18. package/apps/server/src/ragents/engine.ts +18 -12
  19. package/apps/server/src/ragents/host-services.ts +4 -0
  20. package/apps/server/src/ragents/session.ts +1 -1
  21. package/apps/server/src/server.ts +1 -0
  22. package/apps/server/tsconfig.json +2 -0
  23. package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-CFhTJEmH.js → highlighted-body-KPVGNVTW-xXXhTDbh.js} +1 -1
  24. package/apps/web/dist/assets/{main-DlzW4t4c.js → main-C7JeZFjs.js} +1 -1
  25. package/apps/web/dist/assets/{mermaid-HWGCJPDP-kxkB991i.js → mermaid-HWGCJPDP-DfsU5ofC.js} +2 -2
  26. package/apps/web/dist/assets/{runPanel-BJEFQ__w.js → runPanel-ZT6TPDLM.js} +1 -1
  27. package/apps/web/dist/help/guide-clients.html +14 -6
  28. package/apps/web/dist/help/guide-clients.md +14 -6
  29. package/apps/web/dist/help/guide-distributed.html +12 -4
  30. package/apps/web/dist/help/guide-distributed.md +12 -4
  31. package/apps/web/dist/help/guide-getting-started.html +12 -8
  32. package/apps/web/dist/help/guide-getting-started.md +12 -8
  33. package/apps/web/dist/help/guide-plugins.html +6 -2
  34. package/apps/web/dist/help/guide-plugins.md +6 -2
  35. package/apps/web/dist/help/guide-runtime.html +2 -1
  36. package/apps/web/dist/help/guide-runtime.md +2 -1
  37. package/apps/web/dist/help/index.html +3328 -1135
  38. package/apps/web/dist/host-web.json +4 -4
  39. package/apps/web/dist/index.html +2 -2
  40. package/apps/web/dist/run-panel.html +2 -2
  41. package/apps/web/src/StartTiles.tsx +1 -1
  42. package/bundles/ragents.activity/ragents-bundle.json +3 -3
  43. package/bundles/ragents.actor-programs/ragents-bundle.json +3 -3
  44. package/bundles/ragents.ask/ragents-bundle.json +4 -4
  45. package/bundles/ragents.ask/server/index.js +42 -11
  46. package/bundles/ragents.ask/server/index.js.map +3 -3
  47. package/bundles/ragents.ask/web/chunks/chunk-WFN4374U.js +2 -0
  48. package/bundles/ragents.ask/web/chunks/chunk-WFN4374U.js.map +7 -0
  49. package/bundles/ragents.ask/web/classes.json +1 -1
  50. package/bundles/ragents.ask/web/exports/ask-payload.js +1 -1
  51. package/bundles/ragents.ask/web/index.js +1 -1
  52. package/bundles/ragents.ask/web/index.js.map +3 -3
  53. package/bundles/ragents.browser/executor/index.mjs +357 -0
  54. package/bundles/ragents.browser/executor/index.mjs.map +7 -0
  55. package/bundles/ragents.browser/ragents-bundle.json +5 -8
  56. package/bundles/ragents.browser/server/chunks/{chunk-2GKLLCBE.js → chunk-CWLX4VA5.js} +1 -1
  57. package/bundles/ragents.browser/server/chunks/chunk-CWLX4VA5.js.map +7 -0
  58. package/bundles/ragents.browser/server/exports/server/contract.js +1 -1
  59. package/bundles/ragents.browser/server/index.js +20 -9
  60. package/bundles/ragents.browser/server/index.js.map +4 -4
  61. package/bundles/ragents.documents/ragents-bundle.json +3 -3
  62. package/bundles/ragents.lsp-fsharp/executor/index.mjs +68 -0
  63. package/bundles/ragents.lsp-fsharp/executor/index.mjs.map +7 -0
  64. package/bundles/ragents.lsp-fsharp/ragents-bundle.json +5 -9
  65. package/bundles/ragents.lsp-fsharp/server/index.js +21 -6
  66. package/bundles/ragents.lsp-fsharp/server/index.js.map +4 -4
  67. package/bundles/ragents.lsp-roslyn/executor/index.mjs +68 -0
  68. package/bundles/ragents.lsp-roslyn/executor/index.mjs.map +7 -0
  69. package/bundles/ragents.lsp-roslyn/ragents-bundle.json +6 -8
  70. package/bundles/ragents.lsp-roslyn/server/index.js +54 -15
  71. package/bundles/ragents.lsp-roslyn/server/index.js.map +4 -4
  72. package/bundles/ragents.lsp-typescript/executor/index.mjs +49 -0
  73. package/bundles/ragents.lsp-typescript/executor/index.mjs.map +7 -0
  74. package/bundles/ragents.lsp-typescript/ragents-bundle.json +5 -7
  75. package/bundles/ragents.lsp-typescript/server/index.js +22 -2
  76. package/bundles/ragents.lsp-typescript/server/index.js.map +3 -3
  77. package/bundles/ragents.model-relay/ragents-bundle.json +4 -4
  78. package/bundles/ragents.model-relay/server/index.js +3 -2
  79. package/bundles/ragents.model-relay/server/index.js.map +2 -2
  80. package/bundles/ragents.orchestration/ragents-bundle.json +3 -3
  81. package/bundles/ragents.overseer/ragents-bundle.json +3 -3
  82. package/bundles/ragents.processes/ragents-bundle.json +3 -3
  83. package/bundles/ragents.product/ragents-bundle.json +3 -3
  84. package/bundles/ragents.profile-distribution/ragents-bundle.json +4 -4
  85. package/bundles/ragents.profile-distribution/server/index.js +1 -1
  86. package/bundles/ragents.profile-distribution/server/index.js.map +2 -2
  87. package/bundles/ragents.reference/ragents-bundle.json +3 -3
  88. package/bundles/ragents.todo/ragents-bundle.json +3 -3
  89. package/bundles/ragents.transcript/ragents-bundle.json +3 -3
  90. package/bundles/ragents.watch/ragents-bundle.json +3 -3
  91. package/bundles/ragents.workspace/ragents-bundle.json +11 -5
  92. package/bundles/ragents.workspace/server/index.js +101 -28
  93. package/bundles/ragents.workspace/server/index.js.map +3 -3
  94. package/bundles/ragents.workspace/web/classes.json +1 -1
  95. package/bundles/ragents.workspace/web/index.js +1 -1
  96. package/bundles/ragents.workspace/web/index.js.map +3 -3
  97. package/package.json +2 -2
  98. package/packages/agent/dist/core/agent-settings.d.ts +1 -7
  99. package/packages/agent/dist/core/compaction/compaction.d.ts +17 -13
  100. package/packages/agent/dist/core/model-runtime.d.ts +5 -4
  101. package/packages/agent/dist/core/provider-composer.d.ts +2 -1
  102. package/packages/agent/dist/core/tools/bash.d.ts +9 -3
  103. package/packages/agent/dist/index.d.ts +1 -1
  104. package/packages/agent/src/core/agent-settings.ts +1 -4
  105. package/packages/agent/src/core/compaction/compaction.ts +68 -30
  106. package/packages/agent/src/core/model-runtime.ts +12 -5
  107. package/packages/agent/src/core/provider-composer.ts +5 -0
  108. package/packages/agent/src/core/tools/bash.ts +36 -30
  109. package/packages/agent/src/index.ts +3 -0
  110. package/packages/ai/dist/types.d.ts +11 -0
  111. package/packages/ai/src/types.ts +12 -0
  112. package/packages/ragents/src/agents/plugins.ts +5 -0
  113. package/packages/ragents/src/domain/event-semantics.ts +3 -0
  114. package/packages/ragents/src/domain/event-validation.ts +15 -2
  115. package/packages/ragents/src/domain/events.ts +3 -0
  116. package/packages/ragents/src/domain/model.ts +2 -0
  117. package/packages/ragents/src/domain/projection.ts +1 -0
  118. package/packages/ragents/src/drivers/agent-runtime.ts +1 -1
  119. package/packages/ragents/src/drivers/agent-turn.ts +8 -6
  120. package/packages/ragents/src/index.ts +1 -0
  121. package/packages/ragents/src/plugin-host.ts +58 -10
  122. package/packages/ragents/src/plugin-types.ts +7 -2
  123. package/packages/ragents/src/runtime/decisions/inputs.ts +6 -2
  124. package/packages/ragents/src/runtime/journal-storage.ts +5 -4
  125. package/packages/workspace-executor/package.json +0 -1
  126. package/packages/workspace-executor/src/bash-launch.ts +56 -10
  127. package/packages/workspace-executor/src/context.ts +4 -0
  128. package/packages/workspace-executor/src/contributions.ts +150 -0
  129. package/packages/workspace-executor/src/executor.ts +1 -1
  130. package/packages/workspace-executor/src/index.ts +16 -19
  131. package/packages/workspace-executor/src/language-server/host.ts +3 -0
  132. package/packages/workspace-executor/src/language-server/module.ts +12 -2
  133. package/packages/workspace-executor/src/language-server/roots.ts +0 -5
  134. package/packages/workspace-executor/src/modules.ts +7 -6
  135. package/packages/workspace-executor/src/sandbox-tools.ts +3 -9
  136. package/packages/workspace-executor/src/shell-platform.ts +14 -3
  137. package/packages/workspace-executor/src/tools.ts +0 -3
  138. package/plugins/ragents.ask/ask-payload.ts +9 -0
  139. package/plugins/ragents.ask/server/ask-service.ts +53 -13
  140. package/plugins/ragents.ask/web/AskActionView.tsx +3 -2
  141. package/plugins/ragents.browser/executor/index.ts +5 -0
  142. package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/module.ts +25 -31
  143. package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/playwright.ts +9 -23
  144. package/plugins/ragents.browser/provision.ts +1 -1
  145. package/plugins/ragents.browser/server/browser.ts +3 -3
  146. package/plugins/ragents.browser/server/contract.ts +2 -2
  147. package/plugins/ragents.browser/server/index.ts +1 -1
  148. package/plugins/ragents.lsp-fsharp/executor.ts +80 -0
  149. package/plugins/ragents.lsp-fsharp/provision.ts +1 -1
  150. package/plugins/ragents.lsp-fsharp/server/index.ts +2 -2
  151. package/plugins/ragents.lsp-roslyn/executor.ts +64 -0
  152. package/plugins/ragents.lsp-roslyn/provision.ts +1 -1
  153. package/plugins/ragents.lsp-roslyn/server/index.ts +28 -5
  154. package/plugins/ragents.lsp-roslyn/server/solution-on-start.ts +18 -6
  155. package/plugins/ragents.lsp-typescript/executor.ts +50 -0
  156. package/plugins/ragents.lsp-typescript/server/index.ts +2 -2
  157. package/plugins/ragents.model-relay/server/config.ts +1 -1
  158. package/plugins/ragents.model-relay/server/relay.ts +4 -2
  159. package/plugins/ragents.profile-distribution/server/archive.ts +2 -1
  160. package/plugins/ragents.workspace/client/workspace-client.ts +111 -28
  161. package/plugins/ragents.workspace/contract.ts +33 -2
  162. package/plugins/ragents.workspace/server/binding.ts +10 -4
  163. package/plugins/ragents.workspace/server/clients.ts +56 -9
  164. package/plugins/ragents.workspace/server/config.ts +9 -0
  165. package/plugins/ragents.workspace/server/index.ts +13 -4
  166. package/plugins/ragents.workspace/server/runtime.ts +10 -0
  167. package/plugins/ragents.workspace/server/shell-platform.ts +6 -5
  168. package/plugins/ragents.workspace/web/WorkspaceBinding.tsx +1 -1
  169. package/ragents.config.core.ts +4 -1
  170. package/ragents.config.showcase.ts +4 -1
  171. package/scripts/agent/journal.ts +3 -1
  172. package/scripts/package/build-package.ts +3 -1
  173. package/scripts/package/publish-package.ts +4 -3
  174. package/scripts/provision/run-provision.ts +2 -2
  175. package/scripts/workspace-client/run-workspace-client.ts +7 -4
  176. package/apps/web/dist/help/screenshots/release-checklist-vscode.png +0 -0
  177. package/apps/web/dist/help/screenshots/release-checklist-web.png +0 -0
  178. package/bundles/ragents.ask/web/chunks/chunk-HGFDGTNC.js +0 -2
  179. package/bundles/ragents.ask/web/chunks/chunk-HGFDGTNC.js.map +0 -7
  180. package/bundles/ragents.browser/server/chunks/chunk-2GKLLCBE.js.map +0 -7
  181. package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +0 -60
  182. package/packages/workspace-executor/src/language-server/adapters/index.ts +0 -7
  183. package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +0 -56
  184. package/packages/workspace-executor/src/language-server/adapters/typescript.ts +0 -74
  185. package/packages/workspace-executor/src/language-server/solution.ts +0 -13
  186. /package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/contract.ts +0 -0
  187. /package/{packages/workspace-executor/src/browser → plugins/ragents.browser/executor}/pages.ts +0 -0
@@ -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)),
@@ -22,8 +22,3 @@ export const resolveRootDirectory = async (workspaceRoot: string, requested: str
22
22
  if (!info?.isDirectory()) throw new Error(`${requested} ist kein Verzeichnis im Arbeitsverzeichnis`);
23
23
  return absolute;
24
24
  };
25
-
26
- export const dotnetCommand = (server: string, args: readonly string[]): { command: string; args: string[] } =>
27
- server.toLowerCase().endsWith(".dll")
28
- ? { command: "dotnet", args: [server, ...args] }
29
- : { command: server, args: [...args] };
@@ -1,7 +1,6 @@
1
- import { browserModule } from "./browser/module.js";
2
1
  import { commandModule } from "./commands.js";
2
+ import type { WorkspaceExecutorParts } from "./contributions.js";
3
3
  import { fileModule } from "./files.js";
4
- import { workspaceLanguageServers } from "./language-server/adapters/index.js";
5
4
  import { languageServerModule } from "./language-server/module.js";
6
5
  import type { WorkspaceModuleFactory } from "./module.js";
7
6
  import { processModule } from "./processes/module.js";
@@ -11,15 +10,17 @@ import { sandboxToolsModule } from "./sandbox-tools.js";
11
10
  export interface WorkspaceExecutorModuleOptions {
12
11
  /** Der neue Ordner eines Runs auf dieser Maschine; nur ein Arbeitsplatz kennt ihn, auf dem Server legt ihn der Host an. */
13
12
  runFolder?: (runId: string) => string;
13
+ /** Die Beiträge der Plugins, gebaut für diese Maschine; Server und Arbeitsplatz bekommen dieselben. */
14
+ contributions: readonly WorkspaceExecutorParts[];
14
15
  }
15
16
 
16
- /** Die Module jedes Executors, im Server wie auf dem Arbeitsplatz dieselben. */
17
- export const workspaceExecutorModules = (options: WorkspaceExecutorModuleOptions = {}): readonly WorkspaceModuleFactory[] => [
17
+ /** Die Module eines Executors: die eigenen und was die Beiträge der Plugins hinzufügen. */
18
+ export const workspaceExecutorModules = (options: WorkspaceExecutorModuleOptions): readonly WorkspaceModuleFactory[] => [
18
19
  sandboxToolsModule,
19
- languageServerModule(workspaceLanguageServers),
20
+ languageServerModule(options.contributions.flatMap((parts) => parts.languageServers ?? [])),
20
21
  fileModule,
21
22
  processModule(),
22
23
  commandModule(),
23
- browserModule(),
24
+ ...options.contributions.flatMap((parts) => parts.modules ?? []),
24
25
  runFolderModule(options.runFolder),
25
26
  ];
@@ -40,11 +40,6 @@ export interface SandboxTools {
40
40
  shutdown: () => Promise<void>;
41
41
  }
42
42
 
43
- export const DEFAULT_BASH_TIMEOUT_SECONDS = (() => {
44
- const value = Number(process.env.RAGENTS_BASH_TIMEOUT_SECONDS);
45
- return Number.isFinite(value) && value > 0 ? value : 600;
46
- })();
47
-
48
43
  type ToolResult = { content?: Array<{ type: string; text?: string }>; details?: unknown };
49
44
 
50
45
  /** Der Stand einer Datei, den das Modell zuletzt gesehen hat; der Host führt ihn je Actor und gibt ihn mit, das Modell nennt ihn nie. */
@@ -269,7 +264,7 @@ export const createSandboxTools = async (
269
264
  const context = await contextFor();
270
265
  if (shuttingDown) throw new Error("Die Werkzeuge werden killed");
271
266
  options.signal?.throwIfAborted();
272
- const bash = bashLaunch(context.bash, command, context.env);
267
+ const bash = bashLaunch({ bash: context.bash, rg: context.rg }, command, context.env);
273
268
  const launch = await sandboxedLaunch(context, bash);
274
269
  if (shuttingDown) throw new Error("Die Werkzeuge werden killed");
275
270
  options.signal?.throwIfAborted();
@@ -303,11 +298,10 @@ export const createSandboxTools = async (
303
298
  }
304
299
  });
305
300
  };
306
- const timeoutSeconds = options.timeout ?? DEFAULT_BASH_TIMEOUT_SECONDS;
307
301
  const timer = setTimeout(() => {
308
302
  timedOut = true;
309
303
  kill();
310
- }, timeoutSeconds * 1000);
304
+ }, options.timeout * 1000);
311
305
  options.signal?.addEventListener("abort", kill);
312
306
  const cleanup = () => {
313
307
  if (timer) clearTimeout(timer);
@@ -322,7 +316,7 @@ export const createSandboxTools = async (
322
316
  const finish = () => processError
323
317
  ? reject(processError)
324
318
  : timedOut
325
- ? reject(new Error(`timeout:${timeoutSeconds}`))
319
+ ? reject(new Error(`timeout:${options.timeout}`))
326
320
  : resolve({ exitCode: killed ? null : code });
327
321
  if (!child.pid) {
328
322
  finish();
@@ -1,11 +1,22 @@
1
1
  const platformTexts: Partial<Record<NodeJS.Platform, string>> = {
2
- darwin: "`bash` runs on macOS with the BSD userland: `grep` has no `-P` (use `grep -E` or `rg`), in-place `sed` needs an explicit backup suffix (`sed -i '' ...`), `date` uses `-v` instead of `-d`, `xargs` uses `-I{}` instead of `-i`, and other GNU-only flags are unavailable. `/bin/bash` is version 3.2: no associative arrays (`declare -A`), no `mapfile`/`readarray`, no `${var,,}` case conversion.",
2
+ darwin: "`bash` runs on macOS with the BSD userland: `grep` has no `-P` (use `grep -E`), in-place `sed` needs an explicit backup suffix (`sed -i '' ...`), `date` uses `-v` instead of `-d`, `xargs` uses `-I{}` instead of `-i`, and other GNU-only flags are unavailable. `/bin/bash` is version 3.2: no associative arrays (`declare -A`), no `mapfile`/`readarray`, no `${var,,}` case conversion.",
3
3
  linux: "`bash` runs on Linux with the GNU userland: `grep -P`, `sed -i` without suffix and the usual GNU flags work.",
4
4
  win32: "`bash` runs on Windows with the bash RAgents brings along, an MSYS userland with the GNU tools (coreutils, `grep`, `sed`, `awk`, `find`, `xargs`, `diff`, `patch`, `tar`, `gzip`, `unzip`, `file`, `cygpath`, `dos2unix`): `grep -P`, `sed -i` without suffix and the usual GNU flags work. `git`, `dotnet` and `node` are the Windows programs of this machine on the `PATH`, with the user's own configuration and login; they take Windows paths, not `/c/...` (`cygpath -w` converts). The file system is the Windows one: absolute paths are `C:/project/...` or `/c/project/...`, both spellings reach the same file, and a path from another tool may use backslashes. Files often carry CRLF line endings, so anchor patterns with `\\r\\?$` and keep the line ending a file already has.",
5
5
  };
6
6
 
7
- export const shellPlatformText = (platform: NodeJS.Platform = process.platform): string => {
7
+ const searchTexts = {
8
+ ripgrep: "Search code with `rg` (ripgrep) and list files with `rg --files`: it skips hidden files and everything `.gitignore` excludes, such as `node_modules`, `bin` and `obj`, and is far faster than `grep -r` (`rg -uu` searches those too). Use `grep` only on single files or in pipes.",
9
+ grep: "`rg` (ripgrep) is not available here. `grep -r` also walks dependency and build folders, so exclude them: `grep -r --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=bin --exclude-dir=obj ...`.",
10
+ };
11
+
12
+ /** Was der Executor dem Werkzeug bash über die Plattform hinaus bietet. */
13
+ export interface ShellTools {
14
+ readonly ripgrep: boolean;
15
+ }
16
+
17
+ export const shellPlatformText = (platform: NodeJS.Platform, tools: ShellTools): string => {
8
18
  const text = platformTexts[platform];
9
19
  if (!text) throw new Error(`Für die Plattform ${platform} gibt es keine Shell-Beschreibung.`);
10
- return `${text} A nonzero exit code, for example \`grep\` without a match, comes back as an ordinary result with the code at the end, not as a tool error; only start, timeout and abort problems are tool errors.`;
20
+ const search = tools.ripgrep ? searchTexts.ripgrep : searchTexts.grep;
21
+ return `${text} ${search} A nonzero exit code, for example \`grep\` without a match, comes back as an ordinary result with the code at the end, not as a tool error; only start, timeout and abort problems are tool errors.`;
11
22
  };
@@ -23,6 +23,3 @@ export const workspaceDataDirectory = (home = homedir()): string => path.join(ra
23
23
  /** Der Datenordner dieses Prozesses: DATA_DIR wie beim Server, sonst der des Arbeitsplatzes ohne Profil. */
24
24
  export const hostDataDirectory = (environment: NodeJS.ProcessEnv = process.env): string =>
25
25
  path.resolve(environment.DATA_DIR ?? workspaceDataDirectory());
26
-
27
- export const hostToolFile = (pluginId: string, relative: string): string =>
28
- path.join(pluginToolsDirectory(hostDataDirectory(), pluginId), ...relative.split("/"));
@@ -1,5 +1,8 @@
1
1
  export const ASK_PLUGIN_ID = "ragents.ask";
2
2
 
3
+ /** What the asking tool call gets when a person's message to the asker closes its question. */
4
+ export const SUPERSEDED_ANSWER = "Not answered: the user sent a new message instead.";
5
+
3
6
  export interface AskPayload {
4
7
  question: string;
5
8
  options: string[];
@@ -23,3 +26,9 @@ export const askPayloadOf = (payload: unknown): AskPayload | undefined => {
23
26
  ...(typeof candidate.recipient === "string" ? { recipient: candidate.recipient } : {}),
24
27
  };
25
28
  };
29
+
30
+ export const supersedingInputOf = (result: unknown): string | undefined => {
31
+ if (typeof result !== "object" || result === null) return undefined;
32
+ const { supersededBy } = result as { supersededBy?: unknown };
33
+ return typeof supersededBy === "string" ? supersededBy : undefined;
34
+ };
@@ -4,20 +4,34 @@ import {
4
4
  type Action,
5
5
  type CommandContext,
6
6
  type JournalEvent,
7
+ type JsonValue,
7
8
  type Orchestration,
8
9
  } from "@ragents/engine";
9
- import { askPayloadOf, ASK_PLUGIN_ID } from "../ask-payload.js";
10
+ import { askPayloadOf, ASK_PLUGIN_ID, SUPERSEDED_ANSWER, supersedingInputOf } from "../ask-payload.js";
10
11
  import { DISMISSED_ANSWER, type AskCall, type AskRequest, type AskService } from "./contract.js";
11
12
 
12
13
  const answerOf = (decision: "approved" | "dismissed", result: unknown): string =>
13
- decision !== "approved" ? DISMISSED_ANSWER : typeof result === "string" ? result : result === null || result === undefined ? "" : JSON.stringify(result);
14
+ decision !== "approved"
15
+ ? supersedingInputOf(result) === undefined ? DISMISSED_ANSWER : SUPERSEDED_ANSWER
16
+ : typeof result === "string" ? result : result === null || result === undefined ? "" : JSON.stringify(result);
14
17
 
15
18
  const resolvedAnswerOf = (action: Action): string | null =>
16
19
  action.status === "pending" ? null : answerOf(action.status, action.result);
17
20
 
21
+ const humanInputPendingFor = (runtime: Orchestration, runId: string, actorId: string): boolean =>
22
+ [...runtime.state(runId).inputs.values()].some((input) =>
23
+ input.actorId === actorId && input.origin === "human" && input.lifecycle.kind === "pending");
24
+
25
+ interface Waiter {
26
+ runId: string;
27
+ /** The actor whose running turn waits for the answer; null for a question asked outside a turn. */
28
+ blockedActorId: string | null;
29
+ settle: (answer: string) => void;
30
+ }
31
+
18
32
  export class RuntimeAskService implements AskService {
19
33
  #runtime: Orchestration | undefined;
20
- readonly #waiters = new Map<string, (answer: string) => void>();
34
+ readonly #waiters = new Map<string, Waiter>();
21
35
  readonly #aborting = new Set<string>();
22
36
 
23
37
  bind(runtime: Orchestration): void {
@@ -28,6 +42,7 @@ export class RuntimeAskService implements AskService {
28
42
 
29
43
  ask(call: AskCall, request: AskRequest, signal: AbortSignal | undefined): Promise<string> {
30
44
  const runtime = this.#requireRuntime();
45
+ if (call.turnId && humanInputPendingFor(runtime, call.runId, call.agentId)) return Promise.resolve(SUPERSEDED_ANSWER);
31
46
  const context: CommandContext = {
32
47
  actorId: call.agentId,
33
48
  commandId: call.commandId,
@@ -51,7 +66,7 @@ export class RuntimeAskService implements AskService {
51
66
  if (proposed?.type !== "action.proposed") {
52
67
  throw new Error(`Die Frage von ${call.agentId} wurde nicht im Journal von ${call.runId} angelegt`);
53
68
  }
54
- return this.#awaitAnswer(call.runId, proposed.payload.actionId, signal);
69
+ return this.#awaitAnswer(call.runId, proposed.payload.actionId, call.turnId ? call.agentId : null, signal);
55
70
  }
56
71
 
57
72
  answer(runId: string, actionId: string, input: { answer?: string; dismiss?: boolean }): void {
@@ -72,6 +87,18 @@ export class RuntimeAskService implements AskService {
72
87
  }
73
88
 
74
89
  withdraw(runId: string, actionId: string): void {
90
+ this.#dismiss(runId, actionId, "ask-withdraw", null, DISMISSED_ANSWER);
91
+ }
92
+
93
+ #supersede(runId: string, actionId: string, inputId: string): void {
94
+ try {
95
+ this.#dismiss(runId, actionId, "ask-supersede", { supersededBy: inputId }, SUPERSEDED_ANSWER);
96
+ } catch (error) {
97
+ console.error(`Question ${actionId} could not be closed for message ${inputId}: ${error instanceof Error ? error.message : String(error)}`);
98
+ }
99
+ }
100
+
101
+ #dismiss(runId: string, actionId: string, commandPrefix: string, result: JsonValue, answer: string): void {
75
102
  const runtime = this.#requireRuntime();
76
103
  const state = runtime.state(runId);
77
104
  const action = state.actions.get(actionId);
@@ -79,18 +106,18 @@ export class RuntimeAskService implements AskService {
79
106
  this.#aborting.add(actionId);
80
107
  try {
81
108
  runtime.resolveAction(
82
- { actorId: state.ownerId, commandId: `ask-withdraw:${actionId}:${randomUUID()}` },
109
+ { actorId: state.ownerId, commandId: `${commandPrefix}:${actionId}:${randomUUID()}` },
83
110
  runId,
84
111
  actionId,
85
- { decision: "dismissed", result: null },
112
+ { decision: "dismissed", result },
86
113
  );
87
114
  } finally {
88
115
  this.#aborting.delete(actionId);
89
116
  }
90
- this.#waiters.get(actionId)?.(DISMISSED_ANSWER);
117
+ this.#waiters.get(actionId)?.settle(answer);
91
118
  }
92
119
 
93
- #awaitAnswer(runId: string, actionId: string, signal: AbortSignal | undefined): Promise<string> {
120
+ #awaitAnswer(runId: string, actionId: string, blockedActorId: string | null, signal: AbortSignal | undefined): Promise<string> {
94
121
  const runtime = this.#requireRuntime();
95
122
  const action = runtime.state(runId).actions.get(actionId);
96
123
  if (!action) throw new Error(`Frage ${actionId} existiert nicht in Run ${runId}`);
@@ -123,16 +150,29 @@ export class RuntimeAskService implements AskService {
123
150
  return;
124
151
  }
125
152
  signal?.addEventListener("abort", abort, { once: true });
126
- this.#waiters.set(actionId, (answer) => {
127
- signal?.removeEventListener("abort", abort);
128
- this.#waiters.delete(actionId);
129
- resolve(answer);
153
+ this.#waiters.set(actionId, {
154
+ runId,
155
+ blockedActorId,
156
+ settle: (answer) => {
157
+ signal?.removeEventListener("abort", abort);
158
+ this.#waiters.delete(actionId);
159
+ resolve(answer);
160
+ },
130
161
  });
131
162
  });
132
163
  }
133
164
 
134
165
  #onJournal(events: JournalEvent[]): void {
135
166
  for (const event of events) {
167
+ if (event.type === "actor.input.enqueued") {
168
+ if (event.payload.origin !== "human") continue;
169
+ const { actorId, inputId } = event.payload;
170
+ for (const [actionId, waiter] of this.#waiters) {
171
+ if (waiter.runId !== event.runId || waiter.blockedActorId !== actorId) continue;
172
+ queueMicrotask(() => this.#supersede(event.runId, actionId, inputId));
173
+ }
174
+ continue;
175
+ }
136
176
  if (event.type !== "action.resolved") continue;
137
177
  if (this.#aborting.has(event.payload.actionId)) continue;
138
178
  const resolved = this.#requireRuntime().state(event.runId).actions.get(event.payload.actionId);
@@ -140,7 +180,7 @@ export class RuntimeAskService implements AskService {
140
180
  const answer = answerOf(event.payload.decision, event.payload.result);
141
181
  const waiter = this.#waiters.get(event.payload.actionId);
142
182
  if (waiter) {
143
- waiter(answer);
183
+ waiter.settle(answer);
144
184
  continue;
145
185
  }
146
186
  queueMicrotask(() => this.#enqueueAnswer(event.runId, event.payload.actionId, answer));
@@ -1,7 +1,7 @@
1
1
  import { useAccess } from "@ragents/web/AccessContext";
2
2
  import { useState } from "react";
3
3
  import type { ActionViewContext } from "@ragents/web/PluginRegistry";
4
- import { askPayloadOf } from "../ask-payload";
4
+ import { askPayloadOf, SUPERSEDED_ANSWER, supersedingInputOf } from "../ask-payload";
5
5
  import { answerQuestion } from "./api";
6
6
  import { QuestionCard } from "./QuestionCard";
7
7
 
@@ -25,7 +25,8 @@ export function AskActionView({ action, session, text }: ActionViewContext) {
25
25
  <QuestionCard
26
26
  answer={action.status === undefined
27
27
  ? undefined
28
- : action.status === "approved" ? answerTextOf(action.result) : "Der Benutzer hat die Frage verworfen."}
28
+ : action.status === "approved" ? answerTextOf(action.result)
29
+ : supersedingInputOf(action.result) !== undefined ? SUPERSEDED_ANSWER : "Der Benutzer hat die Frage verworfen."}
29
30
  onAnswer={access.can("runs.write") ? answer : undefined}
30
31
  question={question}
31
32
  text={text}
@@ -0,0 +1,5 @@
1
+ import type { WorkspaceExecutorContribution } from "@ragents/workspace-executor";
2
+ import { browserModule } from "./module.js";
3
+
4
+ /** Der Browser läuft dort, wo der Arbeitsbereich des Runs liegt: im Executor des Servers oder in dem des Arbeitsplatzes. */
5
+ export const executor: WorkspaceExecutorContribution = (machine) => ({ modules: [browserModule(machine)] });
@@ -1,6 +1,5 @@
1
1
  import type { Browser } from "playwright-core";
2
- import { WorkspaceOperationError } from "../errors.js";
3
- import type { WorkspaceModuleFactory } from "../module.js";
2
+ import type { WorkspaceExecutorMachine, WorkspaceModuleFactory } from "@ragents/workspace-executor";
4
3
  import { BROWSER_OPERATIONS, type BrowserCheck, type BrowserTarget, type BrowserViewport } from "./contract.js";
5
4
  import { BrowserPages } from "./pages.js";
6
5
  import { launchChromium } from "./playwright.js";
@@ -14,38 +13,33 @@ export interface BrowserModuleOptions {
14
13
  checkTimeoutMs?: number;
15
14
  }
16
15
 
17
- const invalid = (message: string): WorkspaceOperationError => new WorkspaceOperationError("browser-input-invalid", message, 400);
18
-
19
- const fieldsOf = (input: unknown): Readonly<Record<string, unknown>> => {
20
- if (typeof input !== "object" || input === null || Array.isArray(input)) throw invalid("Die Eingabe einer Browseroperation ist ein Objekt");
21
- return input as Readonly<Record<string, unknown>>;
22
- };
23
-
24
- const textOf = (input: unknown, key: string): string => {
25
- const value = fieldsOf(input)[key];
26
- if (typeof value !== "string") throw invalid(`Die Browseroperation braucht ${key} als Text`);
27
- return value;
28
- };
29
-
30
- const targetOf = (input: unknown): BrowserTarget => {
31
- const value = fieldsOf(input).target;
32
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalid("Die Browseroperation braucht ein Ziel");
33
- return value as BrowserTarget;
34
- };
35
-
36
- const viewportOf = (input: unknown): BrowserViewport => {
37
- const { width, height } = fieldsOf(input);
38
- if (typeof width !== "number" || typeof height !== "number" || !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
39
- throw invalid("Ein Viewport braucht Breite und Höhe als positive ganze Zahlen");
40
- }
41
- return { width, height };
42
- };
43
-
44
16
  /** Der Browser eines Runs auf dieser Maschine: Seite, Aktionen, Prüfungen und Aufnahmen; Evidenz und Ablage hält der Aufrufer. */
45
- export const browserModule = (options: BrowserModuleOptions = {}): WorkspaceModuleFactory => (host) => {
17
+ export const browserModule = (machine: WorkspaceExecutorMachine, options: BrowserModuleOptions = {}): WorkspaceModuleFactory => (host) => {
18
+ const invalid = (message: string): Error => machine.operationError("browser-input-invalid", message, 400);
19
+ const fieldsOf = (input: unknown): Readonly<Record<string, unknown>> => {
20
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw invalid("Die Eingabe einer Browseroperation ist ein Objekt");
21
+ return input as Readonly<Record<string, unknown>>;
22
+ };
23
+ const textOf = (input: unknown, key: string): string => {
24
+ const value = fieldsOf(input)[key];
25
+ if (typeof value !== "string") throw invalid(`Die Browseroperation braucht ${key} als Text`);
26
+ return value;
27
+ };
28
+ const targetOf = (input: unknown): BrowserTarget => {
29
+ const value = fieldsOf(input).target;
30
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalid("Die Browseroperation braucht ein Ziel");
31
+ return value as BrowserTarget;
32
+ };
33
+ const viewportOf = (input: unknown): BrowserViewport => {
34
+ const { width, height } = fieldsOf(input);
35
+ if (typeof width !== "number" || typeof height !== "number" || !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
36
+ throw invalid("Ein Viewport braucht Breite und Höhe als positive ganze Zahlen");
37
+ }
38
+ return { width, height };
39
+ };
46
40
  const timeoutMs = options.timeoutMs ?? BROWSER_TIMEOUT_MS;
47
41
  const pages = new BrowserPages({
48
- launch: options.launch ?? (async (runId) => launchChromium((await host.contextFor(runId)).hostRoot, runId, timeoutMs)),
42
+ launch: options.launch ?? (async (runId) => launchChromium(machine, (await host.contextFor(runId)).hostRoot, runId, timeoutMs)),
49
43
  timeoutMs,
50
44
  checkTimeoutMs: options.checkTimeoutMs ?? Math.min(5_000, timeoutMs),
51
45
  });