@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
@@ -24,6 +24,7 @@ import type { WorkspaceToolNaming } from "./workspace-tools.ts";
24
24
  import { TurnToolset } from "./toolset.ts";
25
25
  import { toolOrientationText } from "./tool-orientation.ts";
26
26
  import { claimTurn, type ClaimedTurn } from "./turn.ts";
27
+ import { turnModelContext } from "./model-context.ts";
27
28
  import { FixedWorkspaces, type Workspaces } from "./workspaces.ts";
28
29
 
29
30
  export type TurnSchedulerOptions = {
@@ -899,7 +900,6 @@ export class TurnScheduler {
899
900
  workspaceChapter,
900
901
  ),
901
902
  workspace,
902
- runtimeDirectory: () => this.#workspaces.runtimeDirectory(runId),
903
903
  storeAttachment: (name: string, content: Uint8Array) => this.#workspaces.storeAttachment(runId, name, content),
904
904
  tools: driverRef.kind === "script" ? toolset.functions : toolset.tools,
905
905
  allowedToolNames: actor.toolNames,
@@ -931,9 +931,9 @@ export class TurnScheduler {
931
931
  ...request,
932
932
  driverKind: "agent",
933
933
  selection: selection!,
934
- forkOf: actor.kind === "agent" ? actor.forkOf : null,
935
934
  invoke: (toolCallId: string, name: string, input: JsonValue, modelContext?: string) => toolset.invoke(toolCallId, name, input, modelContext),
936
935
  claimSteering: () => this.#claimSteering(turn, controller.signal),
936
+ ...turnModelContext(this.#runtime, turn, () => emitted++),
937
937
  },
938
938
  controller.signal,
939
939
  )
@@ -1059,12 +1059,8 @@ export class TurnScheduler {
1059
1059
  };
1060
1060
  const payload = { turnId: turn.turnId, text: driverEvent.text };
1061
1061
 
1062
- if (driverEvent.kind === "assistant")
1063
- this.#runtime.appendModelOutput(context, turn.runId, turn.actorId, payload);
1064
- else if (driverEvent.kind === "assistant-interrupted")
1062
+ if (driverEvent.kind === "assistant-interrupted")
1065
1063
  this.#runtime.appendInterruptedModelOutput(context, turn.runId, turn.actorId, payload);
1066
- else if (driverEvent.kind === "reasoning")
1067
- this.#runtime.appendModelReasoning(context, turn.runId, turn.actorId, payload);
1068
1064
  else
1069
1065
  this.#runtime.appendRuntimeOutput(context, turn.runId, turn.actorId, payload);
1070
1066
  }
@@ -544,13 +544,13 @@ export const agentTools: RunFunction[] = [
544
544
  + "It inherits delegable capabilities, but tools is required and never inherited: select exact names, [] for text-only work, or explicit null for an open, dynamically resolved toolset. "
545
545
  + "An empty tools array creates a plain LLM with no runtime, workspace or host tools; "
546
546
  + "drivers without plain-LLM isolation are rejected. "
547
- + "forkOf copies the model context of an existing LLM agent of this run into the new agent at its first turn: the copy ends before any unfinished tool call and carries no reasoning; the new agent still gets its own prompt, tools and model.",
547
+ + "forkOf gives the new agent an unchanged copy of the model context of an existing LLM agent of this run, up to the end of that agent's last finished turn; nothing of its running turn is copied, and a source without a finished turn is rejected. The new agent still gets its own prompt, tools and model, and its first input follows the copy.",
548
548
  schema: Type.Object({
549
549
  handle: Type.String({ minLength: 1 }),
550
550
  displayName: Type.Optional(Type.String({ minLength: 1, description: "Anzeigename; ohne Angabe der Handle" })),
551
551
  description: Type.Optional(Type.String({ minLength: 1, maxLength: actorDescriptionMaxLength, description: "Sehr kurze Beschreibung der Aufgabe für die Übersicht der Beteiligten, wenige Wörter wie \"prüft die Regel zu Kommentaren\"" })),
552
552
  prompt: Type.String(),
553
- forkOf: Type.Optional(Type.String({ minLength: 1, description: "Handle oder ID eines LLM-Agenten dieses Runs, dessen bisheriger Modellkontext in den neuen Agenten kopiert wird" })),
553
+ forkOf: Type.Optional(Type.String({ minLength: 1, description: "Handle or ID of an LLM agent of this run whose model context up to the end of its last finished turn is copied into the new agent" })),
554
554
  tools: Type.Union([Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }), Type.Null()], { description: "Required explicit selection: [] for plain text-only work including app-mediated conversations; an array for exact existing tool names; null only when the task needs an open, dynamically resolved toolset. Never inherits the caller's tools. Names of future, not yet activated actor functions are invalid; choose null when those must become available later." }),
555
555
  withoutCapabilities: Type.Optional(Type.Array(capabilitySchema, { uniqueItems: true })),
556
556
  ...executionSchema,
@@ -164,18 +164,46 @@ export class TurnToolset {
164
164
  this.tools.splice(0, this.tools.length, ...this.functions.filter((tool) => tool.nativeTool === true));
165
165
  }
166
166
 
167
+ /** A model's tool call always lands in the journal: a failure before the start is recorded as start and failure. */
167
168
  async invoke(toolCallId: string, name: string, input: JsonValue, modelContext?: string): Promise<ToolInvocation> {
168
- await this.refresh();
169
- const tool = this.tools.find((entry) => entry.name === name);
169
+ const prepared = await this.#beforeStart(toolCallId, name, input, async () => {
170
+ await this.refresh();
171
+ const tool = this.tools.find((entry) => entry.name === name);
170
172
 
171
- if (!tool)
172
- throw new Error(`Tool ${name} is not available to actor ${this.#turn.actorId}.`);
173
+ if (!tool)
174
+ throw new Error(`Tool ${name} is not available to actor ${this.#turn.actorId}.`);
173
175
 
174
- assertJsonValue(input, `Tool ${name} input`);
175
- const tolerated = withoutUnknownFields(tool.schema, input);
176
- const output = await this.#execute(toolCallId, tool, tolerated.input, tolerated.ignoredFields, modelContext);
176
+ assertJsonValue(input, `Tool ${name} input`);
177
+ const tolerated = withoutUnknownFields(tool.schema, input);
178
+
179
+ if (!this.#replayOf(toolCallId) && !Value.Check(tool.schema, tolerated.input))
180
+ throw new Error(inputComplaint(tool, tolerated.input, tolerated.ignoredFields));
181
+
182
+ return { tool, tolerated };
183
+ });
184
+ const output = await this.#execute(toolCallId, prepared.tool, prepared.tolerated.input, prepared.tolerated.ignoredFields, modelContext);
185
+
186
+ return { output, ignoredFields: prepared.tolerated.ignoredFields };
187
+ }
188
+
189
+ async #beforeStart<T>(toolCallId: string, name: string, input: JsonValue, prepare: () => Promise<T>): Promise<T> {
190
+ try {
191
+ return await prepare();
192
+ } catch (error) {
193
+ if (this.#replayOf(toolCallId))
194
+ throw error;
195
+
196
+ try {
197
+ this.#assertActive();
198
+ assertJsonValue(input, `Tool ${name} input`);
199
+ } catch {
200
+ throw error;
201
+ }
177
202
 
178
- return { output, ignoredFields: tolerated.ignoredFields };
203
+ this.#runtime.startToolCall(this.#context(toolCallId, "started"), this.#turn.runId, this.#turn.actorId, { turnId: this.#turn.turnId, toolCallId, name, input });
204
+ this.#runtime.failToolCall(this.#context(toolCallId, "failed"), this.#turn.runId, this.#turn.actorId, { turnId: this.#turn.turnId, toolCallId, name, error: errorText(error) });
205
+ throw error;
206
+ }
179
207
  }
180
208
 
181
209
  async invokeFunction(toolCallId: string, name: string, input: JsonValue): Promise<JsonValue> {
@@ -282,18 +310,17 @@ export class TurnToolset {
282
310
  }
283
311
 
284
312
  #replayOf(toolCallId: string) {
285
- const events = this.#runtime.events(this.#turn.runId).filter((event): event is ToolCallEvent => {
286
- if (
287
- event.type !== "tool.call.started" &&
288
- event.type !== "tool.call.completed" &&
289
- event.type !== "tool.call.failed"
290
- )
291
- return false;
292
-
293
- return event.actorId === this.#turn.actorId &&
294
- event.payload.turnId === this.#turn.turnId &&
295
- event.payload.toolCallId === toolCallId;
296
- });
313
+ const events: ToolCallEvent[] = [];
314
+
315
+ for (const event of this.#runtime.recentEvents(this.#turn.runId)) {
316
+ if (event.type === "turn.started" && event.payload.turnId === this.#turn.turnId)
317
+ break;
318
+
319
+ if ((event.type === "tool.call.started" || event.type === "tool.call.completed" || event.type === "tool.call.failed")
320
+ && event.actorId === this.#turn.actorId && event.payload.turnId === this.#turn.turnId && event.payload.toolCallId === toolCallId)
321
+ events.unshift(event);
322
+ }
323
+
297
324
  const started = events.find((event) => event.type === "tool.call.started");
298
325
  const terminal = events.findLast(
299
326
  (event) => event.type === "tool.call.completed" || event.type === "tool.call.failed",
@@ -2,8 +2,6 @@ export interface Workspaces {
2
2
  ensure(runId: string, workspacePath: string | null): string;
3
3
  /** How the run's workspace is described to a model; undefined when nothing describes it. */
4
4
  description(runId: string): string | undefined;
5
- /** The folder on this machine where a driver keeps its own work; the workspace itself may lie on another machine. */
6
- runtimeDirectory(runId: string): Promise<string>;
7
5
  /** Stores an attached file under attachments/ of the workspace, on the machine where the workspace lies; returns its name there. */
8
6
  storeAttachment(runId: string, name: string, content: Uint8Array): Promise<string>;
9
7
  }
@@ -23,10 +21,6 @@ export class FixedWorkspaces implements Workspaces {
23
21
  return undefined;
24
22
  }
25
23
 
26
- runtimeDirectory() {
27
- return Promise.resolve(this.#path);
28
- }
29
-
30
24
  storeAttachment(): Promise<string> {
31
25
  return Promise.reject(new Error("Ein fester Arbeitsbereich ohne Executor nimmt keine Dateianhänge an."));
32
26
  }
@@ -20,13 +20,18 @@ export type EventSemanticContext = {
20
20
  events: Map<string, JournalEvent>;
21
21
  /** Tool calls with a recorded source, by turn and call; their state stands in the turn. */
22
22
  sourcedToolCalls: Set<string>;
23
+ /** Inputs and tool results already presented to the model, by turn and input or call. */
24
+ presented: Set<string>;
23
25
  };
24
26
 
25
27
  export const eventSemanticContext = (): EventSemanticContext => ({
26
28
  events: new Map(),
27
29
  sourcedToolCalls: new Set(),
30
+ presented: new Set(),
28
31
  });
29
32
 
33
+ const modelContextEventTypes: ReadonlySet<string> = new Set(["model.input.presented", "model.step.completed", "model.tool-result.presented"]);
34
+
30
35
  const existingActorOf = (state: RunState, actorId: string): Actor => {
31
36
  const actor = state.actors.get(actorId);
32
37
 
@@ -438,6 +443,46 @@ export const assertEventSemantics = (
438
443
  break;
439
444
  }
440
445
 
446
+ case "model.input.presented": {
447
+ const turn = runningTurnOf(state, event.payload.turnId);
448
+ assertTurnAuthor(turn, event);
449
+
450
+ if (event.payload.inputId !== null) {
451
+ const input = state.inputs.get(event.payload.inputId);
452
+
453
+ if (!input || input.lifecycle.kind !== "claimed" || input.lifecycle.turnId !== turn.id)
454
+ throw new Error(`Input ${event.payload.inputId} does not belong to turn ${turn.id}.`);
455
+
456
+ if (context.presented.has(`${turn.id}\0input\0${input.id}`))
457
+ throw new Error(`Input ${input.id} was already presented in turn ${turn.id}.`);
458
+ }
459
+ break;
460
+ }
461
+
462
+ case "model.tool-result.presented": {
463
+ const turn = runningTurnOf(state, event.payload.turnId);
464
+ assertTurnAuthor(turn, event);
465
+ const call = toolCallOf(turn, event.payload.toolCallId);
466
+
467
+ if (call.status === "running")
468
+ throw new Error(`Tool call ${call.id} in turn ${turn.id} has no result yet.`);
469
+
470
+ if (context.presented.has(`${turn.id}\0call\0${call.id}`))
471
+ throw new Error(`The result of tool call ${call.id} in turn ${turn.id} was already presented.`);
472
+ break;
473
+ }
474
+
475
+ case "context.compacted": {
476
+ const turn = runningTurnOf(state, event.payload.turnId);
477
+ assertTurnAuthor(turn, event);
478
+ const firstKept = context.events.get(event.payload.firstKeptEventId);
479
+
480
+ if (!firstKept || !modelContextEventTypes.has(firstKept.type))
481
+ throw new Error(`Compaction keeps from ${event.payload.firstKeptEventId}, which is no model context event.`);
482
+ break;
483
+ }
484
+
485
+ case "model.step.completed":
441
486
  case "model.output.completed":
442
487
  case "model.output.interrupted":
443
488
  case "model.reasoning.completed":
@@ -653,4 +698,10 @@ export const recordEventSemantics = (event: JournalEvent, context: EventSemantic
653
698
 
654
699
  if (event.type === "tool.call.source")
655
700
  context.sourcedToolCalls.add(toolCallKey(event.payload.turnId, event.payload.toolCallId));
701
+
702
+ if (event.type === "model.input.presented" && event.payload.inputId !== null)
703
+ context.presented.add(`${event.payload.turnId}\0input\0${event.payload.inputId}`);
704
+
705
+ if (event.type === "model.tool-result.presented")
706
+ context.presented.add(`${event.payload.turnId}\0call\0${event.payload.toolCallId}`);
656
707
  };
@@ -200,6 +200,98 @@ const usageOf = (value: unknown, path: string) => {
200
200
  fail(`${path}.costUsd`, "must not be negative");
201
201
  };
202
202
 
203
+ const sha256Of = (value: unknown, path: string) => {
204
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value))
205
+ fail(path, "must be a lowercase SHA-256");
206
+ };
207
+
208
+ const modelTextPartOf = (value: unknown, path: string) => {
209
+ const part = exactObject(value, path, ["type", "text"]);
210
+ stringOf(part.text, `${path}.text`);
211
+ };
212
+
213
+ const modelInputPartOf = (value: unknown, path: string) => {
214
+ const candidate = objectOf(value, path);
215
+
216
+ if (candidate.type === "text")
217
+ return modelTextPartOf(value, path);
218
+
219
+ if (candidate.type !== "image" && candidate.type !== "video" && candidate.type !== "file")
220
+ fail(`${path}.type`, "must be text, image, video or file");
221
+
222
+ const part = exactObject(value, path, candidate.type === "file" ? ["type", "mimeType", "filename", "hash"] : ["type", "mimeType", "hash"]);
223
+ nonEmptyStringOf(part.mimeType, `${path}.mimeType`);
224
+ sha256Of(part.hash, `${path}.hash`);
225
+
226
+ if (candidate.type === "file")
227
+ stringOf(part.filename, `${path}.filename`);
228
+ };
229
+
230
+ const modelToolResultPartOf = (value: unknown, path: string) => {
231
+ const candidate = objectOf(value, path);
232
+
233
+ if (candidate.type === "text")
234
+ return modelTextPartOf(value, path);
235
+
236
+ if (candidate.type !== "image")
237
+ fail(`${path}.type`, "must be text or image");
238
+
239
+ const part = exactObject(value, path, ["type", "mimeType", "hash"]);
240
+ nonEmptyStringOf(part.mimeType, `${path}.mimeType`);
241
+ sha256Of(part.hash, `${path}.hash`);
242
+ };
243
+
244
+ const optionalStringOf = (payload: ObjectValue, key: string, path: string) => {
245
+ if (Object.hasOwn(payload, key))
246
+ stringOf(payload[key], `${path}.${key}`);
247
+ };
248
+
249
+ const modelStepBlockOf = (value: unknown, path: string) => {
250
+ const candidate = objectOf(value, path);
251
+
252
+ if (candidate.type === "text") {
253
+ const block = exactObject(value, path, ["type"], ["text", "textSignature"]);
254
+ optionalStringOf(block, "text", path);
255
+ optionalStringOf(block, "textSignature", path);
256
+ return;
257
+ }
258
+
259
+ if (candidate.type === "thinking") {
260
+ const block = exactObject(value, path, ["type"], ["thinking", "thinkingSignature", "redacted"]);
261
+ optionalStringOf(block, "thinking", path);
262
+ optionalStringOf(block, "thinkingSignature", path);
263
+
264
+ if (Object.hasOwn(block, "redacted"))
265
+ booleanOf(block.redacted, `${path}.redacted`);
266
+
267
+ return;
268
+ }
269
+
270
+ if (candidate.type !== "toolCall")
271
+ fail(`${path}.type`, "must be text, thinking or toolCall");
272
+
273
+ const block = exactObject(value, path, ["type", "id", "name", "arguments"], ["thoughtSignature"]);
274
+ nonEmptyStringOf(block.id, `${path}.id`);
275
+ stringOf(block.name, `${path}.name`);
276
+ objectOf(block.arguments, `${path}.arguments`);
277
+ assertJsonValue(block.arguments, `${path}.arguments`);
278
+ optionalStringOf(block, "thoughtSignature", path);
279
+ };
280
+
281
+ const modelStepUsageOf = (value: unknown, path: string) => {
282
+ const usage = exactObject(value, path, ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "cost"], ["cacheWrite1h", "reasoning"]);
283
+
284
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "cacheWrite1h", "reasoning"]) {
285
+ if (Object.hasOwn(usage, key))
286
+ finiteNumberOf(usage[key], `${path}.${key}`);
287
+ }
288
+
289
+ const cost = exactObject(usage.cost, `${path}.cost`, ["input", "output", "cacheRead", "cacheWrite", "total"]);
290
+
291
+ for (const key of Object.keys(cost))
292
+ finiteNumberOf(cost[key], `${path}.cost.${key}`);
293
+ };
294
+
203
295
  const pluginScopeOf = (value: unknown, path: string) => {
204
296
  const candidate = objectOf(value, path);
205
297
 
@@ -391,6 +483,80 @@ const payloadOf = (type: EventType, value: unknown, path: string) => {
391
483
  return;
392
484
  }
393
485
 
486
+ case "model.input.presented": {
487
+ const payload = exactObject(value, path, ["turnId", "inputId", "content"]);
488
+ stringOf(payload.turnId, `${path}.turnId`);
489
+ nullableStringOf(payload.inputId, `${path}.inputId`);
490
+
491
+ if (typeof payload.content !== "string")
492
+ arrayOf(payload.content, `${path}.content`, modelInputPartOf);
493
+
494
+ return;
495
+ }
496
+
497
+ case "model.step.completed": {
498
+ const payload = exactObject(
499
+ value,
500
+ path,
501
+ ["turnId", "api", "provider", "model", "usage", "stopReason", "timestamp", "content"],
502
+ ["responseModel", "responseId", "errorMessage", "diagnostics"],
503
+ );
504
+ stringOf(payload.turnId, `${path}.turnId`);
505
+ nonEmptyStringOf(payload.api, `${path}.api`);
506
+ nonEmptyStringOf(payload.provider, `${path}.provider`);
507
+ nonEmptyStringOf(payload.model, `${path}.model`);
508
+ optionalStringOf(payload, "responseModel", path);
509
+ optionalStringOf(payload, "responseId", path);
510
+ optionalStringOf(payload, "errorMessage", path);
511
+ modelStepUsageOf(payload.usage, `${path}.usage`);
512
+
513
+ if (!["stop", "length", "toolUse", "error"].includes(payload.stopReason as string))
514
+ fail(`${path}.stopReason`, "must be stop, length, toolUse or error");
515
+
516
+ finiteNumberOf(payload.timestamp, `${path}.timestamp`);
517
+ arrayOf(payload.content, `${path}.content`, modelStepBlockOf);
518
+
519
+ if (Object.hasOwn(payload, "diagnostics"))
520
+ arrayOf(payload.diagnostics, `${path}.diagnostics`, assertJsonValue);
521
+
522
+ return;
523
+ }
524
+
525
+ case "model.tool-result.presented": {
526
+ const payload = exactObject(value, path, ["turnId", "toolCallId", "toolName", "isError"], ["content"]);
527
+ stringOf(payload.turnId, `${path}.turnId`);
528
+ nonEmptyStringOf(payload.toolCallId, `${path}.toolCallId`);
529
+ stringOf(payload.toolName, `${path}.toolName`);
530
+ booleanOf(payload.isError, `${path}.isError`);
531
+
532
+ if (Object.hasOwn(payload, "content"))
533
+ arrayOf(payload.content, `${path}.content`, modelToolResultPartOf);
534
+
535
+ return;
536
+ }
537
+
538
+ case "context.compacted": {
539
+ const payload = exactObject(value, path, [
540
+ "turnId",
541
+ "summary",
542
+ "firstKeptEventId",
543
+ "tokensBefore",
544
+ "provider",
545
+ "model",
546
+ "readFiles",
547
+ "modifiedFiles",
548
+ ]);
549
+ stringOf(payload.turnId, `${path}.turnId`);
550
+ nonEmptyStringOf(payload.summary, `${path}.summary`);
551
+ nonEmptyStringOf(payload.firstKeptEventId, `${path}.firstKeptEventId`);
552
+ nonNegativeIntegerOf(payload.tokensBefore, `${path}.tokensBefore`);
553
+ nonEmptyStringOf(payload.provider, `${path}.provider`);
554
+ nonEmptyStringOf(payload.model, `${path}.model`);
555
+ stringArrayOf(payload.readFiles, `${path}.readFiles`);
556
+ stringArrayOf(payload.modifiedFiles, `${path}.modifiedFiles`);
557
+ return;
558
+ }
559
+
394
560
  case "model.output.completed":
395
561
  case "model.output.interrupted":
396
562
  case "model.reasoning.completed":
@@ -17,6 +17,35 @@ import type {
17
17
  } from "./model.ts";
18
18
  import type { JsonChange, JsonObject, JsonValue } from "./json.ts";
19
19
 
20
+ /** A media part the model saw, stored by the SHA-256 of its bytes in the artifact contents instead of Base64. */
21
+ export type ModelMediaPart =
22
+ | { type: "image" | "video"; mimeType: string; hash: string }
23
+ | { type: "file"; mimeType: string; filename: string; hash: string };
24
+
25
+ export type ModelTextPart = { type: "text"; text: string };
26
+
27
+ /** The content of a user message exactly as the model received it. */
28
+ export type ModelInputContent = string | (ModelTextPart | ModelMediaPart)[];
29
+
30
+ export type ModelToolResultPart = ModelTextPart | { type: "image"; mimeType: string; hash: string };
31
+
32
+ /** A block of a model step; text and thinking without their own field stand in the observation events of the same command, in order. */
33
+ export type ModelStepBlock =
34
+ | { type: "text"; text?: string; textSignature?: string }
35
+ | { type: "thinking"; thinking?: string; thinkingSignature?: string; redacted?: boolean }
36
+ | { type: "toolCall"; id: string; name: string; arguments: JsonObject; thoughtSignature?: string };
37
+
38
+ export type ModelStepUsage = {
39
+ input: number;
40
+ output: number;
41
+ cacheRead: number;
42
+ cacheWrite: number;
43
+ cacheWrite1h?: number;
44
+ reasoning?: number;
45
+ totalTokens: number;
46
+ cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
47
+ };
48
+
20
49
  export type EventPayloads = {
21
50
  "run.created": {
22
51
  title: string;
@@ -84,6 +113,45 @@ export type EventPayloads = {
84
113
  turnId: TurnId;
85
114
  reason: string;
86
115
  };
116
+ "model.input.presented": {
117
+ turnId: TurnId;
118
+ /** The claimed input, or null for a message of the agent loop itself. */
119
+ inputId: InputId | null;
120
+ content: ModelInputContent;
121
+ };
122
+ "model.step.completed": {
123
+ turnId: TurnId;
124
+ api: string;
125
+ provider: string;
126
+ model: string;
127
+ responseModel?: string;
128
+ responseId?: string;
129
+ usage: ModelStepUsage;
130
+ stopReason: "stop" | "length" | "toolUse" | "error";
131
+ errorMessage?: string;
132
+ diagnostics?: JsonValue[];
133
+ timestamp: number;
134
+ content: ModelStepBlock[];
135
+ };
136
+ "model.tool-result.presented": {
137
+ turnId: TurnId;
138
+ toolCallId: string;
139
+ toolName: string;
140
+ isError: boolean;
141
+ /** Absent when the model saw exactly the text of the call's tool.call.completed or tool.call.failed. */
142
+ content?: ModelToolResultPart[];
143
+ };
144
+ "context.compacted": {
145
+ turnId: TurnId;
146
+ summary: string;
147
+ /** The first context event after the cut; everything before it is replaced by the summary. */
148
+ firstKeptEventId: string;
149
+ tokensBefore: number;
150
+ provider: string;
151
+ model: string;
152
+ readFiles: string[];
153
+ modifiedFiles: string[];
154
+ };
87
155
  "model.output.completed": {
88
156
  turnId: TurnId;
89
157
  text: string;
@@ -198,6 +266,10 @@ export const eventTypeMap: Record<EventType, true> = {
198
266
  "turn.input-steered": true,
199
267
  "turn.finished": true,
200
268
  "turn.interrupted": true,
269
+ "model.input.presented": true,
270
+ "model.step.completed": true,
271
+ "model.tool-result.presented": true,
272
+ "context.compacted": true,
201
273
  "model.output.completed": true,
202
274
  "model.output.interrupted": true,
203
275
  "model.reasoning.completed": true,
@@ -215,6 +215,8 @@ export type RunState = {
215
215
  pluginStates: ReadonlyMap<string, PluginState>;
216
216
  actions: ReadonlyMap<ActionId, Action>;
217
217
  artifacts: ReadonlyMap<ArtifactId, Artifact>;
218
+ /** Turns that presented input to the model, with their actor; only they leave model context behind. */
219
+ contextTurns: ReadonlyMap<TurnId, ActorId>;
218
220
  };
219
221
 
220
222
  /** Der Actor, den ein Event betrifft: bei Turn-Enden der Besitzer des Turns, bei Stopp und Neustart der Ziel-Actor, sonst der Schreiber. */
@@ -241,8 +243,8 @@ export const eventSubjectOf = (state: RunState, event: JournalEvent): ActorId =>
241
243
 
242
244
  type Collections = "actors" | "inputs" | "turns" | "subscriptions" | "pluginStates" | "actions" | "artifacts";
243
245
 
244
- /** Die Run-Ansicht für Clients; der Eigentümer bleibt serverseitig und steht nicht in ihr. */
245
- export type RunView = Omit<RunState, Collections | "ownerUserId"> & {
246
+ /** Die Run-Ansicht für Clients; Eigentümer und Kontext-Turns bleiben serverseitig und stehen nicht in ihr. */
247
+ export type RunView = Omit<RunState, Collections | "ownerUserId" | "contextTurns"> & {
246
248
  actors: Actor[];
247
249
  inputs: ActorInput[];
248
250
  turns: Turn[];
@@ -50,6 +50,7 @@ export const forkProjection = (state: RunDraft): RunDraft => ({
50
50
  subscriptions: new Map(state.subscriptions),
51
51
  pluginStates: new Map(state.pluginStates),
52
52
  artifacts: new Map(state.artifacts),
53
+ contextTurns: new Map(state.contextTurns),
53
54
  });
54
55
 
55
56
  const actor = (state: RunDraft, id: string) => {
@@ -129,6 +130,7 @@ const created = (event: Extract<JournalEvent, { type: "run.created" }>): RunDraf
129
130
  pluginStates: new Map(),
130
131
  actions: new Map(),
131
132
  artifacts: new Map(),
133
+ contextTurns: new Map(),
132
134
  });
133
135
 
134
136
  export function applyEvent(
@@ -349,6 +351,13 @@ export function applyEvent(
349
351
  case "model.output.interrupted":
350
352
  case "runtime.output.recorded":
351
353
  case "tool.call.source":
354
+ case "model.step.completed":
355
+ case "model.tool-result.presented":
356
+ case "context.compacted":
357
+ break;
358
+
359
+ case "model.input.presented":
360
+ state.contextTurns.set(event.payload.turnId, event.actorId);
352
361
  break;
353
362
 
354
363
  case "actor.tools.opened": {
@@ -0,0 +1,9 @@
1
+ import type { JournalEvent } from "./events.ts";
2
+
3
+ type ToolResultEvent = Extract<JournalEvent, { type: "tool.call.completed" | "tool.call.failed" }>;
4
+
5
+ /** The text the model sees of a tool call when no hook replaced it and the tool recorded its full output. */
6
+ export const derivedToolResultText = (event: ToolResultEvent): string =>
7
+ event.type === "tool.call.failed"
8
+ ? event.payload.error
9
+ : typeof event.payload.output === "string" ? event.payload.output : JSON.stringify(event.payload.output ?? {});
@@ -1,40 +1,27 @@
1
- import type { InlineExtension } from "@ragents/agent";
2
- import type { JsonValue } from "../domain/json.ts";
3
- import type { AgentContribution, AgentContributionContext, AgentHookContext } from "../plugin-types.ts";
1
+ import type {
2
+ AgentContribution,
3
+ AgentContributionContext,
4
+ AgentHookContext,
5
+ ModelCallContext,
6
+ ToolCallOutcome,
7
+ ToolResultReplacement,
8
+ } from "../plugin-types.ts";
4
9
 
5
- const hookContextOf = (context: { signal: AbortSignal | undefined; model: { input: readonly string[] } | undefined }): AgentHookContext => ({
6
- signal: context.signal,
7
- modelReadsImages: context.model?.input.includes("image") === true,
8
- });
10
+ /** A hook of a plugin, bound to one agent; the agent driver calls it directly. */
11
+ export interface AgentHook {
12
+ readonly id: string;
13
+ readonly beforeModelCall?: (call: ModelCallContext) => string | undefined | Promise<string | undefined>;
14
+ readonly afterToolCall?: (outcome: ToolCallOutcome, call: AgentHookContext) =>
15
+ ToolResultReplacement | undefined | Promise<ToolResultReplacement | undefined>;
16
+ }
9
17
 
10
- /** The one place that turns the hooks of a plugin into an extension of the agent runtime; plugins never see the runtime. */
11
- export const agentHookExtension = (contribution: AgentContribution, agent: AgentContributionContext): InlineExtension => ({
12
- name: contribution.id,
13
- factory: (api) => {
14
- const { beforeModelCall, afterToolCall } = contribution;
15
- if (beforeModelCall) {
16
- api.on("context", async (event, context) => {
17
- const stored = context.sessionManager.getBranch()
18
- .filter((entry) => entry.type === "custom" && entry.customType === contribution.id)
19
- .at(-1);
20
- const note = await beforeModelCall(agent, {
21
- ...hookContextOf(context),
22
- kept: stored?.type === "custom" ? stored.data as JsonValue : undefined,
23
- keep: (value) => api.appendEntry(contribution.id, value),
24
- });
25
- if (note === undefined) return undefined;
26
- return { messages: [...event.messages, { role: "custom" as const, customType: contribution.id, content: note, display: false, timestamp: Date.now() }] };
27
- });
28
- }
29
- if (afterToolCall) {
30
- api.on("tool_result", async (event, context) => {
31
- const replacement = await afterToolCall(agent, { toolName: event.toolName, isError: event.isError }, hookContextOf(context));
32
- if (replacement === undefined) return undefined;
33
- return {
34
- content: replacement.content.map((part) => ({ ...part })),
35
- ...(replacement.isError === undefined ? {} : { isError: replacement.isError }),
36
- };
37
- });
38
- }
39
- },
40
- });
18
+ /** The one place that binds the hooks of a plugin to an agent; plugins never see the agent runtime. */
19
+ export const agentHookOf = (contribution: AgentContribution, agent: AgentContributionContext): AgentHook => {
20
+ const { id, beforeModelCall, afterToolCall } = contribution;
21
+
22
+ return {
23
+ id,
24
+ ...(beforeModelCall ? { beforeModelCall: (call: ModelCallContext) => beforeModelCall(agent, call) } : {}),
25
+ ...(afterToolCall ? { afterToolCall: (outcome: ToolCallOutcome, call: AgentHookContext) => afterToolCall(agent, outcome, call) } : {}),
26
+ };
27
+ };