@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
@@ -1,20 +1,10 @@
1
- /**
2
- * Context compaction for long sessions.
3
- *
4
- * Pure functions for compaction logic. The session manager handles I/O,
5
- * and after compaction the session is reloaded.
6
- */
1
+ /** Context compaction over the context log of one agent; pure functions, the caller persists the result. */
7
2
 
8
3
  import type { AgentMessage, StreamFn, ThinkingLevel } from "../../loop/index.ts";
9
4
  import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@ragents/ai";
10
5
  import { completeSimple } from "@ragents/ai";
6
+ import { type ContextCompaction, type ContextLogEntry, contextEntryMessages, contextMessages } from "../context-log.ts";
11
7
  import { convertToLlm } from "../messages.ts";
12
- import {
13
- buildSessionContext,
14
- type CompactionEntry,
15
- type SessionEntry,
16
- sessionEntryToContextMessages,
17
- } from "../session-manager.ts";
18
8
  import {
19
9
  computeFileLists,
20
10
  createFileOps,
@@ -26,72 +16,29 @@ import {
26
16
  collectUserAttachments,
27
17
  } from "./utils.ts";
28
18
 
29
- // ============================================================================
30
- // File Operation Tracking
31
- // ============================================================================
32
-
33
- /** Details stored in CompactionEntry.details for file tracking */
34
- export interface CompactionDetails {
35
- readFiles: string[];
36
- modifiedFiles: string[];
37
- }
38
-
39
- /**
40
- * Extract file operations from messages and previous compaction entries.
41
- */
42
- function extractFileOperations(
43
- messages: AgentMessage[],
44
- entries: SessionEntry[],
45
- prevCompactionIndex: number,
46
- ): FileOperations {
19
+ /** Earlier compaction lists plus the file operations of the summarized messages. */
20
+ function extractFileOperations(messages: AgentMessage[], previous: ContextCompaction | undefined): FileOperations {
47
21
  const fileOps = createFileOps();
48
-
49
- // Collect from previous compaction's details (if agent-generated)
50
- if (prevCompactionIndex >= 0) {
51
- const prevCompaction = entries[prevCompactionIndex] as CompactionEntry;
52
- if (!prevCompaction.fromHook && prevCompaction.details) {
53
- // fromHook field kept for session file compatibility
54
- const details = prevCompaction.details as CompactionDetails;
55
- if (Array.isArray(details.readFiles)) {
56
- for (const f of details.readFiles) fileOps.read.add(f);
57
- }
58
- if (Array.isArray(details.modifiedFiles)) {
59
- for (const f of details.modifiedFiles) fileOps.edited.add(f);
60
- }
61
- }
62
- }
63
-
64
- // Extract from tool calls in messages
22
+ for (const file of previous?.readFiles ?? []) fileOps.read.add(file);
23
+ for (const file of previous?.modifiedFiles ?? []) fileOps.edited.add(file);
65
24
  for (const msg of messages) {
66
25
  extractFileOpsFromMessage(msg, fileOps);
67
26
  }
68
-
69
27
  return fileOps;
70
28
  }
71
29
 
72
- // ============================================================================
73
- // Message Extraction
74
- // ============================================================================
75
-
76
- /**
77
- * Extract AgentMessage from an entry if it produces one.
78
- * Returns undefined for entries that don't contribute to LLM context.
79
- */
80
- function getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | undefined {
81
- if (entry.type === "compaction") {
82
- return undefined;
83
- }
84
- return sessionEntryToContextMessages(entry)[0];
30
+ function messageOf(entry: ContextLogEntry): AgentMessage | undefined {
31
+ return entry.kind === "message" ? entry.message : undefined;
85
32
  }
86
33
 
87
- /** Result from compact() - SessionManager adds uuid/parentUuid when saving */
88
- export interface CompactionResult<T = unknown> {
34
+ /** Result of compact(); the caller records it as a compaction entry. */
35
+ export interface CompactionResult {
89
36
  summary: string;
90
- firstKeptEntryId: string;
37
+ firstKeptId: string;
91
38
  tokensBefore: number;
92
39
  estimatedTokensAfter?: number;
93
- /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
94
- details?: T;
40
+ readFiles: string[];
41
+ modifiedFiles: string[];
95
42
  }
96
43
 
97
44
  // ============================================================================
@@ -283,38 +230,24 @@ function isTurnStartMessage(message: AgentMessage): boolean {
283
230
  return false;
284
231
  }
285
232
 
286
- function isTurnStartEntry(entry: SessionEntry): boolean {
287
- if (entry.type === "compaction") {
288
- return false;
289
- }
290
- return sessionEntryToContextMessages(entry).some(isTurnStartMessage);
233
+ function isTurnStartEntry(entry: ContextLogEntry): boolean {
234
+ return entry.kind === "message" && isTurnStartMessage(entry.message);
291
235
  }
292
236
 
293
- /**
294
- * Find valid cut points: indices of context-visible user-like or assistant messages.
295
- * Never cut at tool results (they must follow their tool call).
296
- * When we cut at an assistant message with tool calls, its tool results follow it
297
- * and will be kept.
298
- */
299
- function findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {
237
+ /** User-like or assistant messages, never a tool result, since it must follow its call. */
238
+ function findValidCutPoints(entries: ContextLogEntry[], startIndex: number, endIndex: number): number[] {
300
239
  const cutPoints: number[] = [];
301
240
  for (let i = startIndex; i < endIndex; i++) {
302
241
  const entry = entries[i];
303
- if (entry.type === "compaction") {
304
- continue;
305
- }
306
- if (sessionEntryToContextMessages(entry).some(isCutPointMessage)) {
242
+ if (entry.kind === "message" && isCutPointMessage(entry.message)) {
307
243
  cutPoints.push(i);
308
244
  }
309
245
  }
310
246
  return cutPoints;
311
247
  }
312
248
 
313
- /**
314
- * Find the context-visible user-role message that starts the turn containing the given entry index.
315
- * Returns -1 if no turn start found before the index.
316
- */
317
- export function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {
249
+ /** The user-like message that starts the turn containing the given index, or -1. */
250
+ export function findTurnStartIndex(entries: ContextLogEntry[], entryIndex: number, startIndex: number): number {
318
251
  for (let i = entryIndex; i >= startIndex; i--) {
319
252
  if (isTurnStartEntry(entries[i])) {
320
253
  return i;
@@ -332,24 +265,9 @@ export interface CutPointResult {
332
265
  isSplitTurn: boolean;
333
266
  }
334
267
 
335
- /**
336
- * Find the cut point in session entries that keeps approximately `keepRecentTokens`.
337
- *
338
- * Algorithm: Walk backwards from newest, accumulating estimated message sizes.
339
- * Stop when we've accumulated >= keepRecentTokens. Cut at that point.
340
- *
341
- * Can cut at user OR assistant messages (never tool results). When cutting at an
342
- * assistant message with tool calls, its tool results come after and will be kept.
343
- *
344
- * Returns CutPointResult with:
345
- * - firstKeptEntryIndex: the entry index to start keeping from
346
- * - turnStartIndex: if cutting mid-turn, the user message that started that turn
347
- * - isSplitTurn: whether we're cutting in the middle of a turn
348
- *
349
- * Only considers entries between `startIndex` and `endIndex` (exclusive).
350
- */
268
+ /** The valid cut point that keeps about `keepRecentTokens` of the entries between `startIndex` and `endIndex`, counted from the newest. */
351
269
  export function findCutPoint(
352
- entries: SessionEntry[],
270
+ entries: ContextLogEntry[],
353
271
  startIndex: number,
354
272
  endIndex: number,
355
273
  keepRecentTokens: number,
@@ -360,22 +278,15 @@ export function findCutPoint(
360
278
  return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
361
279
  }
362
280
 
363
- // Walk backwards from newest, accumulating estimated message sizes
364
281
  let accumulatedTokens = 0;
365
- let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
282
+ let cutIndex = cutPoints[0];
366
283
 
367
284
  for (let i = endIndex - 1; i >= startIndex; i--) {
368
- const entry = entries[i];
369
- const messageTokens = sessionEntryToContextMessages(entry).reduce(
370
- (sum, message) => sum + estimateTokens(message),
371
- 0,
372
- );
285
+ const messageTokens = contextEntryMessages(entries[i]).reduce((sum, message) => sum + estimateTokens(message), 0);
373
286
  if (messageTokens === 0) continue;
374
287
  accumulatedTokens += messageTokens;
375
288
 
376
- // Check if we've exceeded the budget
377
289
  if (accumulatedTokens >= keepRecentTokens) {
378
- // Find the closest valid cut point at or after this entry
379
290
  for (let c = 0; c < cutPoints.length; c++) {
380
291
  if (cutPoints[c] >= i) {
381
292
  cutIndex = cutPoints[c];
@@ -386,17 +297,6 @@ export function findCutPoint(
386
297
  }
387
298
  }
388
299
 
389
- // Scan backwards from cutIndex to include adjacent metadata entries that do not affect context.
390
- while (cutIndex > startIndex) {
391
- const prevEntry = entries[cutIndex - 1];
392
- // Stop at compaction boundaries or context-visible entries.
393
- if (prevEntry.type === "compaction" || sessionEntryToContextMessages(prevEntry).length > 0) {
394
- break;
395
- }
396
- cutIndex--;
397
- }
398
-
399
- // Determine if this is a split turn
400
300
  const cutEntry = entries[cutIndex];
401
301
  const startsTurn = isTurnStartEntry(cutEntry);
402
302
  const turnStartIndex = startsTurn ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
@@ -583,12 +483,12 @@ export async function generateSummary(
583
483
  }
584
484
 
585
485
  // ============================================================================
586
- // Compaction Preparation (for extensions)
486
+ // Compaction Preparation
587
487
  // ============================================================================
588
488
 
589
489
  export interface CompactionPreparation {
590
- /** UUID of first entry to keep */
591
- firstKeptEntryId: string;
490
+ /** Id of the first entry to keep */
491
+ firstKeptId: string;
592
492
  /** Messages that will be summarized and discarded */
593
493
  messagesToSummarize: AgentMessage[];
594
494
  /** Messages that will be turned into turn prefix summary (if splitting) */
@@ -600,61 +500,54 @@ export interface CompactionPreparation {
600
500
  previousSummary?: string;
601
501
  /** File operations extracted from messagesToSummarize */
602
502
  fileOps: FileOperations;
603
- /** Compaction settions from settings.jsonl */
604
503
  settings: CompactionSettings;
605
504
  }
606
505
 
506
+ /** Prepares a compaction of the whole context log; undefined when there is nothing to summarize. */
607
507
  export function prepareCompaction(
608
- pathEntries: SessionEntry[],
508
+ entries: ContextLogEntry[],
609
509
  settings: CompactionSettings,
610
510
  ): CompactionPreparation | undefined {
611
- if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
511
+ if (entries.length > 0 && entries[entries.length - 1].kind === "compaction") {
612
512
  return undefined;
613
513
  }
614
514
 
615
515
  let prevCompactionIndex = -1;
616
- for (let i = pathEntries.length - 1; i >= 0; i--) {
617
- if (pathEntries[i].type === "compaction") {
516
+ for (let i = entries.length - 1; i >= 0; i--) {
517
+ if (entries[i].kind === "compaction") {
618
518
  prevCompactionIndex = i;
619
519
  break;
620
520
  }
621
521
  }
622
522
 
623
- let previousSummary: string | undefined;
523
+ const previous = prevCompactionIndex >= 0 ? (entries[prevCompactionIndex] as ContextCompaction) : undefined;
624
524
  let boundaryStart = 0;
625
- if (prevCompactionIndex >= 0) {
626
- const prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;
627
- previousSummary = prevCompaction.summary;
628
- const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);
525
+ if (previous) {
526
+ const firstKeptEntryIndex = entries.findIndex((entry) => entry.id === previous.firstKeptId);
629
527
  boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
630
528
  }
631
- const boundaryEnd = pathEntries.length;
529
+ const boundaryEnd = entries.length;
632
530
 
633
- const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;
531
+ const tokensBefore = estimateContextTokens(contextMessages(entries)).tokens;
634
532
 
635
- const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
636
-
637
- // Get UUID of first kept entry
638
- const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
639
- if (!firstKeptEntry?.id) {
640
- return undefined; // Session needs migration
533
+ const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
534
+ const firstKeptEntry = entries[cutPoint.firstKeptEntryIndex];
535
+ if (!firstKeptEntry) {
536
+ return undefined;
641
537
  }
642
- const firstKeptEntryId = firstKeptEntry.id;
643
538
 
644
539
  const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
645
540
 
646
- // Messages to summarize (will be discarded after summary)
647
541
  const messagesToSummarize: AgentMessage[] = [];
648
542
  for (let i = boundaryStart; i < historyEnd; i++) {
649
- const msg = getMessageFromEntryForCompaction(pathEntries[i]);
543
+ const msg = messageOf(entries[i]);
650
544
  if (msg) messagesToSummarize.push(msg);
651
545
  }
652
546
 
653
- // Messages for turn prefix summary (if splitting a turn)
654
547
  const turnPrefixMessages: AgentMessage[] = [];
655
548
  if (cutPoint.isSplitTurn) {
656
549
  for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
657
- const msg = getMessageFromEntryForCompaction(pathEntries[i]);
550
+ const msg = messageOf(entries[i]);
658
551
  if (msg) turnPrefixMessages.push(msg);
659
552
  }
660
553
  }
@@ -663,10 +556,7 @@ export function prepareCompaction(
663
556
  return undefined;
664
557
  }
665
558
 
666
- // Extract file operations from messages and previous compaction
667
- const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
668
-
669
- // Also extract file ops from turn prefix if splitting
559
+ const fileOps = extractFileOperations(messagesToSummarize, previous);
670
560
  if (cutPoint.isSplitTurn) {
671
561
  for (const msg of turnPrefixMessages) {
672
562
  extractFileOpsFromMessage(msg, fileOps);
@@ -674,12 +564,12 @@ export function prepareCompaction(
674
564
  }
675
565
 
676
566
  return {
677
- firstKeptEntryId,
567
+ firstKeptId: firstKeptEntry.id,
678
568
  messagesToSummarize,
679
569
  turnPrefixMessages,
680
570
  isSplitTurn: cutPoint.isSplitTurn,
681
571
  tokensBefore,
682
- previousSummary,
572
+ previousSummary: previous?.summary,
683
573
  fileOps,
684
574
  settings,
685
575
  };
@@ -704,13 +594,7 @@ Summarize the prefix to provide context for the retained suffix:
704
594
 
705
595
  Be concise. Focus on what's needed to understand the kept suffix.`;
706
596
 
707
- /**
708
- * Generate summaries for compaction using prepared data.
709
- * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.
710
- *
711
- * @param preparation - Pre-calculated preparation from prepareCompaction()
712
- * @param customInstructions - Optional custom focus for the summary
713
- */
597
+ /** Summarizes a prepared compaction; the caller records the result. */
714
598
  export async function compact(
715
599
  preparation: CompactionPreparation,
716
600
  model: Model<any>,
@@ -723,7 +607,7 @@ export async function compact(
723
607
  env?: Record<string, string>,
724
608
  ): Promise<CompactionResult> {
725
609
  const {
726
- firstKeptEntryId,
610
+ firstKeptId,
727
611
  messagesToSummarize,
728
612
  turnPrefixMessages,
729
613
  isSplitTurn,
@@ -783,20 +667,10 @@ export async function compact(
783
667
  );
784
668
  }
785
669
 
786
- // Compute file lists and append to summary
787
670
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
788
671
  summary += formatFileOperations(readFiles, modifiedFiles);
789
672
 
790
- if (!firstKeptEntryId) {
791
- throw new Error("First kept entry has no UUID - session may need migration");
792
- }
793
-
794
- return {
795
- summary,
796
- firstKeptEntryId,
797
- tokensBefore,
798
- details: { readFiles, modifiedFiles } as CompactionDetails,
799
- };
673
+ return { summary, firstKeptId, tokensBefore, readFiles, modifiedFiles };
800
674
  }
801
675
 
802
676
  /**
@@ -0,0 +1,49 @@
1
+ import type { AgentMessage } from "../loop/index.ts";
2
+ import { createCompactionSummaryMessage } from "./messages.ts";
3
+
4
+ /** A compaction replaces every entry before its first kept entry with its summary. */
5
+ export interface ContextCompaction {
6
+ kind: "compaction";
7
+ id: string;
8
+ summary: string;
9
+ firstKeptId: string;
10
+ tokensBefore: number;
11
+ timestamp: number;
12
+ readFiles: string[];
13
+ modifiedFiles: string[];
14
+ }
15
+
16
+ /** One entry of an agent's model context in the order the conversation produced it. */
17
+ export type ContextLogEntry = { kind: "message"; id: string; message: AgentMessage } | ContextCompaction;
18
+
19
+ export function latestCompaction(entries: readonly ContextLogEntry[]): ContextCompaction | undefined {
20
+ for (let index = entries.length - 1; index >= 0; index--) {
21
+ const entry = entries[index];
22
+ if (entry.kind === "compaction") return entry;
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ /** The entries the model sees: the latest compaction, the entries it keeps, and everything after it. */
28
+ export function activeContextEntries(entries: readonly ContextLogEntry[]): ContextLogEntry[] {
29
+ const compaction = latestCompaction(entries);
30
+ if (!compaction) return [...entries];
31
+ const compactionIndex = entries.indexOf(compaction);
32
+ const firstKeptIndex = entries.findIndex((entry, index) => index < compactionIndex && entry.id === compaction.firstKeptId);
33
+ return [
34
+ compaction,
35
+ ...(firstKeptIndex < 0 ? [] : entries.slice(firstKeptIndex, compactionIndex)),
36
+ ...entries.slice(compactionIndex + 1),
37
+ ];
38
+ }
39
+
40
+ export function contextEntryMessages(entry: ContextLogEntry): AgentMessage[] {
41
+ return entry.kind === "message"
42
+ ? [entry.message]
43
+ : [createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp)];
44
+ }
45
+
46
+ /** The messages of the model context, compaction-aware; the order is the order of the log. */
47
+ export function contextMessages(entries: readonly ContextLogEntry[]): AgentMessage[] {
48
+ return activeContextEntries(entries).flatMap(contextEntryMessages);
49
+ }
@@ -11,7 +11,7 @@ export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this p
11
11
  export const COMPACTION_SUMMARY_SUFFIX = `
12
12
  </summary>`;
13
13
 
14
- /** Message that an extension injects into the context of a model call. */
14
+ /** A hidden note that a hook adds to one model call; it is not part of the context. */
15
15
  export interface CustomMessage<T = unknown> {
16
16
  role: "custom";
17
17
  customType: string;
@@ -39,24 +39,17 @@ declare module "../loop/types.ts" {
39
39
  export function createCompactionSummaryMessage(
40
40
  summary: string,
41
41
  tokensBefore: number,
42
- timestamp: string,
42
+ timestamp: number,
43
43
  ): CompactionSummaryMessage {
44
44
  return {
45
45
  role: "compactionSummary",
46
46
  summary: summary,
47
47
  tokensBefore,
48
- timestamp: new Date(timestamp).getTime(),
48
+ timestamp,
49
49
  };
50
50
  }
51
51
 
52
- /**
53
- * Transform AgentMessages (including custom types) to LLM-compatible Messages.
54
- *
55
- * This is used by:
56
- * - Agent's transormToLlm option (for prompt calls and queued messages)
57
- * - Compaction's generateSummary (for summarization)
58
- * - Custom extensions and tools
59
- */
52
+ /** Transform AgentMessages, including hook notes and compaction summaries, to LLM messages. */
60
53
  export function convertToLlm(messages: AgentMessage[]): Message[] {
61
54
  return messages
62
55
  .map((m): Message | undefined => {
@@ -67,6 +60,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
67
60
  role: "user",
68
61
  content,
69
62
  timestamp: m.timestamp,
63
+ transient: true,
70
64
  };
71
65
  }
72
66
  case "compactionSummary":
@@ -0,0 +1,29 @@
1
+ /** The form of the read, write, edit and bash tools; the host runs them in its workspace executor. */
2
+
3
+ import type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../loop/index.ts";
4
+ import type { Static, TSchema } from "typebox";
5
+
6
+ export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode };
7
+
8
+ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown> {
9
+ name: string;
10
+ label: string;
11
+ description: string;
12
+ parameters: TParams;
13
+ /** Optional compatibility shim to prepare raw tool call arguments before schema validation. */
14
+ prepareArguments?: (args: unknown) => Static<TParams>;
15
+ executionMode?: ToolExecutionMode;
16
+ execute(
17
+ toolCallId: string,
18
+ params: Static<TParams>,
19
+ signal: AbortSignal | undefined,
20
+ onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
21
+ ): Promise<AgentToolResult<TDetails>>;
22
+ }
23
+
24
+ /** Preserve parameter inference for standalone tool definitions. */
25
+ export function defineTool<TParams extends TSchema, TDetails = unknown>(
26
+ tool: ToolDefinition<TParams, TDetails>,
27
+ ): ToolDefinition<TParams, TDetails> {
28
+ return tool;
29
+ }
@@ -5,7 +5,7 @@ import { Type } from "typebox";
5
5
  import { waitForChildProcess } from "../../utils/child-process.ts";
6
6
  import { resolvePath } from "../../utils/paths.ts";
7
7
  import { getShellConfig, killProcessTree } from "../../utils/shell.ts";
8
- import type { ToolDefinition } from "../extensions/types.ts";
8
+ import type { ToolDefinition } from "../tool-definition.ts";
9
9
  import { OutputAccumulator } from "./output-accumulator.ts";
10
10
  import { DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.ts";
11
11
 
@@ -64,12 +64,7 @@ export interface BashOperations {
64
64
  ) => Promise<{ exitCode: number | null }>;
65
65
  }
66
66
 
67
- /**
68
- * Create bash operations using the built-in local shell execution backend.
69
- *
70
- * This is useful for extensions that intercept user_bash and still want the
71
- * standard local shell behavior while wrapping or rewriting commands.
72
- */
67
+ /** Bash operations on the built-in local shell. */
73
68
  export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {
74
69
  return {
75
70
  exec: async (command, cwd, { onData, signal, timeout, env }) => {
@@ -168,14 +163,12 @@ export function createBashToolDefinition(
168
163
  name: "bash",
169
164
  label: "bash",
170
165
  description: `Execute a bash command in the working directory, or in the folder given as cwd. Returns stdout and stderr; a nonzero exit code is reported at the end of the result (for example grep without a match), not as a tool error. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${BASH_MAX_BYTES / 1024}KB (whichever is hit first), and lines longer than ${BASH_MAX_LINE_CHARS} characters are shortened. If anything was cut, the full output is saved to a temp file. Optionally provide a timeout in seconds.`,
171
- promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
172
166
  parameters: bashSchema,
173
167
  async execute(
174
168
  _toolCallId,
175
169
  { command, timeout, cwd: folder }: { command: string; timeout?: number; cwd?: string },
176
170
  signal?: AbortSignal,
177
171
  onUpdate?,
178
- _ctx?,
179
172
  ) {
180
173
  const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
181
174
  const spawnContext = resolveSpawnContext(resolvedCommand, folder === undefined ? cwd : resolvePath(folder, cwd), spawnHook);
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { constants } from "fs";
3
3
  import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
4
4
  import { type Static, Type } from "typebox";
5
- import type { ToolDefinition } from "../extensions/types.ts";
5
+ import type { ToolDefinition } from "../tool-definition.ts";
6
6
  import { applyEditsToNormalizedContent, detectLineEnding, type Edit, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom } from "./edit-diff.ts";
7
7
  import { withFileMutationQueue } from "./file-mutation-queue.ts";
8
8
  import { resolveToCwd } from "./path-utils.ts";
@@ -130,18 +130,9 @@ export function createEditToolDefinition(
130
130
  label: "edit",
131
131
  description:
132
132
  "Edit a single file using exact text replacement. Read the file first; the edit is rejected if the file changed since that read. Every edits[].oldText must match a unique, non-overlapping region of the original file, or be anchored with occurrence, nearLine or replaceAll. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
133
- promptSnippet:
134
- "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
135
- promptGuidelines: [
136
- "Use edit for precise changes (edits[].oldText must match exactly)",
137
- "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
138
- "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
139
- "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
140
- "If oldText is not unique, the error lists every occurrence with its line number. Pick one with occurrence (1-based) or nearLine instead of padding oldText with more context, and use replaceAll only when every occurrence must change.",
141
- ],
142
133
  parameters: editSchema,
143
134
  prepareArguments: prepareEditArguments,
144
- async execute(_toolCallId, input: EditToolInput, signal?: AbortSignal, _onUpdate?, _ctx?) {
135
+ async execute(_toolCallId, input: EditToolInput, signal?: AbortSignal) {
145
136
  const { path, edits } = validateEditInput(input);
146
137
  const absolutePath = resolveToCwd(path, cwd);
147
138
 
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { Api, ImageContent, Model, TextContent } from "@ragents/ai";
2
+ import type { ImageContent, TextContent } from "@ragents/ai";
3
3
  import { constants } from "fs";
4
4
  import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
5
5
  import { Type } from "typebox";
6
6
  import { processImage } from "../../utils/image-process.ts";
7
7
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.ts";
8
- import type { ToolDefinition } from "../extensions/types.ts";
8
+ import type { ToolDefinition } from "../tool-definition.ts";
9
9
  import { BASH_MAX_LINE_CHARS } from "./bash.ts";
10
10
  import { resolveReadPathAsync } from "./path-utils.ts";
11
11
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
@@ -48,13 +48,6 @@ export interface ReadToolOptions {
48
48
  operations?: ReadOperations;
49
49
  }
50
50
 
51
- function getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {
52
- if (!model || model.input.includes("image")) {
53
- return undefined;
54
- }
55
- return "[Current model does not support images. The image will be omitted from this request.]";
56
- }
57
-
58
51
  export function createReadToolDefinition(
59
52
  cwd: string,
60
53
  options?: ReadToolOptions,
@@ -65,15 +58,11 @@ export function createReadToolDefinition(
65
58
  name: "read",
66
59
  label: "read",
67
60
  description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
68
- promptSnippet: "Read file contents",
69
- promptGuidelines: ["Use read to examine files instead of cat or sed."],
70
61
  parameters: readSchema,
71
62
  async execute(
72
63
  _toolCallId,
73
64
  { path, offset, limit }: { path: string; offset?: number; limit?: number },
74
65
  signal?: AbortSignal,
75
- _onUpdate?,
76
- ctx?,
77
66
  ) {
78
67
  return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(
79
68
  (resolve, reject) => {
@@ -100,18 +89,14 @@ export function createReadToolDefinition(
100
89
  const contentHash = createHash("sha256").update(buffer).digest("hex");
101
90
  let content: (TextContent | ImageContent)[];
102
91
  let details: ReadToolDetails | undefined;
103
- const nonVisionImageNote = getNonVisionImageNote(ctx?.model);
104
92
  if (mimeType) {
105
93
  // Read image as binary.
106
94
  const processed = await processImage(buffer, mimeType, { autoResizeImages });
107
95
  if (!processed.ok) {
108
- let textNote = `Read image file [${mimeType}]\n${processed.message}`;
109
- if (nonVisionImageNote) textNote += `\n${nonVisionImageNote}`;
110
- content = [{ type: "text", text: textNote }];
96
+ content = [{ type: "text", text: `Read image file [${mimeType}]\n${processed.message}` }];
111
97
  } else {
112
98
  let textNote = `Read image file [${processed.mimeType}]`;
113
99
  if (processed.hints.length > 0) textNote += `\n${processed.hints.join("\n")}`;
114
- if (nonVisionImageNote) textNote += `\n${nonVisionImageNote}`;
115
100
  content = [
116
101
  { type: "text", text: textNote },
117
102
  { type: "image", data: processed.data, mimeType: processed.mimeType },
@@ -1,7 +1,7 @@
1
1
  import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
2
2
  import { dirname } from "path";
3
3
  import { Type } from "typebox";
4
- import type { ToolDefinition } from "../extensions/types.ts";
4
+ import type { ToolDefinition } from "../tool-definition.ts";
5
5
  import { withFileMutationQueue } from "./file-mutation-queue.ts";
6
6
  import { resolveToCwd } from "./path-utils.ts";
7
7
 
@@ -42,15 +42,11 @@ export function createWriteToolDefinition(
42
42
  label: "write",
43
43
  description:
44
44
  "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
45
- promptSnippet: "Create or overwrite files",
46
- promptGuidelines: ["Use write only for new files or complete rewrites."],
47
45
  parameters: writeSchema,
48
46
  async execute(
49
47
  _toolCallId,
50
48
  { path, content }: { path: string; content: string },
51
49
  signal?: AbortSignal,
52
- _onUpdate?,
53
- _ctx?,
54
50
  ) {
55
51
  const absolutePath = resolveToCwd(path, cwd);
56
52
  const dir = dirname(absolutePath);