@schlenkr/ragents 0.1.6 → 0.1.7

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 (145) hide show
  1. package/apps/server/src/chat-handler.ts +2 -0
  2. package/apps/server/src/config.ts +6 -0
  3. package/apps/server/src/host-api.json +1 -0
  4. package/apps/server/src/layout.ts +0 -15
  5. package/apps/server/src/plugin-support/agent-tool.ts +2 -3
  6. package/apps/server/src/provider.ts +34 -11
  7. package/apps/server/src/ragents/actor-chat-history.ts +1 -0
  8. package/apps/server/src/ragents/engine.ts +5 -13
  9. package/apps/server/src/ragents/journal-chat-events.ts +2 -0
  10. package/apps/server/src/ragents/run-owner.ts +3 -2
  11. package/apps/server/src/ragents/session.ts +3 -3
  12. package/apps/server/src/run-preparation.ts +14 -18
  13. package/apps/server/src/run-transfer.ts +65 -4
  14. package/apps/server/src/settings.ts +2 -4
  15. package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-B9qYBARS.js → highlighted-body-KPVGNVTW-CFhTJEmH.js} +1 -1
  16. package/apps/web/dist/assets/{main-BCjvD2Hh.js → main-DlzW4t4c.js} +1 -1
  17. package/apps/web/dist/assets/{mermaid-HWGCJPDP-CvYYFK_n.js → mermaid-HWGCJPDP-kxkB991i.js} +69 -69
  18. package/apps/web/dist/assets/{runPanel-CG7kRMJh.js → runPanel-BJEFQ__w.js} +1 -1
  19. package/apps/web/dist/help/guide-clients.html +13 -4
  20. package/apps/web/dist/help/guide-clients.md +14 -4
  21. package/apps/web/dist/help/guide-distributed.html +8 -3
  22. package/apps/web/dist/help/guide-distributed.md +8 -3
  23. package/apps/web/dist/help/guide-runtime.html +18 -12
  24. package/apps/web/dist/help/guide-runtime.md +19 -12
  25. package/apps/web/dist/help/mini-app.css +23 -3
  26. package/apps/web/dist/host-web.json +3 -3
  27. package/apps/web/dist/index.html +2 -2
  28. package/apps/web/dist/run-panel.html +2 -2
  29. package/apps/web/src/SessionList.tsx +16 -8
  30. package/apps/web/src/api.ts +2 -0
  31. package/apps/web/src/panel/PanelPage.tsx +1 -1
  32. package/apps/web/src/panel/RunLine.tsx +4 -2
  33. package/apps/web/src/panel/contract.ts +2 -0
  34. package/bundles/ragents.activity/ragents-bundle.json +1 -1
  35. package/bundles/ragents.actor-programs/ragents-bundle.json +1 -1
  36. package/bundles/ragents.ask/ragents-bundle.json +1 -1
  37. package/bundles/ragents.browser/ragents-bundle.json +1 -1
  38. package/bundles/ragents.documents/ragents-bundle.json +1 -1
  39. package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
  40. package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
  41. package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
  42. package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
  43. package/bundles/ragents.orchestration/ragents-bundle.json +1 -1
  44. package/bundles/ragents.overseer/ragents-bundle.json +1 -1
  45. package/bundles/ragents.processes/ragents-bundle.json +1 -1
  46. package/bundles/ragents.product/ragents-bundle.json +1 -1
  47. package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
  48. package/bundles/ragents.reference/ragents-bundle.json +2 -2
  49. package/bundles/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
  50. package/bundles/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
  51. package/bundles/ragents.todo/ragents-bundle.json +1 -1
  52. package/bundles/ragents.transcript/ragents-bundle.json +1 -1
  53. package/bundles/ragents.watch/ragents-bundle.json +1 -1
  54. package/bundles/ragents.workspace/ragents-bundle.json +1 -1
  55. package/package.json +2 -2
  56. package/packages/agent/dist/core/agent-settings.d.ts +2 -2
  57. package/packages/agent/dist/core/compaction/compaction.d.ts +16 -50
  58. package/packages/agent/dist/core/context-log.d.ts +25 -0
  59. package/packages/agent/dist/core/messages.d.ts +3 -10
  60. package/packages/agent/dist/core/tool-definition.d.ts +17 -0
  61. package/packages/agent/dist/core/tools/bash.d.ts +2 -7
  62. package/packages/agent/dist/core/tools/edit.d.ts +1 -1
  63. package/packages/agent/dist/core/tools/read.d.ts +1 -1
  64. package/packages/agent/dist/core/tools/write.d.ts +1 -1
  65. package/packages/agent/dist/index.d.ts +7 -8
  66. package/packages/agent/dist/loop/agent.d.ts +2 -6
  67. package/packages/agent/package.json +1 -1
  68. package/packages/agent/src/core/agent-settings.ts +2 -2
  69. package/packages/agent/src/core/compaction/compaction.ts +49 -175
  70. package/packages/agent/src/core/context-log.ts +49 -0
  71. package/packages/agent/src/core/messages.ts +5 -11
  72. package/packages/agent/src/core/tool-definition.ts +29 -0
  73. package/packages/agent/src/core/tools/bash.ts +2 -9
  74. package/packages/agent/src/core/tools/edit.ts +2 -11
  75. package/packages/agent/src/core/tools/read.ts +3 -18
  76. package/packages/agent/src/core/tools/write.ts +1 -5
  77. package/packages/agent/src/index.ts +28 -14
  78. package/packages/agent/src/loop/agent-loop.ts +1 -2
  79. package/packages/agent/src/loop/agent.ts +8 -2
  80. package/packages/ai/dist/api/ai-sdk.d.ts +2 -0
  81. package/packages/ai/dist/types.d.ts +2 -0
  82. package/packages/ai/src/api/ai-sdk-messages.ts +15 -10
  83. package/packages/ai/src/api/ai-sdk.ts +11 -0
  84. package/packages/ai/src/api/transform-messages.ts +1 -1
  85. package/packages/ai/src/types.ts +2 -0
  86. package/packages/ai/src/utils/retry.ts +14 -0
  87. package/packages/ragents/src/agents/actor-input.ts +15 -0
  88. package/packages/ragents/src/agents/model-context.ts +360 -0
  89. package/packages/ragents/src/agents/scheduler.ts +3 -7
  90. package/packages/ragents/src/agents/tools.ts +2 -2
  91. package/packages/ragents/src/agents/toolset.ts +47 -20
  92. package/packages/ragents/src/agents/workspaces.ts +0 -6
  93. package/packages/ragents/src/domain/event-semantics.ts +51 -0
  94. package/packages/ragents/src/domain/event-validation.ts +166 -0
  95. package/packages/ragents/src/domain/events.ts +72 -0
  96. package/packages/ragents/src/domain/model.ts +4 -2
  97. package/packages/ragents/src/domain/projection.ts +9 -0
  98. package/packages/ragents/src/domain/tool-result-text.ts +9 -0
  99. package/packages/ragents/src/drivers/agent-hooks.ts +25 -38
  100. package/packages/ragents/src/drivers/agent-runtime.ts +118 -933
  101. package/packages/ragents/src/drivers/agent-turn.ts +735 -0
  102. package/packages/ragents/src/drivers/agent.ts +9 -7
  103. package/packages/ragents/src/drivers/skill-preload.ts +53 -63
  104. package/packages/ragents/src/drivers/types.ts +21 -5
  105. package/packages/ragents/src/index.ts +7 -6
  106. package/packages/ragents/src/plugin-host.ts +3 -4
  107. package/packages/ragents/src/runtime/decisions/actors.ts +10 -0
  108. package/packages/ragents/src/runtime/decisions/turns.ts +66 -7
  109. package/packages/ragents/src/runtime/journal-storage.ts +7 -4
  110. package/packages/ragents/src/runtime/journal.ts +35 -0
  111. package/packages/ragents/src/runtime/orchestration.ts +140 -9
  112. package/packages/workspace-executor/src/sandbox-tools.ts +10 -11
  113. package/plugins/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
  114. package/plugins/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
  115. package/scripts/agent/journal.ts +5 -3
  116. package/packages/agent/dist/core/agent-session-runtime.d.ts +0 -33
  117. package/packages/agent/dist/core/agent-session-services.d.ts +0 -35
  118. package/packages/agent/dist/core/agent-session.d.ts +0 -234
  119. package/packages/agent/dist/core/auth-guidance.d.ts +0 -4
  120. package/packages/agent/dist/core/defaults.d.ts +0 -3
  121. package/packages/agent/dist/core/extensions/index.d.ts +0 -11
  122. package/packages/agent/dist/core/extensions/loader.d.ts +0 -12
  123. package/packages/agent/dist/core/extensions/runner.d.ts +0 -39
  124. package/packages/agent/dist/core/extensions/types.d.ts +0 -156
  125. package/packages/agent/dist/core/extensions/wrapper.d.ts +0 -12
  126. package/packages/agent/dist/core/resource-loader.d.ts +0 -39
  127. package/packages/agent/dist/core/sdk.d.ts +0 -38
  128. package/packages/agent/dist/core/session-manager.d.ts +0 -189
  129. package/packages/agent/dist/core/source-info.d.ts +0 -16
  130. package/packages/agent/dist/core/system-prompt.d.ts +0 -21
  131. package/packages/agent/src/core/agent-session-runtime.ts +0 -94
  132. package/packages/agent/src/core/agent-session-services.ts +0 -74
  133. package/packages/agent/src/core/agent-session.ts +0 -1108
  134. package/packages/agent/src/core/auth-guidance.ts +0 -14
  135. package/packages/agent/src/core/defaults.ts +0 -3
  136. package/packages/agent/src/core/extensions/index.ts +0 -38
  137. package/packages/agent/src/core/extensions/loader.ts +0 -125
  138. package/packages/agent/src/core/extensions/runner.ts +0 -211
  139. package/packages/agent/src/core/extensions/types.ts +0 -197
  140. package/packages/agent/src/core/extensions/wrapper.ts +0 -24
  141. package/packages/agent/src/core/resource-loader.ts +0 -142
  142. package/packages/agent/src/core/sdk.ts +0 -123
  143. package/packages/agent/src/core/session-manager.ts +0 -743
  144. package/packages/agent/src/core/source-info.ts +0 -28
  145. package/packages/agent/src/core/system-prompt.ts +0 -108
@@ -1,38 +1,12 @@
1
- import { randomUUID } from "node:crypto";
2
- import { closeSync, fsyncSync, openSync } from "node:fs";
3
- import { access, chmod, mkdir, open, readFile, rename } from "node:fs/promises";
4
- import { basename, dirname, join, resolve } from "node:path";
5
-
6
- import {
7
- createAgentSessionFromServices,
8
- createAgentSessionRuntime,
9
- createAgentSessionServices,
10
- defineTool,
11
- CURRENT_SESSION_VERSION,
12
- SessionManager,
13
- type AgentSession,
14
- type SessionEntry,
15
- type AgentSessionRuntime,
16
- type CreateAgentSessionRuntimeFactory,
17
- type ExtensionAPI,
18
- type ExtensionContext,
19
- type ExtensionError,
20
- type InlineExtension,
21
- type ModelRuntime,
22
- type Skill,
23
- } from "@ragents/agent";
24
- import type { UserMessage } from "@ragents/ai";
25
-
26
- import { ignoredFieldsNotice } from "../agents/toolset.ts";
27
- import { assertJsonValue, type JsonValue } from "../domain/json.ts";
28
- import { addUsage, emptyUsage, type TurnUsage } from "../domain/model.ts";
29
- import { type TurnRequest, type TurnResult } from "./types.ts";
30
- import {
31
- createSkillPreloadExtension,
32
- type SkillPreloadConfiguration,
33
- } from "./skill-preload.ts";
34
- import { prepareInputAttachments } from "./attachments.ts";
1
+ import { agentSettings, type AgentSettingsInput, type ModelRuntime, type Skill, type ThinkingLevel as LoopThinkingLevel } from "@ragents/agent";
2
+ import { clampThinkingLevel, getSupportedThinkingLevels } from "@ragents/ai";
3
+
4
+ import { emptyUsage, type TurnUsage } from "../domain/model.ts";
35
5
  import type { ThinkingLevel } from "../domain/driver.ts";
6
+ import type { AgentHook } from "./agent-hooks.ts";
7
+ import { AgentTurn } from "./agent-turn.ts";
8
+ import { createSkillPreload, type SkillPreload, type SkillPreloadConfiguration } from "./skill-preload.ts";
9
+ import type { TurnRequest, TurnResult } from "./types.ts";
36
10
 
37
11
  export type AgentRuntimeContext = {
38
12
  runId: string;
@@ -40,7 +14,6 @@ export type AgentRuntimeContext = {
40
14
  /** The working directory of the workspace tools; a runtime stays bound to it, and it is never a local path of this runtime. */
41
15
  workspace: string;
42
16
  allowedToolNames: readonly string[] | null;
43
- forkOf: string | null;
44
17
  };
45
18
 
46
19
  export type AgentRuntimeDiagnostic = {
@@ -49,11 +22,6 @@ export type AgentRuntimeDiagnostic = {
49
22
  context: AgentRuntimeContext;
50
23
  };
51
24
 
52
- export type AgentSessionStore = {
53
- directory: (runId: string, agentId: string) => string | undefined;
54
- directoryMode: number;
55
- };
56
-
57
25
  export type AgentRuntimeManagerOptions = {
58
26
  modelRuntime: ModelRuntime | Promise<ModelRuntime>;
59
27
  /** The skills of an agent, read and checked by the host. */
@@ -63,29 +31,17 @@ export type AgentRuntimeManagerOptions = {
63
31
  ) => readonly Skill[] | Promise<readonly Skill[]>;
64
32
  skillPreload?: false | SkillPreloadConfiguration;
65
33
  thinkingLevel?: ThinkingLevel;
66
- sessions?: AgentSessionStore;
67
- extensionFactories?: readonly InlineExtension[];
68
- resolveExtensionFactories?: (
34
+ /** The hooks of the plugins, bound to the agent. */
35
+ resolveHooks?: (
69
36
  context: AgentRuntimeContext,
70
37
  signal: AbortSignal,
71
- ) => readonly InlineExtension[] | Promise<readonly InlineExtension[]>;
38
+ ) => readonly AgentHook[] | Promise<readonly AgentHook[]>;
39
+ /** Compaction, retry and provider request limits; unset values keep the defaults. */
40
+ settings?: AgentSettingsInput;
72
41
  turnAbortTimeoutMs?: number;
73
42
  onDiagnostic?: (diagnostic: AgentRuntimeDiagnostic) => void;
74
43
  };
75
44
 
76
- type AgentUsage = {
77
- input: number;
78
- output: number;
79
- cacheRead: number;
80
- cacheWrite: number;
81
- cost?: { total?: number };
82
- };
83
-
84
- type RuntimeConfiguration = {
85
- selection: TurnRequest<"agent">["selection"];
86
- systemPrompt: string;
87
- };
88
-
89
45
  const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
90
46
 
91
47
  const sameToolNames = (left: readonly string[] | null, right: readonly string[] | null) =>
@@ -198,79 +154,6 @@ class TurnAbortDeadline {
198
154
  }
199
155
  }
200
156
 
201
- const syncFile = (file: string) => {
202
- const descriptor = openSync(file, "r+");
203
-
204
- try {
205
- fsyncSync(descriptor);
206
- } finally {
207
- closeSync(descriptor);
208
- }
209
- };
210
-
211
- const syncDirectory = async (directory: string) => {
212
- const handle = await open(directory, "r");
213
-
214
- try {
215
- await handle.sync();
216
- } finally {
217
- await handle.close();
218
- }
219
- };
220
-
221
- const writeDurably = async (file: string, content: string) => {
222
- const handle = await open(file, "wx");
223
-
224
- try {
225
- await handle.writeFile(content);
226
- await handle.sync();
227
- } finally {
228
- await handle.close();
229
- }
230
- };
231
-
232
- const usageOf = (usage: AgentUsage | undefined): TurnUsage => ({
233
- inputTokens: usage?.input ?? 0,
234
- outputTokens: usage?.output ?? 0,
235
- cacheReadTokens: usage?.cacheRead ?? 0,
236
- cacheWriteTokens: usage?.cacheWrite ?? 0,
237
- costUsd: usage?.cost?.total ?? 0,
238
- });
239
-
240
- const asJson = (value: unknown) => {
241
- try {
242
- return JSON.stringify(value ?? {});
243
- } catch {
244
- return String(value);
245
- }
246
- };
247
-
248
- const asJsonValue = (value: unknown): JsonValue => JSON.parse(asJson(value)) as JsonValue;
249
-
250
- /** The conversation and its latest compaction; a new session or a compaction yields another model context. */
251
- const modelContextOf = (branch: readonly SessionEntry[]): string =>
252
- `${branch[0]?.id ?? ""}:${branch.findLast((entry) => entry.type === "compaction")?.id ?? ""}`;
253
-
254
- const asText = (value: unknown): string => {
255
- if (typeof value === "string")
256
- return value;
257
-
258
- const content = (value as { content?: unknown })?.content;
259
-
260
- if (Array.isArray(content)) {
261
- const texts = content
262
- .filter((entry): entry is { type: string; text: string } => (entry as { type?: string })?.type === "text")
263
- .map((entry) => entry.text);
264
-
265
- if (texts.length > 0)
266
- return texts.join("\n");
267
- }
268
-
269
- return asJson(value);
270
- };
271
-
272
- export const turnDispatcherExtensionName = "ragents-turn-dispatcher";
273
-
274
157
  export type TurnAbortLatch = {
275
158
  request: () => Promise<void> | null;
276
159
  accept: () => void;
@@ -311,542 +194,20 @@ export const createTurnAbortLatch = (abort: () => Promise<void>): TurnAbortLatch
311
194
  };
312
195
  };
313
196
 
314
- class TurnDispatcher {
315
- readonly #context: AgentRuntimeContext;
316
- readonly #onDiagnostic: ((diagnostic: AgentRuntimeDiagnostic) => void) | undefined;
317
- #api: ExtensionAPI | null = null;
318
- #registeredNames = new Set<string>();
319
- #activeManagedNames = new Set<string>();
320
- readonly #directToolCalls = new Map<string, { name: string; input: unknown }>();
321
- readonly #unexecutedManagedCalls = new Map<string, { name: string; input: JsonValue }>();
322
- #turn: {
323
- request: TurnRequest<"agent">;
324
- signal: AbortSignal;
325
- usage: TurnUsage;
326
- failure: string | null;
327
- modelFailure: string | null;
328
- pendingText: string;
329
- } | null = null;
330
-
331
- constructor(context: AgentRuntimeContext, onDiagnostic: ((diagnostic: AgentRuntimeDiagnostic) => void) | undefined) {
332
- this.#context = context;
333
- this.#onDiagnostic = onDiagnostic;
334
- }
335
-
336
- extension(): InlineExtension {
337
- return {
338
- name: turnDispatcherExtensionName,
339
- factory: (agent) => {
340
- this.#api = agent;
341
- },
342
- };
343
- }
344
-
345
- initialize() {
346
- const api = this.#requiredApi();
347
- this.#registeredNames = new Set();
348
- api.setActiveTools([]);
349
-
350
- if (this.#turn)
351
- this.#configureTools(this.#turn.request);
352
- }
353
-
354
- unknownToolError(name: string): string | undefined {
355
- const turn = this.#turn;
356
- if (!turn || turn.signal.aborted)
357
- return undefined;
358
-
359
- return `Die Funktion ${name} ist kein natives Werkzeug. Suche ihren Vertrag mit typescript_api und rufe sie in typescript_eval über context.functions auf.`;
360
- }
361
-
362
- /** The running turn that may still take steering; null between turns and after an abort. */
363
- steerableTurn(): TurnRequest<"agent"> | null {
364
- const turn = this.#turn;
365
-
366
- return turn && !turn.signal.aborted ? turn.request : null;
367
- }
368
-
369
- activate(request: TurnRequest<"agent">, signal: AbortSignal) {
370
- if (this.#turn)
371
- throw new Error(`agent ${request.agentId} already has an active turn.`);
372
-
373
- this.#configureTools(request);
374
- this.#turn = { request, signal, usage: emptyUsage(), failure: null, modelFailure: null, pendingText: "" };
375
- }
376
-
377
- async refreshTools(): Promise<string | undefined> {
378
- const turn = this.#turn;
379
- if (!turn?.request.refreshTools) return undefined;
380
- turn.signal.throwIfAborted();
381
- const snapshot = await turn.request.refreshTools();
382
- turn.signal.throwIfAborted();
383
- if (this.#turn !== turn) throw new Error("The agent turn ended while refreshing tools.");
384
- Object.assign(turn.request, snapshot);
385
- this.#configureTools(turn.request);
386
- return snapshot.systemPrompt;
387
- }
388
-
389
- deactivate() {
390
- if (
391
- !this.#turn &&
392
- this.#activeManagedNames.size === 0 &&
393
- this.#directToolCalls.size === 0 &&
394
- this.#unexecutedManagedCalls.size === 0
395
- )
396
- return;
397
-
398
- const turn = this.#turn;
399
- this.#turn = null;
400
- this.#activeManagedNames.clear();
401
- this.#directToolCalls.clear();
402
- this.#unexecutedManagedCalls.clear();
403
-
404
- this.#api?.setActiveTools([]);
405
-
406
- if (turn?.signal.aborted)
407
- this.#recordInterruptedOutput(turn);
408
- }
409
-
410
- result(): { failure: string | null; usage: TurnUsage } {
411
- return this.#turn
412
- ? { failure: this.#turn.failure ?? this.#turn.modelFailure, usage: this.#turn.usage }
413
- : { failure: null, usage: emptyUsage() };
414
- }
415
-
416
- addUsage(usage: TurnUsage) {
417
- if (this.#turn)
418
- this.#turn.usage = addUsage(this.#turn.usage, usage);
419
- }
420
-
421
- #recordInterruptedOutput(turn = this.#turn) {
422
- if (!turn?.pendingText.trim())
423
- return;
424
-
425
- turn.request.emit({ kind: "assistant-interrupted", text: turn.pendingText });
426
- turn.pendingText = "";
427
- }
428
-
429
- handleExtensionError(error: ExtensionError) {
430
- const message = `agent extension ${error.extensionPath} failed during ${error.event}: ${error.error}`;
431
-
432
- if (this.#turn)
433
- this.#turn.failure ??= message;
434
- else
435
- this.#onDiagnostic?.({ type: "error", message, context: this.#context });
436
- }
437
-
438
- handleEvent(raw: unknown) {
439
- const turn = this.#turn;
440
-
441
- if (!turn)
442
- return;
443
-
444
- const event = raw as Record<string, unknown>;
445
-
446
- if (event.type === "message_update") {
447
- if (turn.signal.aborted)
448
- return;
449
-
450
- const delta = event.assistantMessageEvent as { type?: string; delta?: string } | undefined;
451
-
452
- if (delta?.type === "text_delta" && delta.delta) {
453
- turn.pendingText += delta.delta;
454
- turn.request.publish({ kind: "text", delta: delta.delta });
455
- } else if (delta?.type === "thinking_delta" && delta.delta)
456
- turn.request.publish({ kind: "thinking", delta: delta.delta });
457
-
458
- return;
459
- }
460
-
461
- if (event.type === "tool_execution_start") {
462
- const id = String(event.toolCallId);
463
- const name = String(event.toolName);
464
- turn.request.publish({
465
- kind: "tool",
466
- id,
467
- name,
468
- arguments: asJson(event.args),
469
- });
470
-
471
- if (this.#activeManagedNames.has(name)) {
472
- this.#unexecutedManagedCalls.set(id, { name, input: asJsonValue(event.args) });
473
- } else {
474
- assertJsonValue(event.args, `agent tool ${name} input`);
475
- this.#directToolCalls.set(id, { name, input: event.args });
476
- turn.request.recordTool?.({ kind: "started", id, name, input: event.args });
477
- }
478
-
479
- return;
480
- }
481
-
482
- if (event.type === "tool_execution_end") {
483
- const id = String(event.toolCallId);
484
- turn.request.publish({
485
- kind: "tool-result",
486
- id,
487
- result: asText(event.result),
488
- isError: event.isError === true,
489
- });
490
-
491
- const unexecuted = this.#unexecutedManagedCalls.get(id);
492
-
493
- if (unexecuted) {
494
- this.#unexecutedManagedCalls.delete(id);
495
-
496
- if (event.isError === true) {
497
- turn.request.recordTool?.({
498
- kind: "started",
499
- id,
500
- name: unexecuted.name,
501
- input: unexecuted.input,
502
- });
503
- turn.request.recordTool?.({
504
- kind: "failed",
505
- id,
506
- name: unexecuted.name,
507
- error: asText(event.result),
508
- });
509
- }
510
- }
511
-
512
- const direct = this.#directToolCalls.get(id);
513
-
514
- if (direct) {
515
- this.#directToolCalls.delete(id);
516
- const output = asText(event.result);
517
- turn.request.recordTool?.(event.isError === true
518
- ? { kind: "failed", id, name: direct.name, error: output }
519
- : { kind: "completed", id, name: direct.name, output });
520
- }
521
-
522
- return;
523
- }
524
-
525
- if (event.type !== "message_end")
526
- return;
527
-
528
- const message = event.message as {
529
- role?: string;
530
- usage?: AgentUsage;
531
- content?: Array<{ type: string; text?: string; thinking?: string }>;
532
- stopReason?: string;
533
- errorMessage?: string;
534
- };
535
-
536
- if (message.role !== "assistant")
537
- return;
538
-
539
- turn.usage = addUsage(turn.usage, usageOf(message.usage));
540
-
541
- if (turn.signal.aborted || message.stopReason === "aborted") {
542
- this.#recordInterruptedOutput(turn);
543
- turn.failure ??= "The agent turn was aborted.";
544
- return;
545
- }
546
-
547
- turn.pendingText = "";
548
- for (const entry of message.content ?? []) {
549
- if (entry.type === "text" && entry.text?.trim())
550
- turn.request.emit({ kind: "assistant", text: entry.text.trim() });
551
- else if (entry.type === "thinking" && entry.thinking?.trim())
552
- turn.request.emit({ kind: "reasoning", text: entry.thinking.trim() });
553
- }
554
-
555
- // The session retries or compacts after a provider error, so only the last answer decides.
556
- turn.modelFailure = message.stopReason === "error"
557
- ? message.errorMessage ?? "The model provider returned an error."
558
- : null;
559
- }
560
-
561
- #configureTools(request: TurnRequest<"agent">) {
562
- const api = this.#requiredApi();
563
- const requestedNames = new Set<string>();
564
- const allowedNames = request.allowedToolNames === null ? null : new Set(request.allowedToolNames);
565
-
566
- if (allowedNames && allowedNames.size !== request.allowedToolNames?.length)
567
- throw new Error(`agent ${request.agentId} received duplicate allowed tool names.`);
568
-
569
- if (allowedNames) {
570
- const outside = request.tools
571
- .map((tool) => tool.name)
572
- .filter((name) => !["typescript_api", "typescript_eval"].includes(name) && !allowedNames.has(name));
573
-
574
- if (outside.length > 0)
575
- throw new Error(`Der Agent erhielt Werkzeuge außerhalb seiner Freigabeliste: ${[...new Set(outside)].join(", ")}.`);
576
- }
577
-
578
- for (const entry of request.tools) {
579
- if (requestedNames.has(entry.name))
580
- throw new Error(`RAgents supplied duplicate agent tool ${entry.name}.`);
581
-
582
- requestedNames.add(entry.name);
583
- const existing = api.getAllTools().find((tool) => tool.name === entry.name);
584
-
585
- if (existing && !this.#registeredNames.has(entry.name))
586
- throw new Error(`RAgents tool ${entry.name} conflicts with agent extension tool ${existing.sourceInfo.path}.`);
587
-
588
- api.registerTool(
589
- defineTool({
590
- name: entry.name,
591
- label: entry.label,
592
- description: [entry.description, entry.longDescription].filter(Boolean).join("\n\n"),
593
- parameters: entry.schema,
594
- executionMode:
595
- entry.executionMode ??
596
- (entry.name.startsWith("agent_") ? "sequential" : "parallel"),
597
- execute: async (toolCallId: string, params: unknown, toolSignal: AbortSignal | undefined, _onUpdate: unknown, context: ExtensionContext) => {
598
- const current = this.#turn;
599
-
600
- if (!current)
601
- throw new Error(`agent tool ${entry.name} was called outside an RAgents turn.`);
602
-
603
- if (current.signal.aborted || toolSignal?.aborted)
604
- throw new Error(`agent tool ${entry.name} was aborted.`);
605
-
606
- assertJsonValue(params, `agent tool ${entry.name} input`);
607
- this.#unexecutedManagedCalls.delete(toolCallId);
608
- const call = await current.request.invoke(toolCallId, entry.name, params, modelContextOf(context.sessionManager.getBranch()));
609
- const text = typeof call.output === "string" ? call.output : asJson(call.output);
610
-
611
- return {
612
- content: [{
613
- type: "text" as const,
614
- text: call.ignoredFields.length === 0 ? text : `${ignoredFieldsNotice(entry, call.ignoredFields)}\n\n${text}`,
615
- }],
616
- details: {},
617
- };
618
- },
619
- }),
620
- );
621
- this.#registeredNames.add(entry.name);
622
- }
623
-
624
- this.#activeManagedNames = requestedNames;
625
- api.setActiveTools([...requestedNames]);
626
- }
627
-
628
- #requiredApi() {
629
- if (!this.#api)
630
- throw new Error(`agent extensions are not bound for agent ${this.#context.agentId}.`);
631
-
632
- return this.#api;
633
- }
634
- }
635
-
636
- type SessionIdentityRecord = {
637
- version: 1;
638
- runId: string;
639
- agentId: string;
640
- sessionId: string;
641
- file: string;
642
- };
643
-
644
- /** The source branch up to its last finished exchange, without unfinished tool calls and without reasoning. */
645
- export const forkableBranch = (branch: readonly SessionEntry[]): SessionEntry[] => {
646
- const answered = new Set(branch.flatMap((entry) =>
647
- entry.type === "message" && entry.message.role === "toolResult" ? [entry.message.toolCallId] : []));
648
- const unfinished = (entry: SessionEntry) =>
649
- entry.type === "message" && entry.message.role === "assistant"
650
- && entry.message.content.some((block) => block.type === "toolCall" && !answered.has(block.id));
651
- const end = branch.findIndex(unfinished);
652
- const kept = end === -1 ? branch : branch.slice(0, end);
653
-
654
- return kept.map((entry) => entry.type === "message" && entry.message.role === "assistant"
655
- ? { ...entry, message: { ...entry.message, content: entry.message.content.filter((block) => block.type !== "thinking") } }
656
- : entry);
197
+ type ManagedAgentRuntimeSetup = {
198
+ context: AgentRuntimeContext;
199
+ modelRuntime: ModelRuntime;
200
+ skills: readonly Skill[];
201
+ hooks: readonly AgentHook[];
202
+ options: AgentRuntimeManagerOptions;
203
+ turnAbortTimeoutMs: number;
657
204
  };
658
205
 
659
- class PersistedAgentSession {
660
- readonly #context: AgentRuntimeContext;
661
- readonly #store: AgentSessionStore | undefined;
662
- readonly #runtimeDirectory: string;
663
- #directory: string | null = null;
664
- #signature: string | null = null;
665
-
666
- constructor(context: AgentRuntimeContext, store: AgentSessionStore | undefined, runtimeDirectory: string) {
667
- this.#context = context;
668
- this.#store = store;
669
- this.#runtimeDirectory = runtimeDirectory;
670
- }
671
-
672
- async open(): Promise<SessionManager> {
673
- if (!this.#store)
674
- return SessionManager.inMemory(this.#runtimeDirectory);
675
-
676
- const configuredDirectory = this.#store.directory(this.#context.runId, this.#context.agentId);
677
-
678
- if (!configuredDirectory)
679
- return SessionManager.inMemory(this.#runtimeDirectory);
680
-
681
- this.#directory = resolve(configuredDirectory);
682
- await mkdir(this.#directory, { recursive: true, mode: this.#store.directoryMode });
683
- await chmod(this.#directory, this.#store.directoryMode);
684
- const stored = await this.#readMarker();
685
-
686
- if (stored) {
687
- const manager = await this.#openStored(stored);
688
- this.#signature = `${stored.sessionId}\u0000${resolve(this.#directory, stored.file)}`;
689
-
690
- return manager;
691
- }
692
-
693
- const manager = this.#context.forkOf ? await this.#forkedManager(this.#context.forkOf) : await this.#newManager();
694
- await this.persist(manager.getSessionFile(), manager.getSessionId());
695
-
696
- return manager;
697
- }
698
-
699
- async persist(file: string | undefined, sessionId: string) {
700
- if (!file || !this.#store)
701
- return;
702
-
703
- const resolvedFile = resolve(file);
704
-
705
- if (this.#directory && dirname(resolvedFile) !== this.#directory)
706
- throw new Error(`agent session ${resolvedFile} is outside its configured directory ${this.#directory}.`);
707
-
708
- const signature = `${sessionId}\u0000${resolvedFile}`;
709
-
710
- if (signature === this.#signature)
711
- return;
712
-
713
- if (this.#directory) {
714
- const marker = this.#markerPath();
715
- const temporary = `${marker}.${process.pid}.${randomUUID()}.tmp`;
716
- const record: SessionIdentityRecord = {
717
- version: 1,
718
- runId: this.#context.runId,
719
- agentId: this.#context.agentId,
720
- sessionId,
721
- file: basename(resolvedFile),
722
- };
723
- await writeDurably(temporary, `${JSON.stringify(record, null, 2)}\n`);
724
- await rename(temporary, marker);
725
- await syncDirectory(this.#directory);
726
- }
727
-
728
- this.#signature = signature;
729
- }
730
-
731
- async #newManager() {
732
- if (!this.#directory)
733
- return SessionManager.inMemory(this.#runtimeDirectory);
734
-
735
- const file = join(this.#directory, `ragents-${randomUUID()}.jsonl`);
736
- await writeDurably(file, "");
737
- await syncDirectory(this.#directory);
738
- const manager = SessionManager.open(file, this.#directory, this.#runtimeDirectory);
739
- syncFile(file);
740
-
741
- return manager;
742
- }
743
-
744
- async #forkedManager(sourceAgentId: string) {
745
- if (!this.#directory || !this.#store)
746
- throw new Error("Forking an agent session requires a session directory.");
747
-
748
- const sourceDirectory = this.#store.directory(this.#context.runId, sourceAgentId);
749
-
750
- if (!sourceDirectory)
751
- throw new Error(`Fork source ${sourceAgentId} has no persisted session directory.`);
752
-
753
- const record = await this.#readMarker(join(resolve(sourceDirectory), "active-session.json"), sourceAgentId);
754
-
755
- if (!record)
756
- throw new Error(`Fork source ${sourceAgentId} has no model context yet.`);
757
-
758
- const sourceFile = resolve(sourceDirectory, record.file);
759
- const source = SessionManager.open(sourceFile, resolve(sourceDirectory), this.#runtimeDirectory);
760
- const header = {
761
- type: "session",
762
- version: CURRENT_SESSION_VERSION,
763
- id: randomUUID(),
764
- timestamp: new Date().toISOString(),
765
- cwd: this.#runtimeDirectory,
766
- parentSession: sourceFile,
767
- };
768
- const lines = [header, ...forkableBranch(source.getBranch())].map((entry) => JSON.stringify(entry)).join("\n");
769
- const file = join(this.#directory, `ragents-${randomUUID()}.jsonl`);
770
- await writeDurably(file, `${lines}\n`);
771
- await syncDirectory(this.#directory);
772
- const manager = SessionManager.open(file, this.#directory, this.#runtimeDirectory);
773
- syncFile(file);
774
-
775
- return manager;
776
- }
777
-
778
- async #readMarker(marker = this.#markerPath(), agentId = this.#context.agentId): Promise<SessionIdentityRecord | null> {
779
- let content: string;
780
-
781
- try {
782
- content = await readFile(marker, "utf8");
783
- } catch (error) {
784
- if ((error as NodeJS.ErrnoException).code === "ENOENT")
785
- return null;
786
-
787
- throw error;
788
- }
789
-
790
- let candidate: unknown;
791
-
792
- try {
793
- candidate = JSON.parse(content);
794
- } catch (error) {
795
- throw new Error(`agent session identity marker is invalid: ${errorMessage(error)}`);
796
- }
797
-
798
- const record = candidate as Partial<SessionIdentityRecord>;
799
-
800
- if (
801
- record.version !== 1 ||
802
- record.runId !== this.#context.runId ||
803
- record.agentId !== agentId ||
804
- typeof record.sessionId !== "string" ||
805
- !record.sessionId ||
806
- typeof record.file !== "string" ||
807
- !record.file ||
808
- basename(record.file) !== record.file
809
- )
810
- throw new Error(`agent session identity marker for ${agentId} has invalid contents.`);
811
-
812
- return record as SessionIdentityRecord;
813
- }
814
-
815
- async #openStored(record: SessionIdentityRecord) {
816
- const directory = this.#directory;
817
-
818
- if (!directory)
819
- throw new Error("A agent session marker requires a session directory.");
820
-
821
- const file = resolve(directory, record.file);
822
- await access(file);
823
- const manager = SessionManager.open(file, directory, this.#runtimeDirectory);
824
-
825
- if (manager.getSessionId() !== record.sessionId)
826
- throw new Error(`agent session identity ${record.sessionId} does not match ${manager.getSessionId()} in ${file}.`);
827
-
828
- return manager;
829
- }
830
-
831
- #markerPath() {
832
- if (!this.#directory)
833
- throw new Error("A agent session marker requires a session directory.");
834
-
835
- return join(this.#directory, "active-session.json");
836
- }
837
- }
838
-
206
+ /** What an agent keeps between its turns: skills, hooks and the skill preload. Its conversation lives only in the journal. */
839
207
  class ManagedAgentRuntime {
840
- readonly #context: AgentRuntimeContext;
841
- readonly #runtime: AgentSessionRuntime;
842
- readonly #dispatcher: TurnDispatcher;
843
- readonly #identity: PersistedAgentSession;
844
- readonly #configuration: RuntimeConfiguration;
845
- readonly #modelRuntime: ModelRuntime;
846
- readonly #defaultThinking: ThinkingLevel | undefined;
847
- readonly #turnAbortTimeoutMs: number;
848
- readonly #onDiagnostic: ((diagnostic: AgentRuntimeDiagnostic) => void) | undefined;
849
- #unsubscribe: (() => void) | null = null;
208
+ readonly #setup: ManagedAgentRuntimeSetup;
209
+ readonly #preload: SkillPreload | null;
210
+ #turn: AgentTurn | null = null;
850
211
  #running = false;
851
212
  #disposed = false;
852
213
  #disposePromise: Promise<void> | null = null;
@@ -855,30 +216,17 @@ class ManagedAgentRuntime {
855
216
  #resolveIdle: (() => void) | null = null;
856
217
  readonly #inFlight = new Set<Promise<unknown>>();
857
218
 
858
- constructor(options: {
859
- context: AgentRuntimeContext;
860
- runtime: AgentSessionRuntime;
861
- dispatcher: TurnDispatcher;
862
- identity: PersistedAgentSession;
863
- configuration: RuntimeConfiguration;
864
- modelRuntime: ModelRuntime;
865
- defaultThinking: ThinkingLevel | undefined;
866
- turnAbortTimeoutMs: number;
867
- onDiagnostic: ((diagnostic: AgentRuntimeDiagnostic) => void) | undefined;
868
- }) {
869
- this.#context = options.context;
870
- this.#runtime = options.runtime;
871
- this.#dispatcher = options.dispatcher;
872
- this.#identity = options.identity;
873
- this.#configuration = options.configuration;
874
- this.#modelRuntime = options.modelRuntime;
875
- this.#defaultThinking = options.defaultThinking;
876
- this.#turnAbortTimeoutMs = options.turnAbortTimeoutMs;
877
- this.#onDiagnostic = options.onDiagnostic;
878
- }
879
-
880
- async start() {
881
- await this.#bind(this.#runtime.session);
219
+ constructor(setup: ManagedAgentRuntimeSetup) {
220
+ this.#setup = setup;
221
+ const { skills, options, context, modelRuntime } = setup;
222
+ this.#preload = skills.length > 0 && options.skillPreload !== false
223
+ ? createSkillPreload({
224
+ modelRuntime,
225
+ ...options.skillPreload,
226
+ onDiagnostic: (message) => options.onDiagnostic?.({ type: "warning", message, context }),
227
+ onUsage: (usage: TurnUsage) => this.#turn?.addUsage(usage),
228
+ })
229
+ : null;
882
230
  }
883
231
 
884
232
  async runTurn(request: TurnRequest<"agent">, signal: AbortSignal): Promise<TurnResult> {
@@ -888,99 +236,102 @@ class ManagedAgentRuntime {
888
236
  if (this.#running)
889
237
  throw new Error(`agent runtime for ${request.agentId} cannot run concurrent turns.`);
890
238
 
891
- const promptChanged = request.systemPrompt !== this.#configuration.systemPrompt;
892
239
  const stopController = new AbortController();
893
240
  const turnSignal = AbortSignal.any([signal, stopController.signal]);
894
241
  this.#running = true;
895
242
  this.#idle = new Promise<void>((resolveIdle) => {
896
243
  this.#resolveIdle = resolveIdle;
897
244
  });
898
- this.#configuration.selection = request.selection;
899
- const abortLatch = createTurnAbortLatch(() => this.#runtime.session.abort());
900
- const abortDeadline = new TurnAbortDeadline(turnSignal, this.#turnAbortTimeoutMs);
245
+ const abortDeadline = new TurnAbortDeadline(turnSignal, this.#setup.turnAbortTimeoutMs);
246
+ const abortLatch = createTurnAbortLatch(() => this.#turn?.abort() ?? Promise.resolve());
901
247
  const abort = () => void abortLatch.request();
902
248
  const abortTurn = () => {
903
249
  stopController.abort(new Error(`agent runtime ${request.runId}/${request.agentId} is stopping.`));
904
250
 
905
251
  return abortLatch.request();
906
252
  };
253
+ let turn: AgentTurn | null = null;
907
254
  this.#abortActiveTurn = abortTurn;
908
-
909
255
  turnSignal.addEventListener("abort", abort, { once: true });
910
256
 
911
257
  try {
912
258
  if (turnSignal.aborted) {
913
259
  abortLatch.request();
914
260
 
915
- return this.#abortedResult();
916
- }
917
-
918
- if (promptChanged) {
919
- this.#configuration.systemPrompt = request.systemPrompt;
920
- this.#runtime.session.setSystemPrompt(request.systemPrompt);
261
+ return { failure: "The agent turn was aborted.", usage: emptyUsage() };
921
262
  }
922
263
 
923
- await abortDeadline.wait(this.#track(this.#selectModel(request)));
924
-
925
- if (turnSignal.aborted) {
926
- return this.#abortedResult();
927
- }
928
-
929
- this.#dispatcher.activate(request, turnSignal);
930
- await abortDeadline.wait(this.#track(this.#refreshTools()));
931
-
932
- const prepared = await prepareInputAttachments(request, this.#runtime.session.model?.input ?? []);
933
-
934
- if (turnSignal.aborted) {
935
- abortLatch.request();
936
- } else {
937
- await abortDeadline.wait(this.#track(this.#runtime.session.prompt(prepared.prompt, {
938
- attachments: prepared.attachments,
939
- preflightResult: (accepted) => {
940
- if (!accepted)
941
- return;
942
-
943
- abortLatch.accept();
944
- queueMicrotask(() => void abortLatch.reapply());
945
- },
946
- })));
947
- }
264
+ turn = this.#turnFor(request, turnSignal);
265
+ this.#turn = turn;
266
+ await abortDeadline.wait(this.#track(turn.run(() => {
267
+ abortLatch.accept();
268
+ queueMicrotask(() => void abortLatch.reapply());
269
+ })));
948
270
 
949
271
  const abortPromise = abortLatch.pending();
950
272
 
951
273
  if (abortPromise)
952
274
  await abortDeadline.wait(this.#track(abortPromise));
953
275
 
954
- return turnSignal.aborted
955
- ? this.#abortedResult()
956
- : this.#dispatcher.result();
276
+ return turnSignal.aborted ? turn.abortedResult() : turn.result();
957
277
  } catch (error) {
958
- const result = this.#dispatcher.result();
278
+ if (!turn)
279
+ return { failure: turnSignal.aborted ? "The agent turn was aborted." : errorMessage(error), usage: emptyUsage() };
280
+
281
+ const result = turn.result();
959
282
 
960
283
  return turnSignal.aborted
961
- ? this.#abortedResult()
284
+ ? turn.abortedResult()
962
285
  : { failure: result.failure ?? errorMessage(error), usage: result.usage };
963
286
  } finally {
964
- try {
965
- turnSignal.removeEventListener("abort", abort);
966
- this.#dispatcher.deactivate();
967
- if (this.#abortActiveTurn === abortTurn)
968
- this.#abortActiveTurn = null;
969
-
970
- await abortDeadline.wait(
971
- this.#track(
972
- this.#identity.persist(this.#runtime.session.sessionFile, this.#runtime.session.sessionId),
973
- ),
974
- );
975
- } finally {
976
- abortDeadline.dispose();
977
- this.#running = false;
978
- this.#resolveIdle?.();
979
- this.#resolveIdle = null;
980
- }
287
+ turnSignal.removeEventListener("abort", abort);
288
+ turn?.deactivate();
289
+
290
+ if (this.#turn === turn)
291
+ this.#turn = null;
292
+
293
+ if (this.#abortActiveTurn === abortTurn)
294
+ this.#abortActiveTurn = null;
295
+
296
+ abortDeadline.dispose();
297
+ this.#running = false;
298
+ this.#resolveIdle?.();
299
+ this.#resolveIdle = null;
981
300
  }
982
301
  }
983
302
 
303
+ #turnFor(request: TurnRequest<"agent">, signal: AbortSignal): AgentTurn {
304
+ const { modelRuntime, options, context } = this.#setup;
305
+ const { provider, model: modelId, thinking } = request.selection;
306
+ const model = modelRuntime.getModel(provider, modelId);
307
+
308
+ if (!model)
309
+ throw new Error(`The model ${provider}/${modelId} is not configured.`);
310
+
311
+ const level = thinking ?? options.thinkingLevel;
312
+
313
+ if (level) {
314
+ const available = getSupportedThinkingLevels(model);
315
+
316
+ if (!available.includes(level))
317
+ throw new Error(`The thinking level ${level} does not exist for ${modelId}; valid: ${available.join(", ")}.`);
318
+ }
319
+
320
+ return new AgentTurn({
321
+ request,
322
+ signal,
323
+ modelRuntime,
324
+ model,
325
+ thinkingLevel: (level ?? clampThinkingLevel(model, "medium")) as LoopThinkingLevel,
326
+ skills: this.#setup.skills,
327
+ hooks: this.#setup.hooks,
328
+ preload: this.#preload,
329
+ settings: agentSettings(options.settings),
330
+ track: (operation) => this.#track(operation),
331
+ onDiagnostic: (message) => options.onDiagnostic?.({ type: "warning", message, context }),
332
+ });
333
+ }
334
+
984
335
  dispose(): Promise<void> {
985
336
  if (this.#disposed)
986
337
  return Promise.resolve();
@@ -1006,22 +357,12 @@ class ManagedAgentRuntime {
1006
357
  if (this.#running)
1007
358
  this.#abortActiveTurn?.();
1008
359
 
1009
- this.#unsubscribe?.();
1010
- this.#unsubscribe = null;
1011
- this.#dispatcher.deactivate();
1012
- const active = await Promise.allSettled([this.#runtime.dispose(), idle]);
1013
- const persistence = await Promise.allSettled([
1014
- this.#identity.persist(this.#runtime.session.sessionFile, this.#runtime.session.sessionId),
1015
- ]);
1016
-
1017
- for (const result of [...active, ...persistence]) {
1018
- if (result.status === "rejected")
1019
- throw result.reason;
1020
- }
360
+ this.#turn?.deactivate();
361
+ await idle;
1021
362
  }
1022
363
 
1023
364
  waitForSettlement(): Promise<void> {
1024
- return Promise.allSettled([...this.#inFlight, this.#runtime.waitForSettlement()]).then(() => undefined);
365
+ return Promise.allSettled([...this.#inFlight]).then(() => undefined);
1025
366
  }
1026
367
 
1027
368
  #track<T>(operation: PromiseLike<T> | T): Promise<T> {
@@ -1034,92 +375,6 @@ class ManagedAgentRuntime {
1034
375
 
1035
376
  return pending;
1036
377
  }
1037
-
1038
- #abortedResult(): TurnResult {
1039
- const result = this.#dispatcher.result();
1040
-
1041
- return {
1042
- failure: result.failure ?? "The agent turn was aborted.",
1043
- usage: result.usage,
1044
- };
1045
- }
1046
-
1047
- async #bind(session: AgentSession) {
1048
- const bindingErrors: ExtensionError[] = [];
1049
- let initializing = true;
1050
- await session.bindExtensions({
1051
- onError: (error) => {
1052
- if (initializing)
1053
- bindingErrors.push(error);
1054
- else
1055
- this.#dispatcher.handleExtensionError(error);
1056
- },
1057
- });
1058
- initializing = false;
1059
-
1060
- if (bindingErrors.length > 0)
1061
- throw new Error(bindingErrors.map((entry) => `${entry.extensionPath}: ${entry.error}`).join("; "));
1062
-
1063
- this.#dispatcher.initialize();
1064
- const prepareNextTurn = session.agent.prepareNextTurnWithContext;
1065
- session.agent.prepareNextTurnWithContext = async (turn, signal) => {
1066
- await this.#refreshTools();
1067
- return prepareNextTurn?.(turn, signal);
1068
- };
1069
- session.agent.formatUnknownToolError = (name) => this.#dispatcher.unknownToolError(name);
1070
- session.agent.steeringSource = () => this.#steering();
1071
- this.#unsubscribe = session.subscribe((event) => this.#dispatcher.handleEvent(event));
1072
- await this.#identity.persist(session.sessionFile, session.sessionId);
1073
- }
1074
-
1075
- async #steering(): Promise<UserMessage[]> {
1076
- const request = this.#dispatcher.steerableTurn();
1077
-
1078
- if (!request)
1079
- return [];
1080
-
1081
- const messages: UserMessage[] = [];
1082
-
1083
- for (const steered of request.claimSteering()) {
1084
- const prepared = await prepareInputAttachments(
1085
- { ...request, attachments: steered.attachments, prompt: steered.prompt },
1086
- this.#runtime.session.model?.input ?? [],
1087
- );
1088
- messages.push({ role: "user", content: [{ type: "text", text: prepared.prompt }, ...prepared.attachments], timestamp: Date.now() });
1089
- }
1090
-
1091
- return messages;
1092
- }
1093
-
1094
- async #refreshTools(): Promise<void> {
1095
- const systemPrompt = await this.#dispatcher.refreshTools();
1096
- if (systemPrompt !== undefined && systemPrompt !== this.#configuration.systemPrompt) {
1097
- this.#configuration.systemPrompt = systemPrompt;
1098
- this.#runtime.session.setSystemPrompt(systemPrompt);
1099
- }
1100
- }
1101
-
1102
- async #selectModel(request: TurnRequest<"agent">) {
1103
- const { provider, model: modelId, thinking } = request.selection;
1104
- const model = this.#modelRuntime.getModel(provider, modelId);
1105
-
1106
- if (!model)
1107
- throw new Error(`Das Modell ${provider}/${modelId} ist nicht konfiguriert.`);
1108
-
1109
- if (this.#runtime.session.model?.provider !== provider || this.#runtime.session.model.id !== modelId)
1110
- await this.#runtime.session.setModel(model);
1111
-
1112
- const level = thinking ?? this.#defaultThinking;
1113
-
1114
- if (!level)
1115
- return;
1116
-
1117
- const available = this.#runtime.session.getAvailableThinkingLevels();
1118
- if (!available.includes(level))
1119
- throw new Error(`Die Denktiefe ${level} gibt es für ${modelId} nicht; gültig: ${available.join(", ")}.`);
1120
-
1121
- this.#runtime.session.setThinkingLevel(level);
1122
- }
1123
378
  }
1124
379
 
1125
380
  type RuntimeRecord = {
@@ -1159,7 +414,6 @@ export class AgentRuntimeManager {
1159
414
  agentId: request.agentId,
1160
415
  workspace: request.workspace,
1161
416
  allowedToolNames: request.allowedToolNames === null ? null : [...request.allowedToolNames],
1162
- forkOf: request.forkOf,
1163
417
  };
1164
418
  let record = this.#runtimes.get(key);
1165
419
 
@@ -1172,7 +426,7 @@ export class AgentRuntimeManager {
1172
426
  if (!record) {
1173
427
  const creationAbort = new AbortController();
1174
428
  const creationSignal = AbortSignal.any([signal, creationAbort.signal]);
1175
- const runtime = this.#create(context, request, creationSignal);
429
+ const runtime = this.#create(context, creationSignal);
1176
430
  record = { context, runtime, creationAbort };
1177
431
  this.#runtimes.set(key, record);
1178
432
  void runtime.catch(() => {
@@ -1291,103 +545,34 @@ export class AgentRuntimeManager {
1291
545
  }
1292
546
  }
1293
547
 
1294
- async #create(context: AgentRuntimeContext, request: TurnRequest<"agent">, signal: AbortSignal) {
548
+ async #create(context: AgentRuntimeContext, signal: AbortSignal) {
1295
549
  const modelRuntime = await awaitWithSignal(this.#modelRuntime, signal);
1296
- const runtimeDirectory = resolve(await awaitWithSignal(request.runtimeDirectory(), signal));
1297
- const identity = new PersistedAgentSession(context, this.#options.sessions, runtimeDirectory);
1298
- const sessionManager = await awaitWithSignal(identity.open(), signal);
1299
- const dispatcher = new TurnDispatcher(context, this.#options.onDiagnostic);
1300
- const managedEnvironment = request.allowedToolNames === null;
1301
- const loadHostFactories = managedEnvironment || (request.allowedToolNames?.length ?? 0) > 0;
1302
- const resolvedFactories = loadHostFactories && this.#options.resolveExtensionFactories
1303
- ? await awaitWithSignal(this.#options.resolveExtensionFactories(context, signal), signal)
1304
- : undefined;
550
+ const managedEnvironment = context.allowedToolNames === null;
551
+ const loadHostHooks = managedEnvironment || (context.allowedToolNames?.length ?? 0) > 0;
552
+ const hooks = loadHostHooks && this.#options.resolveHooks
553
+ ? await this.#settled(context.runId, this.#options.resolveHooks(context, signal), signal)
554
+ : [];
1305
555
  const skills = managedEnvironment && this.#options.resolveSkills
1306
- ? await awaitWithSignal(this.#options.resolveSkills(context, signal), signal)
556
+ ? await this.#settled(context.runId, this.#options.resolveSkills(context, signal), signal)
1307
557
  : [];
1308
558
  assertUniqueSkillNames(skills);
1309
- const skillPreload = skills.length > 0 && this.#options.skillPreload !== false
1310
- ? createSkillPreloadExtension({
1311
- modelRuntime,
1312
- ...this.#options.skillPreload,
1313
- onDiagnostic: (message) => this.#options.onDiagnostic?.({ type: "warning", message, context }),
1314
- onUsage: (usage) => dispatcher.addUsage(usage),
1315
- })
1316
- : null;
1317
- const extensionFactories = [
1318
- ...(loadHostFactories ? this.#options.extensionFactories ?? [] : []),
1319
- ...(resolvedFactories ?? []),
1320
- ...(skillPreload ? [skillPreload] : []),
1321
- dispatcher.extension(),
1322
- ];
1323
- const configuration: RuntimeConfiguration = {
1324
- selection: request.selection,
1325
- systemPrompt: request.systemPrompt,
1326
- };
1327
- const factory: CreateAgentSessionRuntimeFactory = async (runtimeOptions) => {
1328
- const services = await createAgentSessionServices({
1329
- cwd: runtimeOptions.cwd,
1330
- signal: runtimeOptions.signal,
1331
- ...(runtimeOptions.onExtensionFactorySettlement
1332
- ? { onExtensionFactorySettlement: runtimeOptions.onExtensionFactorySettlement }
1333
- : {}),
1334
- modelRuntime,
1335
- resourceLoaderOptions: {
1336
- skills,
1337
- extensionFactories,
1338
- systemPrompt: configuration.systemPrompt,
1339
- },
1340
- });
1341
- const extensionErrors = services.resourceLoader.getExtensions().errors;
1342
-
1343
- if (extensionErrors.length > 0)
1344
- throw new Error(extensionErrors.map((entry) => `${entry.path}: ${entry.error}`).join("; "));
1345
-
1346
- const { provider, model: modelId, thinking } = configuration.selection;
1347
- const model = modelRuntime.getModel(provider, modelId);
1348
-
1349
- if (!model)
1350
- throw new Error(`Das Modell ${provider}/${modelId} ist nicht konfiguriert.`);
1351
559
 
1352
- runtimeOptions.signal.throwIfAborted();
1353
-
1354
- const created = await createAgentSessionFromServices({
1355
- services,
1356
- sessionManager: runtimeOptions.sessionManager,
1357
- model,
1358
- ...(thinking ?? this.#options.thinkingLevel
1359
- ? { thinkingLevel: thinking ?? this.#options.thinkingLevel }
1360
- : {}),
1361
- });
1362
-
1363
- return { ...created, services };
1364
- };
1365
- const runtime = await createAgentSessionRuntime(factory, {
1366
- cwd: runtimeDirectory,
1367
- sessionManager,
1368
- signal,
1369
- onExtensionFactorySettlement: (settlement) =>
1370
- this.#rememberRunSettlement(context.runId, settlement),
1371
- });
1372
- const managed = new ManagedAgentRuntime({
560
+ return new ManagedAgentRuntime({
1373
561
  context,
1374
- runtime,
1375
- dispatcher,
1376
- identity,
1377
- configuration,
1378
562
  modelRuntime,
1379
- defaultThinking: this.#options.thinkingLevel,
563
+ skills,
564
+ hooks,
565
+ options: this.#options,
1380
566
  turnAbortTimeoutMs: this.#turnAbortTimeoutMs,
1381
- onDiagnostic: this.#options.onDiagnostic,
1382
567
  });
568
+ }
1383
569
 
1384
- try {
1385
- await awaitWithSignal(managed.start(), signal);
1386
- return managed;
1387
- } catch (error) {
1388
- await managed.dispose().catch(() => undefined);
1389
- throw error;
1390
- }
570
+ /** Waits for a resolution under the creation signal; a halt of the run still waits for its real end. */
571
+ #settled<T>(runId: string, operation: PromiseLike<T> | T, signal: AbortSignal): Promise<T> {
572
+ const pending = Promise.resolve(operation);
573
+ this.#rememberRunSettlement(runId, pending.then(() => undefined, () => undefined));
574
+
575
+ return awaitWithSignal(pending, signal);
1391
576
  }
1392
577
 
1393
578
  async #disposeRecord(key: string, record: RuntimeRecord) {