@schlenkr/ragents 0.1.6 → 0.1.8

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 (150) 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/plugin-support/plugin-folder.ts +2 -1
  7. package/apps/server/src/provider.ts +34 -11
  8. package/apps/server/src/ragents/actor-chat-history.ts +1 -0
  9. package/apps/server/src/ragents/engine.ts +23 -24
  10. package/apps/server/src/ragents/journal-chat-events.ts +2 -0
  11. package/apps/server/src/ragents/run-owner.ts +3 -2
  12. package/apps/server/src/ragents/session.ts +3 -3
  13. package/apps/server/src/run-preparation.ts +14 -18
  14. package/apps/server/src/run-transfer.ts +65 -4
  15. package/apps/server/src/settings.ts +2 -4
  16. package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-B9qYBARS.js → highlighted-body-KPVGNVTW-xXXhTDbh.js} +1 -1
  17. package/apps/web/dist/assets/{main-BCjvD2Hh.js → main-C7JeZFjs.js} +1 -1
  18. package/apps/web/dist/assets/{mermaid-HWGCJPDP-CvYYFK_n.js → mermaid-HWGCJPDP-DfsU5ofC.js} +69 -69
  19. package/apps/web/dist/assets/{runPanel-CG7kRMJh.js → runPanel-ZT6TPDLM.js} +1 -1
  20. package/apps/web/dist/help/guide-clients.html +13 -4
  21. package/apps/web/dist/help/guide-clients.md +14 -4
  22. package/apps/web/dist/help/guide-distributed.html +8 -3
  23. package/apps/web/dist/help/guide-distributed.md +8 -3
  24. package/apps/web/dist/help/guide-runtime.html +18 -12
  25. package/apps/web/dist/help/guide-runtime.md +19 -12
  26. package/apps/web/dist/help/mini-app.css +23 -3
  27. package/apps/web/dist/host-web.json +4 -4
  28. package/apps/web/dist/index.html +2 -2
  29. package/apps/web/dist/run-panel.html +2 -2
  30. package/apps/web/src/SessionList.tsx +16 -8
  31. package/apps/web/src/StartTiles.tsx +1 -1
  32. package/apps/web/src/api.ts +2 -0
  33. package/apps/web/src/panel/PanelPage.tsx +1 -1
  34. package/apps/web/src/panel/RunLine.tsx +4 -2
  35. package/apps/web/src/panel/contract.ts +2 -0
  36. package/bundles/ragents.activity/ragents-bundle.json +1 -1
  37. package/bundles/ragents.actor-programs/ragents-bundle.json +1 -1
  38. package/bundles/ragents.ask/ragents-bundle.json +1 -1
  39. package/bundles/ragents.browser/ragents-bundle.json +1 -1
  40. package/bundles/ragents.documents/ragents-bundle.json +1 -1
  41. package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
  42. package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
  43. package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
  44. package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
  45. package/bundles/ragents.orchestration/ragents-bundle.json +1 -1
  46. package/bundles/ragents.overseer/ragents-bundle.json +1 -1
  47. package/bundles/ragents.processes/ragents-bundle.json +1 -1
  48. package/bundles/ragents.product/ragents-bundle.json +1 -1
  49. package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
  50. package/bundles/ragents.reference/ragents-bundle.json +2 -2
  51. package/bundles/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
  52. package/bundles/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
  53. package/bundles/ragents.todo/ragents-bundle.json +1 -1
  54. package/bundles/ragents.transcript/ragents-bundle.json +1 -1
  55. package/bundles/ragents.watch/ragents-bundle.json +1 -1
  56. package/bundles/ragents.workspace/ragents-bundle.json +1 -1
  57. package/package.json +2 -2
  58. package/packages/agent/dist/core/agent-settings.d.ts +2 -2
  59. package/packages/agent/dist/core/compaction/compaction.d.ts +16 -50
  60. package/packages/agent/dist/core/context-log.d.ts +25 -0
  61. package/packages/agent/dist/core/messages.d.ts +3 -10
  62. package/packages/agent/dist/core/tool-definition.d.ts +17 -0
  63. package/packages/agent/dist/core/tools/bash.d.ts +2 -7
  64. package/packages/agent/dist/core/tools/edit.d.ts +1 -1
  65. package/packages/agent/dist/core/tools/read.d.ts +1 -1
  66. package/packages/agent/dist/core/tools/write.d.ts +1 -1
  67. package/packages/agent/dist/index.d.ts +7 -8
  68. package/packages/agent/dist/loop/agent.d.ts +2 -6
  69. package/packages/agent/package.json +1 -1
  70. package/packages/agent/src/core/agent-settings.ts +2 -2
  71. package/packages/agent/src/core/compaction/compaction.ts +49 -175
  72. package/packages/agent/src/core/context-log.ts +49 -0
  73. package/packages/agent/src/core/messages.ts +5 -11
  74. package/packages/agent/src/core/tool-definition.ts +29 -0
  75. package/packages/agent/src/core/tools/bash.ts +2 -9
  76. package/packages/agent/src/core/tools/edit.ts +2 -11
  77. package/packages/agent/src/core/tools/read.ts +3 -18
  78. package/packages/agent/src/core/tools/write.ts +1 -5
  79. package/packages/agent/src/index.ts +28 -14
  80. package/packages/agent/src/loop/agent-loop.ts +1 -2
  81. package/packages/agent/src/loop/agent.ts +8 -2
  82. package/packages/ai/dist/api/ai-sdk.d.ts +2 -0
  83. package/packages/ai/dist/types.d.ts +2 -0
  84. package/packages/ai/src/api/ai-sdk-messages.ts +15 -10
  85. package/packages/ai/src/api/ai-sdk.ts +11 -0
  86. package/packages/ai/src/api/transform-messages.ts +1 -1
  87. package/packages/ai/src/types.ts +2 -0
  88. package/packages/ai/src/utils/retry.ts +14 -0
  89. package/packages/ragents/src/agents/actor-input.ts +15 -0
  90. package/packages/ragents/src/agents/model-context.ts +360 -0
  91. package/packages/ragents/src/agents/plugins.ts +5 -0
  92. package/packages/ragents/src/agents/scheduler.ts +3 -7
  93. package/packages/ragents/src/agents/tools.ts +2 -2
  94. package/packages/ragents/src/agents/toolset.ts +47 -20
  95. package/packages/ragents/src/agents/workspaces.ts +0 -6
  96. package/packages/ragents/src/domain/event-semantics.ts +51 -0
  97. package/packages/ragents/src/domain/event-validation.ts +166 -0
  98. package/packages/ragents/src/domain/events.ts +72 -0
  99. package/packages/ragents/src/domain/model.ts +4 -2
  100. package/packages/ragents/src/domain/projection.ts +9 -0
  101. package/packages/ragents/src/domain/tool-result-text.ts +9 -0
  102. package/packages/ragents/src/drivers/agent-hooks.ts +25 -38
  103. package/packages/ragents/src/drivers/agent-runtime.ts +118 -933
  104. package/packages/ragents/src/drivers/agent-turn.ts +735 -0
  105. package/packages/ragents/src/drivers/agent.ts +9 -7
  106. package/packages/ragents/src/drivers/skill-preload.ts +53 -63
  107. package/packages/ragents/src/drivers/types.ts +21 -5
  108. package/packages/ragents/src/index.ts +8 -6
  109. package/packages/ragents/src/plugin-host.ts +60 -13
  110. package/packages/ragents/src/plugin-types.ts +7 -2
  111. package/packages/ragents/src/runtime/decisions/actors.ts +10 -0
  112. package/packages/ragents/src/runtime/decisions/turns.ts +66 -7
  113. package/packages/ragents/src/runtime/journal-storage.ts +7 -4
  114. package/packages/ragents/src/runtime/journal.ts +35 -0
  115. package/packages/ragents/src/runtime/orchestration.ts +140 -9
  116. package/packages/workspace-executor/src/sandbox-tools.ts +10 -11
  117. package/plugins/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
  118. package/plugins/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
  119. package/scripts/agent/journal.ts +5 -3
  120. package/scripts/package/publish-package.ts +4 -3
  121. package/packages/agent/dist/core/agent-session-runtime.d.ts +0 -33
  122. package/packages/agent/dist/core/agent-session-services.d.ts +0 -35
  123. package/packages/agent/dist/core/agent-session.d.ts +0 -234
  124. package/packages/agent/dist/core/auth-guidance.d.ts +0 -4
  125. package/packages/agent/dist/core/defaults.d.ts +0 -3
  126. package/packages/agent/dist/core/extensions/index.d.ts +0 -11
  127. package/packages/agent/dist/core/extensions/loader.d.ts +0 -12
  128. package/packages/agent/dist/core/extensions/runner.d.ts +0 -39
  129. package/packages/agent/dist/core/extensions/types.d.ts +0 -156
  130. package/packages/agent/dist/core/extensions/wrapper.d.ts +0 -12
  131. package/packages/agent/dist/core/resource-loader.d.ts +0 -39
  132. package/packages/agent/dist/core/sdk.d.ts +0 -38
  133. package/packages/agent/dist/core/session-manager.d.ts +0 -189
  134. package/packages/agent/dist/core/source-info.d.ts +0 -16
  135. package/packages/agent/dist/core/system-prompt.d.ts +0 -21
  136. package/packages/agent/src/core/agent-session-runtime.ts +0 -94
  137. package/packages/agent/src/core/agent-session-services.ts +0 -74
  138. package/packages/agent/src/core/agent-session.ts +0 -1108
  139. package/packages/agent/src/core/auth-guidance.ts +0 -14
  140. package/packages/agent/src/core/defaults.ts +0 -3
  141. package/packages/agent/src/core/extensions/index.ts +0 -38
  142. package/packages/agent/src/core/extensions/loader.ts +0 -125
  143. package/packages/agent/src/core/extensions/runner.ts +0 -211
  144. package/packages/agent/src/core/extensions/types.ts +0 -197
  145. package/packages/agent/src/core/extensions/wrapper.ts +0 -24
  146. package/packages/agent/src/core/resource-loader.ts +0 -142
  147. package/packages/agent/src/core/sdk.ts +0 -123
  148. package/packages/agent/src/core/session-manager.ts +0 -743
  149. package/packages/agent/src/core/source-info.ts +0 -28
  150. package/packages/agent/src/core/system-prompt.ts +0 -108
@@ -10,12 +10,13 @@ export type {
10
10
  AgentRuntimeContext,
11
11
  AgentRuntimeDiagnostic,
12
12
  AgentRuntimeManagerOptions,
13
- AgentSessionStore,
14
13
  } from "./agent-runtime.ts";
15
- export { AgentRuntimeManager, turnDispatcherExtensionName } from "./agent-runtime.ts";
14
+ export { AgentRuntimeManager } from "./agent-runtime.ts";
15
+ export { agentHookOf, type AgentHook } from "./agent-hooks.ts";
16
16
  export {
17
- createSkillPreloadExtension,
18
- skillPreloadExtensionName,
17
+ createSkillPreload,
18
+ skillPreloadHookName,
19
+ type SkillPreload,
19
20
  type SkillPreloadConfiguration,
20
21
  type SkillPreloadOptions,
21
22
  type SkillCatalogEntry,
@@ -23,17 +24,18 @@ export {
23
24
  type SkillSelector,
24
25
  } from "./skill-preload.ts";
25
26
 
26
- export type AgentSessionDriverOptions = Omit<AgentRuntimeManagerOptions, "modelRuntime"> & {
27
+ export type AgentLoopDriverOptions = Omit<AgentRuntimeManagerOptions, "modelRuntime"> & {
27
28
  modelRuntime?: ModelRuntime | Promise<ModelRuntime>;
28
29
  };
29
30
 
30
- export class AgentSessionDriver implements AgentDriver<"agent"> {
31
+ /** The driver of LLM actors: the agent loop on the model context the journal holds. */
32
+ export class AgentLoopDriver implements AgentDriver<"agent"> {
31
33
  readonly kind = "agent" as const;
32
34
  readonly supportsPlainLlm = true;
33
35
  readonly #modelRuntime: Promise<ModelRuntime>;
34
36
  readonly #runtimeManager: AgentRuntimeManager;
35
37
 
36
- constructor(options: AgentSessionDriverOptions = {}) {
38
+ constructor(options: AgentLoopDriverOptions = {}) {
37
39
  this.#modelRuntime = Promise.resolve(options.modelRuntime ?? ModelRuntime.create());
38
40
  this.#runtimeManager = new AgentRuntimeManager({ ...options, modelRuntime: this.#modelRuntime });
39
41
  }
@@ -1,10 +1,10 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
 
3
- import { stripFrontmatter, type InlineExtension, type ModelRuntime, type Skill } from "@ragents/agent";
3
+ import { stripFrontmatter, type ModelRuntime, type Skill } from "@ragents/agent";
4
4
 
5
5
  import type { TurnUsage } from "../domain/model.ts";
6
6
 
7
- export const skillPreloadExtensionName = "ragents-skill-preload";
7
+ export const skillPreloadHookName = "ragents-skill-preload";
8
8
 
9
9
  export type SkillCatalogEntry = {
10
10
  name: string;
@@ -130,64 +130,54 @@ const preloadedSkills = async (skills: readonly Skill[]): Promise<string> => {
130
130
  ].join("\n\n");
131
131
  };
132
132
 
133
- export const createSkillPreloadExtension = (options: SkillPreloadOptions): InlineExtension => ({
134
- name: skillPreloadExtensionName,
135
- factory: (agent) => {
136
- agent.on("before_agent_start", async (event, context) => {
137
- try {
138
- const skills = event.systemPromptOptions.skills ?? [];
139
-
140
- if (skills.length === 0)
141
- return;
142
-
143
- const explicit = explicitSkillNames(event.prompt, skills);
144
- let names: readonly string[] | null = explicit.length > 0 ? explicit : null;
145
- let candidates = skills;
146
-
147
- if (explicit.length === 0) {
148
- candidates = skills.filter((skill) => !skill.disableModelInvocation);
149
- const catalog = candidates.map(({ name, description }) => ({ name, description }));
150
-
151
- if (catalog.length === 0)
152
- return;
153
-
154
- const selector = options.selector ?? (context.model
155
- ? agentSelector(
156
- options.modelRuntime,
157
- context.model,
158
- options.maxSelectionTokens ?? 100,
159
- options.onUsage,
160
- )
161
- : null);
162
-
163
- if (!selector)
164
- return;
165
-
166
- names = await selector({
167
- prompt: event.prompt,
168
- skills: catalog,
169
- signal: context.signal,
170
- });
171
- }
172
-
173
- if (!names || names.length === 0)
174
- return;
175
-
176
- const selected = names
177
- .map((name) => candidates.find((skill) => skill.name === name))
178
- .filter((skill): skill is Skill => skill !== undefined);
179
-
180
- if (selected.length === 0)
181
- return;
182
-
183
- return {
184
- systemPrompt: `${event.systemPrompt}\n\n${await preloadedSkills(selected)}`,
185
- };
186
- } catch (error) {
187
- options.onDiagnostic?.(
188
- `Agent skill preloading fell back to the skill catalog: ${error instanceof Error ? error.message : String(error)}`,
189
- );
190
- }
191
- });
192
- },
193
- });
133
+ export type SkillPreloadRequest = {
134
+ prompt: string;
135
+ skills: readonly Skill[];
136
+ model: ReturnType<ModelRuntime["getModels"]>[number];
137
+ signal: AbortSignal | undefined;
138
+ };
139
+
140
+ /** Selects skills for the task of a turn and returns their bodies as a section of this turn's system prompt, or undefined. */
141
+ export type SkillPreload = (request: SkillPreloadRequest) => Promise<string | undefined>;
142
+
143
+ export const createSkillPreload = (options: SkillPreloadOptions): SkillPreload => async ({ prompt, skills, model, signal }) => {
144
+ try {
145
+ if (skills.length === 0)
146
+ return undefined;
147
+
148
+ const explicit = explicitSkillNames(prompt, skills);
149
+ let names: readonly string[] | null = explicit.length > 0 ? explicit : null;
150
+ let candidates = skills;
151
+
152
+ if (explicit.length === 0) {
153
+ candidates = skills.filter((skill) => !skill.disableModelInvocation);
154
+ const catalog = candidates.map(({ name, description }) => ({ name, description }));
155
+
156
+ if (catalog.length === 0)
157
+ return undefined;
158
+
159
+ const selector = options.selector ?? agentSelector(
160
+ options.modelRuntime,
161
+ model,
162
+ options.maxSelectionTokens ?? 100,
163
+ options.onUsage,
164
+ );
165
+
166
+ names = await selector({ prompt, skills: catalog, signal });
167
+ }
168
+
169
+ if (!names || names.length === 0)
170
+ return undefined;
171
+
172
+ const selected = names
173
+ .map((name) => candidates.find((skill) => skill.name === name))
174
+ .filter((skill): skill is Skill => skill !== undefined);
175
+
176
+ return selected.length === 0 ? undefined : await preloadedSkills(selected);
177
+ } catch (error) {
178
+ options.onDiagnostic?.(
179
+ `Agent skill preloading fell back to the skill catalog: ${error instanceof Error ? error.message : String(error)}`,
180
+ );
181
+ return undefined;
182
+ }
183
+ };
@@ -1,4 +1,9 @@
1
+ import type { ImageContent, TextContent, UserContent } from "@ragents/ai";
2
+
1
3
  import type { AgentDriverKind, ModelSelection } from "../domain/driver.ts";
4
+ import type { EventPayloads } from "../domain/events.ts";
5
+ import type { CompletedModelStep } from "../runtime/decisions/turns.ts";
6
+ import type { ModelContext } from "../agents/model-context.ts";
2
7
  import type { JsonValue } from "../domain/json.ts";
3
8
  import type { TurnUsage } from "../domain/model.ts";
4
9
  import type { DeliveredInput } from "../agents/delivery.ts";
@@ -24,20 +29,33 @@ type TurnDriverFacts = {
24
29
  agent: {
25
30
  driverKind: "agent";
26
31
  selection: ModelSelection;
27
- forkOf: string | null;
28
32
  /** `modelContext` names the model context the result enters, see `ToolScope.modelContext`; without it file tools track no seen state. */
29
33
  invoke: (toolCallId: string, name: string, input: JsonValue, modelContext?: string) => Promise<ToolInvocation>;
30
34
  /** Claims the pending inputs that may join this turn now, in journal order; empty once the turn ends or aborts. */
31
35
  claimSteering: () => readonly SteeredInput[];
36
+ /** The model context of this agent as the journal holds it now. */
37
+ modelContext: () => ModelContext;
38
+ /** Records what enters the model context; only a recorded entry is context. */
39
+ recordContext: (entry: ModelContextRecord) => void;
40
+ /** What a hook kept for this agent, in the journal as plugin state of the actor. */
41
+ hookState: {
42
+ kept: (hookId: string) => JsonValue | undefined;
43
+ keep: (hookId: string, value: JsonValue) => void;
44
+ };
32
45
  };
33
46
  };
34
47
 
35
48
  export type DriverEvent =
36
- | { kind: "assistant"; text: string }
37
49
  | { kind: "assistant-interrupted"; text: string }
38
- | { kind: "reasoning"; text: string }
39
50
  | { kind: "runtime"; text: string };
40
51
 
52
+ /** What a driver writes into the model context of its agent, in the order the model sees it. */
53
+ export type ModelContextRecord =
54
+ | { kind: "input"; inputId: string | null; content: string | readonly UserContent[] }
55
+ | { kind: "step"; step: CompletedModelStep }
56
+ | { kind: "tool-result"; toolCallId: string; toolName: string; isError: boolean; content: readonly (TextContent | ImageContent)[] }
57
+ | { kind: "compaction"; compaction: Omit<EventPayloads["context.compacted"], "turnId"> };
58
+
41
59
  export type DriverToolEvent =
42
60
  | { kind: "started"; id: string; name: string; input: JsonValue }
43
61
  | { kind: "completed"; id: string; name: string; output: JsonValue }
@@ -60,8 +78,6 @@ export type TurnRequest<Kind extends AutomatedDriverKind = AutomatedDriverKind>
60
78
  systemPrompt: string;
61
79
  /** The working directory of the workspace tools, as the model sees it; it may lie on another machine. */
62
80
  workspace: string;
63
- /** The folder on this machine for the driver's own work, resolved only when a driver needs it. */
64
- runtimeDirectory: () => Promise<string>;
65
81
  /** Stores an attached file under attachments/ of the workspace, where the workspace tools read it; returns its name there. */
66
82
  storeAttachment: (name: string, content: Uint8Array) => Promise<string>;
67
83
  tools: readonly RunFunction[];
@@ -27,6 +27,7 @@ export {
27
27
  PluginHost,
28
28
  ProfileContributionRegistry,
29
29
  PromptContributionRegistry,
30
+ RunConditionRegistry,
30
31
  SessionMetadataContributionRegistry,
31
32
  SkillContributionRegistry,
32
33
  StartEntryContributionRegistry,
@@ -91,18 +92,19 @@ export { TurnToolset, schemaComplaints, type ToolInvocation } from "./agents/too
91
92
  export { claimTurn, workingActorFrom, type ClaimedTurn } from "./agents/turn.ts";
92
93
  export { FixedWorkspaces, type Workspaces } from "./agents/workspaces.ts";
93
94
  export {
94
- AgentSessionDriver,
95
+ AgentLoopDriver,
95
96
  AgentRuntimeManager,
96
- createSkillPreloadExtension,
97
- skillPreloadExtensionName,
98
- turnDispatcherExtensionName,
99
- type AgentSessionDriverOptions,
97
+ agentHookOf,
98
+ createSkillPreload,
99
+ skillPreloadHookName,
100
+ type AgentHook,
101
+ type AgentLoopDriverOptions,
100
102
  type AgentRuntimeContext,
101
103
  type AgentRuntimeDiagnostic,
102
104
  type AgentRuntimeManagerOptions,
105
+ type SkillPreload,
103
106
  type SkillPreloadConfiguration,
104
107
  type SkillPreloadOptions,
105
- type AgentSessionStore,
106
108
  type SkillCatalogEntry,
107
109
  type SkillSelectionRequest,
108
110
  type SkillSelector,
@@ -1,7 +1,6 @@
1
1
  import path from "node:path";
2
2
  import type { IncomingMessage, ServerResponse } from "node:http";
3
- import type { InlineExtension } from "@ragents/agent";
4
- import { agentHookExtension } from "./drivers/agent-hooks.ts";
3
+ import { agentHookOf, type AgentHook } from "./drivers/agent-hooks.ts";
5
4
  import { Value } from "typebox/value";
6
5
  import { canStartEntry, defaultHttpRights, isAccessRight, unrestrictedAccess, type AccessContext } from "./access.ts";
7
6
  import { DomainError } from "./runtime/domain-error.ts";
@@ -42,6 +41,7 @@ import type {
42
41
  RAgentsPlugin,
43
42
  ServiceToken,
44
43
  RegisteredStartOption,
44
+ RunCondition,
45
45
  SessionLifecycleContribution,
46
46
  SessionMetadata,
47
47
  SessionStartedContext,
@@ -87,6 +87,24 @@ class ContributionRegistry<T extends { id: string }> {
87
87
  }
88
88
  }
89
89
 
90
+ /** Die Laufbedingung je Plugin; ein Plugin ohne gilt in jedem Run. */
91
+ export class RunConditionRegistry {
92
+ readonly #conditions = new Map<string, RunCondition>();
93
+
94
+ register(owner: string, condition: RunCondition): void {
95
+ if (this.#conditions.has(owner)) throw new Error(`Plugin ${owner} hat bereits eine Laufbedingung`);
96
+ this.#conditions.set(owner, condition);
97
+ }
98
+
99
+ has(owner: string): boolean {
100
+ return this.#conditions.has(owner);
101
+ }
102
+
103
+ applies(owner: string, runId: string): boolean {
104
+ return this.#conditions.get(owner)?.(runId) ?? true;
105
+ }
106
+ }
107
+
90
108
  export class MethodContributionRegistry {
91
109
  readonly #methods = new ContributionRegistry<{ id: string; contribution: MethodContribution }>("Methode");
92
110
 
@@ -220,6 +238,11 @@ export class OperationContributionRegistry {
220
238
 
221
239
  export class AgentContributionRegistry {
222
240
  readonly #contributions = new ContributionRegistry<AgentContribution>("Agent-Beitrag");
241
+ readonly #conditions: RunConditionRegistry;
242
+
243
+ constructor(conditions = new RunConditionRegistry()) {
244
+ this.#conditions = conditions;
245
+ }
223
246
 
224
247
  register(owner: string, contributions: readonly AgentContribution[]): void {
225
248
  for (const contribution of contributions) {
@@ -230,8 +253,10 @@ export class AgentContributionRegistry {
230
253
  this.#contributions.register(owner, contributions);
231
254
  }
232
255
 
233
- resolve(context: AgentContributionContext): readonly InlineExtension[] {
234
- return this.#contributions.entries().map(({ value }) => agentHookExtension(value, context));
256
+ resolve(context: AgentContributionContext): readonly AgentHook[] {
257
+ return this.#contributions.entries()
258
+ .filter(({ owner }) => this.#conditions.applies(owner, context.runId))
259
+ .map(({ value }) => agentHookOf(value, context));
235
260
  }
236
261
 
237
262
  describe(): readonly PublicAgentHookContribution[] {
@@ -247,6 +272,11 @@ export class AgentContributionRegistry {
247
272
 
248
273
  export class ToolContributionRegistry {
249
274
  readonly #contributors: Owned<ToolContributor>[] = [];
275
+ readonly #conditions: RunConditionRegistry;
276
+
277
+ constructor(conditions = new RunConditionRegistry()) {
278
+ this.#conditions = conditions;
279
+ }
250
280
 
251
281
  register(owner: string, contributors: readonly ToolContributor[]): void {
252
282
  for (const contributor of contributors) {
@@ -279,7 +309,9 @@ export class ToolContributionRegistry {
279
309
  }
280
310
 
281
311
  entries(): readonly ToolContributor[] {
282
- return this.#contributors.map(({ value }) => value);
312
+ return this.#contributors.map(({ owner, value }) => this.#conditions.has(owner)
313
+ ? { ...value, runCondition: (runId: string) => this.#conditions.applies(owner, runId) }
314
+ : value);
283
315
  }
284
316
 
285
317
  describe(): readonly PublicToolDescriptor[] {
@@ -296,6 +328,11 @@ export class ToolContributionRegistry {
296
328
 
297
329
  export class PromptContributionRegistry {
298
330
  readonly #prompts = new ContributionRegistry<PromptContribution>("Prompt-Beitrag");
331
+ readonly #conditions: RunConditionRegistry;
332
+
333
+ constructor(conditions = new RunConditionRegistry()) {
334
+ this.#conditions = conditions;
335
+ }
299
336
 
300
337
  register(owner: string, prompts: readonly PromptContribution[]): void {
301
338
  this.#prompts.register(owner, prompts);
@@ -331,10 +368,13 @@ export class PromptContributionRegistry {
331
368
  })));
332
369
  }
333
370
 
334
- /** Die Beiträge, die für diesen Run anders lauten als im Schnappschuss; alles andere bleibt der gerenderte Text. */
371
+ /** Die Beiträge, die für diesen Run anders lauten als im Schnappschuss; ein leerer Text fehlt dort, alles andere bleibt der gerenderte Text. */
335
372
  runOverrides(runId: string): ReadonlyMap<string, string> {
336
- return new Map([...this.#prompts.entries()].flatMap(({ value }) =>
337
- value.renderForRun ? [[value.id, value.renderForRun(runId).trim()] as const] : []));
373
+ return new Map([...this.#prompts.entries()].flatMap(({ owner, value }) => {
374
+ if (!this.#conditions.applies(owner, runId)) return [[value.id, ""] as const];
375
+ const text = value.renderForRun?.(runId);
376
+ return text === undefined ? [] : [[value.id, text.trim()] as const];
377
+ }));
338
378
  }
339
379
 
340
380
  static handlebarsContext(context: PromptRenderContext): Record<string, unknown> {
@@ -344,6 +384,11 @@ export class PromptContributionRegistry {
344
384
 
345
385
  export class SkillContributionRegistry {
346
386
  readonly #skills = new ContributionRegistry<SkillContribution>("Skill-Beitrag");
387
+ readonly #conditions: RunConditionRegistry;
388
+
389
+ constructor(conditions = new RunConditionRegistry()) {
390
+ this.#conditions = conditions;
391
+ }
347
392
 
348
393
  register(owner: string, skills: readonly SkillContribution[]): void {
349
394
  for (const skill of skills) {
@@ -386,7 +431,7 @@ export class SkillContributionRegistry {
386
431
  id: value.id,
387
432
  owner,
388
433
  audiences: value.audiences ?? allAudiences,
389
- paths: await value.paths(context),
434
+ paths: context && !this.#conditions.applies(owner, context.runId) ? [] : await value.paths(context),
390
435
  })));
391
436
  }
392
437
  }
@@ -1069,10 +1114,11 @@ export class PluginHost {
1069
1114
  readonly channels = new ChannelContributionRegistry();
1070
1115
  readonly http = new HttpContributionRegistry();
1071
1116
  readonly operations = new OperationContributionRegistry();
1072
- readonly agentRuntime = new AgentContributionRegistry();
1073
- readonly tools = new ToolContributionRegistry();
1074
- readonly prompts = new PromptContributionRegistry();
1075
- readonly skills = new SkillContributionRegistry();
1117
+ readonly runConditions = new RunConditionRegistry();
1118
+ readonly agentRuntime = new AgentContributionRegistry(this.runConditions);
1119
+ readonly tools = new ToolContributionRegistry(this.runConditions);
1120
+ readonly prompts = new PromptContributionRegistry(this.runConditions);
1121
+ readonly skills = new SkillContributionRegistry(this.runConditions);
1076
1122
  readonly startEntries = new StartEntryContributionRegistry();
1077
1123
  readonly profiles = new ProfileContributionRegistry();
1078
1124
  readonly script = new ScriptContributionRegistry();
@@ -1122,6 +1168,7 @@ export class PluginHost {
1122
1168
  agentRuntime: (...entries) => this.agentRuntime.register(manifest.id, entries),
1123
1169
  profiles: (...entries) => this.profiles.register(manifest.id, entries),
1124
1170
  prompts: (...entries) => this.prompts.register(manifest.id, entries),
1171
+ runCondition: (condition) => this.runConditions.register(manifest.id, condition),
1125
1172
  provide: (token, service) => this.#services.provide(manifest.id, token, service),
1126
1173
  service: (token) => this.#services.require(token),
1127
1174
  optionalService: (token) => this.#services.optional(token),
@@ -277,10 +277,13 @@ export interface PromptContribution {
277
277
  delivery?: "initial" | "on-demand";
278
278
  requiresTools?: readonly string[];
279
279
  render: (context: PromptRenderContext) => string | Promise<string>;
280
- /** Hängt der Beitrag am einzelnen Run, ersetzt dieser Text den von `render` - synchron, damit der Turn ihn ohne Warten hat. */
281
- renderForRun?: (runId: string) => string;
280
+ /** Hängt der Beitrag am einzelnen Run, ersetzt dieser Text den von `render`, undefined behält ihn - synchron, damit der Turn ihn ohne Warten hat. */
281
+ renderForRun?: (runId: string) => string | undefined;
282
282
  }
283
283
 
284
+ /** Ob die laufbezogenen Beiträge eines Plugins in einem Run gelten; der Host fragt je Run, ein Wurf lässt dessen Turn mit dieser Ursache scheitern. */
285
+ export type RunCondition = (runId: string) => boolean;
286
+
284
287
  export type ModelProviderConfig = Parameters<ModelRuntime["registerProvider"]>[1];
285
288
 
286
289
  export interface ModelProviderRegistration {
@@ -420,6 +423,8 @@ export interface PluginRegistration {
420
423
  agentRuntime: (...contributions: readonly AgentContribution[]) => void;
421
424
  profiles: (...contributions: readonly ProfileContribution[]) => void;
422
425
  prompts: (...contributions: readonly PromptContribution[]) => void;
426
+ /** Prompts, Funktionen, Skills und Agent-Hooks dieses Plugins gelten nur in Runs, für die die Bedingung zutrifft; höchstens eine je Plugin. */
427
+ runCondition: (condition: RunCondition) => void;
423
428
  provide: <T>(token: ServiceToken<T>, service: T) => void;
424
429
  service: <T>(token: ServiceToken<T>) => T;
425
430
  optionalService: <T>(token: ServiceToken<T>) => T | undefined;
@@ -132,6 +132,16 @@ const forkSourceOf = (state: RunState, reference: string): string => {
132
132
  if (source.execution.driver.kind !== "agent")
133
133
  throw new DomainError("invalid-value", `Fork source ${reference} has no model context to fork.`, 400);
134
134
 
135
+ const finishedContext = [...state.turns.values()].some((turn) =>
136
+ turn.actorId === source.id && turn.status !== "running" && state.contextTurns.has(turn.id));
137
+
138
+ if (source.forkOf === null && !finishedContext)
139
+ throw new DomainError(
140
+ "fork-without-turn",
141
+ `Fork source ${reference} has no model context from a finished turn yet; a fork takes the context of finished turns only, never of the running one.`,
142
+ 409,
143
+ );
144
+
135
145
  return source.id;
136
146
  };
137
147
 
@@ -1,3 +1,4 @@
1
+ import type { EventPayloads, ModelInputContent, ModelStepBlock, ModelToolResultPart, UncommittedEvent } from "../../domain/events.ts";
1
2
  import type { JsonValue } from "../../domain/json.ts";
2
3
  import { isPendingActorInput, type TurnUsage } from "../../domain/model.ts";
3
4
  import { event, type Decision } from "../command.ts";
@@ -77,7 +78,7 @@ export const steerInputs =
77
78
  };
78
79
 
79
80
  const appendText = (
80
- type: "model.output.completed" | "model.output.interrupted" | "model.reasoning.completed" | "runtime.output.recorded",
81
+ type: "model.output.interrupted" | "runtime.output.recorded",
81
82
  actorId: string,
82
83
  input: { turnId: string; text: string },
83
84
  ): Decision => (state, context) => {
@@ -86,18 +87,76 @@ const appendText = (
86
87
  return [event(context, { type, payload: { turnId: input.turnId, text: clean(input.text, "text") } })];
87
88
  };
88
89
 
89
- export const appendModelOutput = (actorId: string, input: { turnId: string; text: string }): Decision =>
90
- appendText("model.output.completed", actorId, input);
91
-
92
90
  export const appendInterruptedModelOutput = (actorId: string, input: { turnId: string; text: string }): Decision =>
93
91
  appendText("model.output.interrupted", actorId, input);
94
92
 
95
- export const appendModelReasoning = (actorId: string, input: { turnId: string; text: string }): Decision =>
96
- appendText("model.reasoning.completed", actorId, input);
97
-
98
93
  export const appendRuntimeOutput = (actorId: string, input: { turnId: string; text: string }): Decision =>
99
94
  appendText("runtime.output.recorded", actorId, input);
100
95
 
96
+ /** The input exactly as the model received it, at its place between the model steps. */
97
+ export const presentModelInput =
98
+ (actorId: string, input: { turnId: string; inputId: string | null; content: ModelInputContent }): Decision =>
99
+ (state, context) => {
100
+ runningTurn(state, context, actorId, input.turnId);
101
+
102
+ return [event(context, { type: "model.input.presented", payload: input })];
103
+ };
104
+
105
+ export type CompletedModelStep = Omit<EventPayloads["model.step.completed"], "turnId" | "content"> & {
106
+ content: readonly (
107
+ | { type: "text"; text: string; textSignature?: string }
108
+ | { type: "thinking"; thinking: string; thinkingSignature?: string; redacted?: boolean }
109
+ | Extract<ModelStepBlock, { type: "toolCall" }>
110
+ )[];
111
+ };
112
+
113
+ /** One command: the text and reasoning a step made visible as observation events, then the step itself, which does not repeat them. */
114
+ export const completeModelStep =
115
+ (actorId: string, input: { turnId: string; step: CompletedModelStep }): Decision =>
116
+ (state, context) => {
117
+ runningTurn(state, context, actorId, input.turnId);
118
+ const observed: UncommittedEvent[] = [];
119
+ const content = input.step.content.map((block): ModelStepBlock => {
120
+ if (block.type === "text" && block.text.trim()) {
121
+ observed.push(event(context, { type: "model.output.completed", payload: { turnId: input.turnId, text: block.text } }));
122
+ const { text: _text, ...rest } = block;
123
+ return rest;
124
+ }
125
+
126
+ if (block.type === "thinking" && block.thinking.trim()) {
127
+ observed.push(event(context, { type: "model.reasoning.completed", payload: { turnId: input.turnId, text: block.thinking } }));
128
+ const { thinking: _thinking, ...rest } = block;
129
+ return rest;
130
+ }
131
+
132
+ return { ...block };
133
+ });
134
+
135
+ return [
136
+ ...observed,
137
+ event(context, { type: "model.step.completed", payload: { turnId: input.turnId, ...input.step, content } }),
138
+ ];
139
+ };
140
+
141
+ export const presentToolResult =
142
+ (
143
+ actorId: string,
144
+ input: { turnId: string; toolCallId: string; toolName: string; isError: boolean; content?: ModelToolResultPart[] },
145
+ ): Decision =>
146
+ (state, context) => {
147
+ runningTurn(state, context, actorId, input.turnId);
148
+
149
+ return [event(context, { type: "model.tool-result.presented", payload: input })];
150
+ };
151
+
152
+ export const compactContext =
153
+ (actorId: string, input: EventPayloads["context.compacted"]): Decision =>
154
+ (state, context) => {
155
+ runningTurn(state, context, actorId, input.turnId);
156
+
157
+ return [event(context, { type: "context.compacted", payload: input })];
158
+ };
159
+
101
160
  export const startToolCall =
102
161
  (actorId: string, input: { turnId: string; toolCallId: string; name: string; input: JsonValue; ignoredFields?: string[] }): Decision =>
103
162
  (state, context) => {
@@ -8,9 +8,9 @@ import { isRunId } from "../domain/portable-id.ts";
8
8
  import { syncDirectory } from "./durable-fs.ts";
9
9
  import type { CommandRecord, JournalCommand } from "./journal.ts";
10
10
 
11
- /** Rises whenever an older stand would reject newly written lines; the encoding is unchanged since 4, so 4 stays readable. */
12
- export const journalStorageVersion = 6;
13
- const readableJournalStorageVersions: readonly unknown[] = [4, 5, journalStorageVersion];
11
+ /** Rises whenever an older stand would reject newly written lines; the journal reads only this version. */
12
+ export const journalStorageVersion = 7;
13
+ const readableJournalStorageVersions: readonly unknown[] = [journalStorageVersion];
14
14
  export const journalPayloadThresholdBytes = 4096;
15
15
 
16
16
  type PayloadReference = { sha256: string; bytes: number };
@@ -161,7 +161,10 @@ export const parseJournalRecord = (line: string, runDirectory: string, location:
161
161
  const record = objectOf(parsed, location);
162
162
 
163
163
  if (!readableJournalStorageVersions.includes(record.formatVersion))
164
- throw new Error(`${location} has unsupported journal format version ${String(record.formatVersion)}.`);
164
+ throw new Error(`${location} has unsupported journal format version ${String(record.formatVersion)}.${
165
+ typeof record.formatVersion === "number" && record.formatVersion < journalStorageVersion
166
+ ? " Journals before format 7 hold no model context and are not migrated."
167
+ : ""}`);
165
168
 
166
169
  exactKeys(record, location, ["formatVersion", "runId", "command", "occurredAt", "events"]);
167
170
 
@@ -346,6 +346,12 @@ export class Journal {
346
346
  return [...this.#loadFailures.values()];
347
347
  }
348
348
 
349
+ /** Every locked run with its cause: journals rejected at load and runs isolated by a write failure. */
350
+ unavailableRuns(): readonly JournalLoadFailure[] {
351
+ return [...new Set([...this.#loadFailures.keys(), ...this.#writeFailures.keys()])]
352
+ .flatMap((runId) => this.failureOf(runId) ?? []);
353
+ }
354
+
349
355
  failureOf(runId: string): JournalLoadFailure | null {
350
356
  const failure = this.#loadFailures.get(runId);
351
357
  const writeFailure = this.#writeFailures.get(runId);
@@ -369,6 +375,34 @@ export class Journal {
369
375
  return [...(this.#runs.get(runId)?.events ?? [])];
370
376
  }
371
377
 
378
+ /** The events after the given sequence; the sequence counts from 1 without gaps. */
379
+ eventsSince(runId: RunId, sequence: number): JournalEvent[] {
380
+ this.assertRunAvailable(runId);
381
+ return (this.#runs.get(runId)?.events ?? []).slice(sequence);
382
+ }
383
+
384
+ /** The events from the newest back, without copying the journal; a caller stops as soon as it has what it needs. */
385
+ *recentEvents(runId: RunId): Generator<JournalEvent> {
386
+ this.assertRunAvailable(runId);
387
+ const events = this.#runs.get(runId)?.events ?? [];
388
+
389
+ for (let index = events.length - 1; index >= 0; index--)
390
+ yield events[index]!;
391
+ }
392
+
393
+ /** A value read from the current state without cloning all of it; `read` must neither keep nor change the state. */
394
+ select<Value>(runId: RunId, read: (state: RunState) => Value): Value | null {
395
+ this.assertRunAvailable(runId);
396
+ const state = this.#runs.get(runId)?.state;
397
+
398
+ return state ? clone(read(state)) : null;
399
+ }
400
+
401
+ firstEventId(runId: RunId): string | undefined {
402
+ this.assertRunAvailable(runId);
403
+ return this.#runs.get(runId)?.events[0]?.eventId;
404
+ }
405
+
372
406
  records(runId: RunId): readonly CommandRecord[] {
373
407
  this.assertRunAvailable(runId);
374
408
  return [...(this.#runs.get(runId)?.records ?? [])];
@@ -571,6 +605,7 @@ export class Journal {
571
605
  const semantics = log ? {
572
606
  events: new Map(log.semantics.events),
573
607
  sourcedToolCalls: new Set(log.semantics.sourcedToolCalls),
608
+ presented: new Set(log.semantics.presented),
574
609
  } : eventSemanticContext();
575
610
  let state: RunDraft | null = log?.state ? forkProjection(log.state) : null;
576
611