conversationalist 0.0.10 → 0.0.11

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 (67) hide show
  1. package/README.md +89 -15
  2. package/dist/adapters/anthropic/index.d.ts.map +1 -1
  3. package/dist/adapters/anthropic/index.js +255 -2
  4. package/dist/adapters/anthropic/index.js.map +8 -4
  5. package/dist/adapters/gemini/index.d.ts.map +1 -1
  6. package/dist/adapters/gemini/index.js +255 -3
  7. package/dist/adapters/gemini/index.js.map +8 -4
  8. package/dist/adapters/openai/index.d.ts.map +1 -1
  9. package/dist/adapters/openai/index.js +247 -3
  10. package/dist/adapters/openai/index.js.map +8 -4
  11. package/dist/context.d.ts +6 -0
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/conversation/append.d.ts +5 -0
  14. package/dist/conversation/append.d.ts.map +1 -1
  15. package/dist/conversation/create.d.ts +9 -0
  16. package/dist/conversation/create.d.ts.map +1 -1
  17. package/dist/conversation/index.d.ts +8 -3
  18. package/dist/conversation/index.d.ts.map +1 -1
  19. package/dist/conversation/integrity.d.ts +16 -0
  20. package/dist/conversation/integrity.d.ts.map +1 -0
  21. package/dist/conversation/modify.d.ts +8 -2
  22. package/dist/conversation/modify.d.ts.map +1 -1
  23. package/dist/conversation/serialization.d.ts +0 -17
  24. package/dist/conversation/serialization.d.ts.map +1 -1
  25. package/dist/conversation/system-messages.d.ts.map +1 -1
  26. package/dist/conversation/tool-interactions.d.ts +45 -0
  27. package/dist/conversation/tool-interactions.d.ts.map +1 -0
  28. package/dist/conversation/transform.d.ts.map +1 -1
  29. package/dist/conversation/validation.d.ts +8 -0
  30. package/dist/conversation/validation.d.ts.map +1 -0
  31. package/dist/errors.d.ts +6 -1
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/export/index.js +249 -12
  34. package/dist/export/index.js.map +10 -6
  35. package/dist/history.d.ts +4 -1
  36. package/dist/history.d.ts.map +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +604 -231
  40. package/dist/index.js.map +24 -13
  41. package/dist/markdown/index.js +595 -215
  42. package/dist/markdown/index.js.map +22 -12
  43. package/dist/schemas/index.js +35 -19
  44. package/dist/schemas/index.js.map +3 -3
  45. package/dist/schemas.d.ts +15 -35
  46. package/dist/schemas.d.ts.map +1 -1
  47. package/dist/streaming.d.ts.map +1 -1
  48. package/dist/types.d.ts +0 -4
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/utilities/index.d.ts +0 -1
  51. package/dist/utilities/index.d.ts.map +1 -1
  52. package/dist/utilities/markdown.d.ts.map +1 -1
  53. package/dist/utilities/tool-calls.d.ts +2 -6
  54. package/dist/utilities/tool-calls.d.ts.map +1 -1
  55. package/dist/utilities/tool-results.d.ts.map +1 -1
  56. package/dist/utilities/transient.d.ts.map +1 -1
  57. package/dist/utilities.d.ts +0 -1
  58. package/dist/utilities.d.ts.map +1 -1
  59. package/dist/versioning/index.d.ts +0 -1
  60. package/dist/versioning/index.d.ts.map +1 -1
  61. package/dist/versioning/index.js +1 -52
  62. package/dist/versioning/index.js.map +4 -5
  63. package/dist/with-conversation.d.ts +4 -1
  64. package/dist/with-conversation.d.ts.map +1 -1
  65. package/package.json +6 -4
  66. package/dist/conversation.d.ts +0 -109
  67. package/dist/conversation.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -23,6 +23,12 @@ function copyContent(content) {
23
23
  }
24
24
  // src/schemas.ts
25
25
  import { z } from "zod";
26
+ var isPlainObject = (value) => {
27
+ if (!value || typeof value !== "object")
28
+ return false;
29
+ const prototype = Reflect.getPrototypeOf(value);
30
+ return prototype === Object.prototype || prototype === null;
31
+ };
26
32
  function toMultiModalContent(value) {
27
33
  const result = { type: value.type };
28
34
  if (value.text !== undefined)
@@ -33,14 +39,28 @@ function toMultiModalContent(value) {
33
39
  result.mimeType = value.mimeType;
34
40
  return result;
35
41
  }
36
- var jsonValueSchema = z.lazy(() => z.union([
37
- z.string(),
38
- z.number(),
39
- z.boolean(),
40
- z.null(),
41
- z.array(jsonValueSchema),
42
- z.record(z.string(), jsonValueSchema)
43
- ]));
42
+ var jsonValueSchema = z.lazy(() => {
43
+ const jsonObjectSchema = z.preprocess((value, ctx) => {
44
+ if (!isPlainObject(value)) {
45
+ ctx.addIssue({
46
+ code: z.ZodIssueCode.custom,
47
+ message: "expected a plain object"
48
+ });
49
+ return z.NEVER;
50
+ }
51
+ return value;
52
+ }, z.record(z.string(), jsonValueSchema));
53
+ return z.union([
54
+ z.string(),
55
+ z.number().refine((value) => Number.isFinite(value), {
56
+ message: "expected a finite number"
57
+ }),
58
+ z.boolean(),
59
+ z.null(),
60
+ z.array(jsonValueSchema),
61
+ jsonObjectSchema
62
+ ]);
63
+ });
44
64
  var multiModalContentSchema = z.discriminatedUnion("type", [
45
65
  z.object({
46
66
  type: z.literal("text"),
@@ -66,16 +86,12 @@ var toolCallSchema = z.object({
66
86
  id: z.string(),
67
87
  name: z.string(),
68
88
  arguments: jsonValueSchema
69
- });
89
+ }).strict();
70
90
  var toolResultSchema = z.object({
71
91
  callId: z.string(),
72
92
  outcome: z.enum(["success", "error"]),
73
- content: jsonValueSchema,
74
- toolCallId: z.string().optional(),
75
- toolName: z.string().optional(),
76
- result: jsonValueSchema.optional(),
77
- error: z.string().optional()
78
- });
93
+ content: jsonValueSchema
94
+ }).strict();
79
95
  var tokenUsageSchema = z.object({
80
96
  prompt: z.number().int().min(0),
81
97
  completion: z.number().int().min(0),
@@ -90,7 +106,7 @@ var messageInputSchema = z.object({
90
106
  toolResult: toolResultSchema.optional(),
91
107
  tokenUsage: tokenUsageSchema.optional(),
92
108
  goalCompleted: z.boolean().optional()
93
- });
109
+ }).strict();
94
110
  var messageSchema = z.object({
95
111
  id: z.string(),
96
112
  role: messageRoleSchema,
@@ -103,7 +119,7 @@ var messageSchema = z.object({
103
119
  toolResult: toolResultSchema.optional(),
104
120
  tokenUsage: tokenUsageSchema.optional(),
105
121
  goalCompleted: z.boolean().optional()
106
- }).loose();
122
+ }).strict();
107
123
  var conversationStatusSchema = z.enum([
108
124
  "active",
109
125
  "archived",
@@ -120,7 +136,7 @@ var conversationShape = {
120
136
  createdAt: z.string(),
121
137
  updatedAt: z.string()
122
138
  };
123
- var conversationSchema = z.object(conversationShape);
139
+ var conversationSchema = z.object(conversationShape).strict();
124
140
  // src/utilities/content.ts
125
141
  function toMultiModalArray(input) {
126
142
  if (typeof input === "string")
@@ -203,6 +219,160 @@ function pairToolCallsWithResults(messages) {
203
219
  }
204
220
  return pairs;
205
221
  }
222
+ // src/errors.ts
223
+ class ConversationalistError extends Error {
224
+ code;
225
+ context;
226
+ cause;
227
+ constructor(code, message, options) {
228
+ super(message);
229
+ this.name = "ConversationalistError";
230
+ this.code = code;
231
+ this.context = options?.context;
232
+ this.cause = options?.cause;
233
+ if (Error.captureStackTrace) {
234
+ Error.captureStackTrace(this, ConversationalistError);
235
+ }
236
+ }
237
+ toDetailedString() {
238
+ const parts = [`[${this.code}] ${this.message}`];
239
+ if (this.context && Object.keys(this.context).length > 0) {
240
+ parts.push(`Context: ${JSON.stringify(this.context, null, 2)}`);
241
+ }
242
+ if (this.cause) {
243
+ parts.push(`Caused by: ${this.cause.message}`);
244
+ }
245
+ return parts.join(`
246
+ `);
247
+ }
248
+ }
249
+ function createLockedError(conversationId) {
250
+ return new ConversationalistError("error:locked", `conversation ${conversationId} is locked (concurrent modification detected)`, { context: { conversationId } });
251
+ }
252
+ function createInvalidInputError(message, context) {
253
+ return new ConversationalistError("error:invalid-input", message, { context });
254
+ }
255
+ function createInvalidPositionError(expected, actual) {
256
+ return new ConversationalistError("error:invalid-position", `invalid position: expected ${expected}, got ${actual}`, { context: { expected, actual } });
257
+ }
258
+ function createInvalidToolReferenceError(callId) {
259
+ return new ConversationalistError("error:invalid-tool-reference", `tool result references non-existent tool-use: ${callId}`, { context: { callId } });
260
+ }
261
+ function createDuplicateIdError(id) {
262
+ return new ConversationalistError("error:duplicate-id", `conversation with id ${id} already exists`, { context: { id } });
263
+ }
264
+ function createNotFoundError(id) {
265
+ return new ConversationalistError("error:not-found", `conversation with id ${id} not found`, {
266
+ context: { id }
267
+ });
268
+ }
269
+ function createSerializationError(message, cause) {
270
+ return new ConversationalistError("error:serialization", message, { cause });
271
+ }
272
+ function createValidationError(message, context, cause) {
273
+ return new ConversationalistError("error:validation", message, { context, cause });
274
+ }
275
+ function createIntegrityError(message, context) {
276
+ return new ConversationalistError("error:integrity", message, { context });
277
+ }
278
+
279
+ // src/conversation/integrity.ts
280
+ function validateConversationIntegrity(conversation) {
281
+ const issues = [];
282
+ const seenIds = new Set;
283
+ conversation.ids.forEach((id, index) => {
284
+ if (seenIds.has(id)) {
285
+ issues.push({
286
+ code: "integrity:duplicate-message-id",
287
+ message: `duplicate message id in ids: ${id}`,
288
+ data: { id, position: index }
289
+ });
290
+ } else {
291
+ seenIds.add(id);
292
+ }
293
+ if (!conversation.messages[id]) {
294
+ issues.push({
295
+ code: "integrity:missing-message",
296
+ message: `missing message for id ${id}`,
297
+ data: { id, position: index }
298
+ });
299
+ }
300
+ });
301
+ for (const id of Object.keys(conversation.messages)) {
302
+ if (!seenIds.has(id)) {
303
+ issues.push({
304
+ code: "integrity:unlisted-message",
305
+ message: `message ${id} is not listed in ids`,
306
+ data: { id }
307
+ });
308
+ }
309
+ }
310
+ const toolUses = new Map;
311
+ conversation.ids.forEach((id, index) => {
312
+ const message = conversation.messages[id];
313
+ if (!message)
314
+ return;
315
+ if (message.role === "tool-use" && message.toolCall) {
316
+ if (toolUses.has(message.toolCall.id)) {
317
+ issues.push({
318
+ code: "integrity:duplicate-tool-call",
319
+ message: `duplicate toolCall.id ${message.toolCall.id}`,
320
+ data: { toolCallId: message.toolCall.id, messageId: message.id }
321
+ });
322
+ } else {
323
+ toolUses.set(message.toolCall.id, { position: index, messageId: message.id });
324
+ }
325
+ }
326
+ });
327
+ conversation.ids.forEach((id, index) => {
328
+ const message = conversation.messages[id];
329
+ if (!message)
330
+ return;
331
+ if (message.role === "tool-result" && message.toolResult) {
332
+ const toolUse = toolUses.get(message.toolResult.callId);
333
+ if (!toolUse) {
334
+ issues.push({
335
+ code: "integrity:orphan-tool-result",
336
+ message: `tool-result references missing tool-use ${message.toolResult.callId}`,
337
+ data: { callId: message.toolResult.callId, messageId: message.id }
338
+ });
339
+ } else if (toolUse.position >= index) {
340
+ issues.push({
341
+ code: "integrity:tool-result-before-call",
342
+ message: `tool-result ${message.toolResult.callId} occurs before tool-use`,
343
+ data: {
344
+ callId: message.toolResult.callId,
345
+ messageId: message.id,
346
+ toolUseMessageId: toolUse.messageId
347
+ }
348
+ });
349
+ }
350
+ }
351
+ });
352
+ return issues;
353
+ }
354
+ function assertConversationIntegrity(conversation) {
355
+ const issues = validateConversationIntegrity(conversation);
356
+ if (issues.length === 0)
357
+ return;
358
+ throw createIntegrityError("conversation integrity check failed", { issues });
359
+ }
360
+
361
+ // src/conversation/validation.ts
362
+ function assertConversationSafe(conversation) {
363
+ const parsed = conversationSchema.safeParse(conversation);
364
+ if (!parsed.success) {
365
+ throw createValidationError("conversation failed schema validation", {
366
+ issues: parsed.error.issues
367
+ });
368
+ }
369
+ assertConversationIntegrity(conversation);
370
+ }
371
+ function ensureConversationSafe(conversation) {
372
+ assertConversationSafe(conversation);
373
+ return conversation;
374
+ }
375
+
206
376
  // src/utilities/message-store.ts
207
377
  function getOrderedMessages(conversation) {
208
378
  const ordered = [];
@@ -259,7 +429,7 @@ function stripTransientMetadata(conversation) {
259
429
  }
260
430
  return toReadonly(baseMessage);
261
431
  });
262
- return toReadonly({
432
+ return ensureConversationSafe(toReadonly({
263
433
  schemaVersion: conversation.schemaVersion,
264
434
  id: conversation.id,
265
435
  title: conversation.title,
@@ -269,7 +439,7 @@ function stripTransientMetadata(conversation) {
269
439
  messages: toIdRecord(strippedMessages),
270
440
  createdAt: conversation.createdAt,
271
441
  updatedAt: conversation.updatedAt
272
- });
442
+ }));
273
443
  }
274
444
  // src/environment.ts
275
445
  function simpleTokenEstimator(message) {
@@ -302,64 +472,43 @@ function withEnvironment(environment, fn) {
302
472
  return (...args) => fn(...args, environment);
303
473
  }
304
474
 
305
- // src/errors.ts
306
- class ConversationalistError extends Error {
307
- code;
308
- context;
309
- cause;
310
- constructor(code, message, options) {
311
- super(message);
312
- this.name = "ConversationalistError";
313
- this.code = code;
314
- this.context = options?.context;
315
- this.cause = options?.cause;
316
- if (Error.captureStackTrace) {
317
- Error.captureStackTrace(this, ConversationalistError);
318
- }
319
- }
320
- toDetailedString() {
321
- const parts = [`[${this.code}] ${this.message}`];
322
- if (this.context && Object.keys(this.context).length > 0) {
323
- parts.push(`Context: ${JSON.stringify(this.context, null, 2)}`);
324
- }
325
- if (this.cause) {
326
- parts.push(`Caused by: ${this.cause.message}`);
327
- }
328
- return parts.join(`
329
- `);
330
- }
331
- }
332
- function createLockedError(conversationId) {
333
- return new ConversationalistError("error:locked", `conversation ${conversationId} is locked (concurrent modification detected)`, { context: { conversationId } });
334
- }
335
- function createInvalidInputError(message, context) {
336
- return new ConversationalistError("error:invalid-input", message, { context });
337
- }
338
- function createInvalidPositionError(expected, actual) {
339
- return new ConversationalistError("error:invalid-position", `invalid position: expected ${expected}, got ${actual}`, { context: { expected, actual } });
340
- }
341
- function createInvalidToolReferenceError(callId) {
342
- return new ConversationalistError("error:invalid-tool-reference", `tool result references non-existent tool-use: ${callId}`, { context: { callId } });
343
- }
344
- function createDuplicateIdError(id) {
345
- return new ConversationalistError("error:duplicate-id", `conversation with id ${id} already exists`, { context: { id } });
346
- }
347
- function createNotFoundError(id) {
348
- return new ConversationalistError("error:not-found", `conversation with id ${id} not found`, {
349
- context: { id }
350
- });
351
- }
352
- function createSerializationError(message, cause) {
353
- return new ConversationalistError("error:serialization", message, { cause });
354
- }
355
- function createValidationError(message, context, cause) {
356
- return new ConversationalistError("error:validation", message, { context, cause });
357
- }
358
-
359
475
  // src/types.ts
360
476
  var CURRENT_SCHEMA_VERSION = 3;
361
477
 
362
- // src/conversation.ts
478
+ // src/conversation/create.ts
479
+ function createConversation(options, environment) {
480
+ const resolvedEnvironment = resolveConversationEnvironment(environment);
481
+ const now = resolvedEnvironment.now();
482
+ const conv = {
483
+ schemaVersion: CURRENT_SCHEMA_VERSION,
484
+ id: options?.id ?? resolvedEnvironment.randomId(),
485
+ title: options?.title,
486
+ status: options?.status ?? "active",
487
+ metadata: { ...options?.metadata ?? {} },
488
+ ids: [],
489
+ messages: {},
490
+ createdAt: now,
491
+ updatedAt: now
492
+ };
493
+ return ensureConversationSafe(toReadonly(conv));
494
+ }
495
+ function createConversationUnsafe(options, environment) {
496
+ const resolvedEnvironment = resolveConversationEnvironment(environment);
497
+ const now = resolvedEnvironment.now();
498
+ const conv = {
499
+ schemaVersion: CURRENT_SCHEMA_VERSION,
500
+ id: options?.id ?? resolvedEnvironment.randomId(),
501
+ title: options?.title,
502
+ status: options?.status ?? "active",
503
+ metadata: { ...options?.metadata ?? {} },
504
+ ids: [],
505
+ messages: {},
506
+ createdAt: now,
507
+ updatedAt: now
508
+ };
509
+ return toReadonly(conv);
510
+ }
511
+ // src/conversation/tool-tracking.ts
363
512
  var buildToolUseIndex = (messages) => messages.reduce((index, message) => {
364
513
  if (message.role === "tool-use" && message.toolCall) {
365
514
  index.set(message.toolCall.id, { name: message.toolCall.name });
@@ -376,44 +525,37 @@ var assertToolReference = (index, callId) => {
376
525
  throw createInvalidToolReferenceError(callId);
377
526
  }
378
527
  };
528
+
529
+ // src/conversation/append.ts
379
530
  function partitionAppendArgs(args) {
380
- if (args.length === 0) {
531
+ const filtered = args.filter((arg) => arg !== undefined);
532
+ if (filtered.length === 0) {
381
533
  return { inputs: [] };
382
534
  }
383
- const last = args[args.length - 1];
535
+ const last = filtered[filtered.length - 1];
384
536
  if (isConversationEnvironmentParameter(last)) {
385
537
  return {
386
- inputs: args.slice(0, -1),
538
+ inputs: filtered.slice(0, -1),
387
539
  environment: last
388
540
  };
389
541
  }
390
- return { inputs: args };
391
- }
392
- function createConversation(options, environment) {
393
- const resolvedEnvironment = resolveConversationEnvironment(environment);
394
- const now = resolvedEnvironment.now();
395
- const conv = {
396
- schemaVersion: CURRENT_SCHEMA_VERSION,
397
- id: options?.id ?? resolvedEnvironment.randomId(),
398
- title: options?.title,
399
- status: options?.status ?? "active",
400
- metadata: { ...options?.metadata ?? {} },
401
- ids: [],
402
- messages: {},
403
- createdAt: now,
404
- updatedAt: now
405
- };
406
- return toReadonly(conv);
542
+ return { inputs: filtered };
407
543
  }
408
544
  function appendMessages(conversation, ...args) {
545
+ return appendMessagesInternal(conversation, args, true);
546
+ }
547
+ function appendUnsafeMessage(conversation, input, environment) {
548
+ return appendMessagesInternal(conversation, [input, environment], false);
549
+ }
550
+ var appendMessagesInternal = (conversation, args, validate) => {
409
551
  const { inputs, environment } = partitionAppendArgs(args);
410
552
  const resolvedEnvironment = resolveConversationEnvironment(environment);
411
553
  const now = resolvedEnvironment.now();
412
554
  const startPosition = conversation.ids.length;
413
- const initialToolUses = buildToolUseIndex(getOrderedMessages(conversation));
555
+ const initialToolUses = validate ? buildToolUseIndex(getOrderedMessages(conversation)) : new Map;
414
556
  const { messages } = inputs.reduce((state, input, index) => {
415
557
  const processedInput = resolvedEnvironment.plugins.reduce((acc, plugin) => plugin(acc), input);
416
- if (processedInput.role === "tool-result" && processedInput.toolResult) {
558
+ if (validate && processedInput.role === "tool-result" && processedInput.toolResult) {
417
559
  assertToolReference(state.toolUses, processedInput.toolResult.callId);
418
560
  }
419
561
  const normalizedContent = normalizeContent(processedInput.content);
@@ -440,7 +582,16 @@ function appendMessages(conversation, ...args) {
440
582
  } else {
441
583
  message = createMessage(baseMessage);
442
584
  }
443
- const toolUses = processedInput.role === "tool-use" && processedInput.toolCall ? registerToolUse(state.toolUses, processedInput.toolCall) : state.toolUses;
585
+ let toolUses = state.toolUses;
586
+ if (processedInput.role === "tool-use" && processedInput.toolCall) {
587
+ if (validate && state.toolUses.has(processedInput.toolCall.id)) {
588
+ throw createIntegrityError("duplicate toolCall.id in conversation", {
589
+ toolCallId: processedInput.toolCall.id,
590
+ messageId: baseMessage.id
591
+ });
592
+ }
593
+ toolUses = validate ? registerToolUse(state.toolUses, processedInput.toolCall) : state.toolUses;
594
+ }
444
595
  return {
445
596
  toolUses,
446
597
  messages: [...state.messages, message]
@@ -453,17 +604,25 @@ function appendMessages(conversation, ...args) {
453
604
  messages: { ...conversation.messages, ...toIdRecord(messages) },
454
605
  updatedAt: now
455
606
  };
456
- return toReadonly(next);
457
- }
607
+ const readonly = toReadonly(next);
608
+ return validate ? ensureConversationSafe(readonly) : readonly;
609
+ };
458
610
  function appendUserMessage(conversation, content, metadata, environment) {
459
- return environment ? appendMessages(conversation, { role: "user", content, metadata }, environment) : appendMessages(conversation, { role: "user", content, metadata });
611
+ const resolvedEnvironment = isConversationEnvironmentParameter(metadata) ? metadata : environment;
612
+ const resolvedMetadata = isConversationEnvironmentParameter(metadata) ? undefined : metadata;
613
+ return appendMessages(conversation, { role: "user", content, metadata: resolvedMetadata }, resolvedEnvironment);
460
614
  }
461
615
  function appendAssistantMessage(conversation, content, metadata, environment) {
462
- return environment ? appendMessages(conversation, { role: "assistant", content, metadata }, environment) : appendMessages(conversation, { role: "assistant", content, metadata });
616
+ const resolvedEnvironment = isConversationEnvironmentParameter(metadata) ? metadata : environment;
617
+ const resolvedMetadata = isConversationEnvironmentParameter(metadata) ? undefined : metadata;
618
+ return appendMessages(conversation, { role: "assistant", content, metadata: resolvedMetadata }, resolvedEnvironment);
463
619
  }
464
620
  function appendSystemMessage(conversation, content, metadata, environment) {
465
- return environment ? appendMessages(conversation, { role: "system", content, metadata }, environment) : appendMessages(conversation, { role: "system", content, metadata });
621
+ const resolvedEnvironment = isConversationEnvironmentParameter(metadata) ? metadata : environment;
622
+ const resolvedMetadata = isConversationEnvironmentParameter(metadata) ? undefined : metadata;
623
+ return appendMessages(conversation, { role: "system", content, metadata: resolvedMetadata }, resolvedEnvironment);
466
624
  }
625
+ // src/conversation/query.ts
467
626
  function getMessages(conversation, options) {
468
627
  const includeHidden = options?.includeHidden ?? false;
469
628
  const ordered = getOrderedMessages(conversation);
@@ -497,6 +656,7 @@ function getStatistics(conversation) {
497
656
  }, { byRole: {}, hidden: 0, withImages: 0 });
498
657
  return { total: ordered.length, ...stats };
499
658
  }
659
+ // src/conversation/system-messages.ts
500
660
  function hasSystemMessage(conversation) {
501
661
  return getOrderedMessages(conversation).some((m) => m.role === "system");
502
662
  }
@@ -507,7 +667,8 @@ function getSystemMessages(conversation) {
507
667
  return getOrderedMessages(conversation).filter((m) => m.role === "system");
508
668
  }
509
669
  function prependSystemMessage(conversation, content, metadata, environment) {
510
- const resolvedEnvironment = resolveConversationEnvironment(environment);
670
+ const resolvedEnvironment = resolveConversationEnvironment(isConversationEnvironmentParameter(metadata) ? metadata : environment);
671
+ const resolvedMetadata = isConversationEnvironmentParameter(metadata) ? undefined : metadata;
511
672
  const now = resolvedEnvironment.now();
512
673
  const newMessage = createMessage({
513
674
  id: resolvedEnvironment.randomId(),
@@ -515,7 +676,7 @@ function prependSystemMessage(conversation, content, metadata, environment) {
515
676
  content,
516
677
  position: 0,
517
678
  createdAt: now,
518
- metadata: { ...metadata ?? {} },
679
+ metadata: { ...resolvedMetadata ?? {} },
519
680
  hidden: false,
520
681
  toolCall: undefined,
521
682
  toolResult: undefined,
@@ -534,20 +695,21 @@ function prependSystemMessage(conversation, content, metadata, environment) {
534
695
  toolResult: message.toolResult,
535
696
  tokenUsage: message.tokenUsage
536
697
  }));
537
- return toReadonly({
698
+ return ensureConversationSafe(toReadonly({
538
699
  ...conversation,
539
700
  ids: [newMessage.id, ...ordered.map((message) => message.id)],
540
701
  messages: toIdRecord([newMessage, ...renumberedMessages]),
541
702
  updatedAt: now
542
- });
703
+ }));
543
704
  }
544
705
  function replaceSystemMessage(conversation, content, metadata, environment) {
545
- const resolvedEnvironment = resolveConversationEnvironment(environment);
706
+ const resolvedEnvironment = resolveConversationEnvironment(isConversationEnvironmentParameter(metadata) ? metadata : environment);
707
+ const resolvedMetadata = isConversationEnvironmentParameter(metadata) ? undefined : metadata;
546
708
  const now = resolvedEnvironment.now();
547
709
  const ordered = getOrderedMessages(conversation);
548
710
  const firstSystemIndex = ordered.findIndex((m) => m.role === "system");
549
711
  if (firstSystemIndex === -1) {
550
- return prependSystemMessage(conversation, content, metadata, resolvedEnvironment);
712
+ return prependSystemMessage(conversation, content, resolvedMetadata, resolvedEnvironment);
551
713
  }
552
714
  const original = ordered[firstSystemIndex];
553
715
  const replaced = createMessage({
@@ -556,7 +718,7 @@ function replaceSystemMessage(conversation, content, metadata, environment) {
556
718
  content,
557
719
  position: original.position,
558
720
  createdAt: original.createdAt,
559
- metadata: { ...metadata ?? original.metadata },
721
+ metadata: { ...resolvedMetadata ?? original.metadata },
560
722
  hidden: original.hidden,
561
723
  toolCall: undefined,
562
724
  toolResult: undefined,
@@ -568,13 +730,13 @@ function replaceSystemMessage(conversation, content, metadata, environment) {
568
730
  messages: { ...conversation.messages, [replaced.id]: replaced },
569
731
  updatedAt: now
570
732
  };
571
- return toReadonly(next);
733
+ return ensureConversationSafe(toReadonly(next));
572
734
  }
573
735
  function collapseSystemMessages(conversation, environment) {
574
736
  const ordered = getOrderedMessages(conversation);
575
737
  const systemMessages = ordered.filter((m) => m.role === "system");
576
738
  if (systemMessages.length <= 1) {
577
- return conversation;
739
+ return ensureConversationSafe(conversation);
578
740
  }
579
741
  const resolvedEnvironment = resolveConversationEnvironment(environment);
580
742
  const now = resolvedEnvironment.now();
@@ -630,9 +792,39 @@ function collapseSystemMessages(conversation, environment) {
630
792
  messages: toIdRecord(renumbered),
631
793
  updatedAt: now
632
794
  };
633
- return toReadonly(next);
795
+ return ensureConversationSafe(toReadonly(next));
634
796
  }
635
- function redactMessageAtPosition(conversation, position, placeholder = "[REDACTED]", environment) {
797
+ // src/utilities/tool-results.ts
798
+ function redactToolResult(toolResult, placeholder) {
799
+ return { ...toolResult, content: placeholder };
800
+ }
801
+
802
+ // src/conversation/modify.ts
803
+ var isRedactMessageOptions = (value) => {
804
+ if (!value || typeof value !== "object")
805
+ return false;
806
+ const candidate = value;
807
+ return "placeholder" in candidate || "redactToolArguments" in candidate || "redactToolResults" in candidate || "clearToolMetadata" in candidate;
808
+ };
809
+ function redactMessageAtPosition(conversation, position, placeholderOrOptions, environment) {
810
+ let placeholder = "[REDACTED]";
811
+ let options = {};
812
+ let env = environment;
813
+ if (typeof placeholderOrOptions === "string") {
814
+ placeholder = placeholderOrOptions;
815
+ } else if (placeholderOrOptions) {
816
+ if (!environment && isConversationEnvironmentParameter(placeholderOrOptions)) {
817
+ env = placeholderOrOptions;
818
+ } else if (isRedactMessageOptions(placeholderOrOptions)) {
819
+ options = placeholderOrOptions;
820
+ if (options.placeholder) {
821
+ placeholder = options.placeholder;
822
+ }
823
+ }
824
+ }
825
+ const redactToolArguments = options.redactToolArguments ?? true;
826
+ const redactToolResults = options.redactToolResults ?? true;
827
+ const clearToolMetadata = options.clearToolMetadata ?? false;
636
828
  if (position < 0 || position >= conversation.ids.length) {
637
829
  throw createInvalidPositionError(conversation.ids.length - 1, position);
638
830
  }
@@ -641,6 +833,22 @@ function redactMessageAtPosition(conversation, position, placeholder = "[REDACTE
641
833
  if (!original) {
642
834
  throw createInvalidPositionError(conversation.ids.length - 1, position);
643
835
  }
836
+ let toolCall = original.toolCall ? { ...original.toolCall } : undefined;
837
+ let toolResult = original.toolResult ? { ...original.toolResult } : undefined;
838
+ if (clearToolMetadata) {
839
+ toolCall = undefined;
840
+ toolResult = undefined;
841
+ } else {
842
+ if (original.role === "tool-use" && toolCall) {
843
+ toolCall = {
844
+ ...toolCall,
845
+ arguments: redactToolArguments ? placeholder : toolCall.arguments
846
+ };
847
+ }
848
+ if (original.role === "tool-result" && toolResult) {
849
+ toolResult = redactToolResults ? redactToolResult(toolResult, placeholder) : { ...toolResult };
850
+ }
851
+ }
644
852
  const redacted = createMessage({
645
853
  id: original.id,
646
854
  role: original.role,
@@ -649,11 +857,11 @@ function redactMessageAtPosition(conversation, position, placeholder = "[REDACTE
649
857
  createdAt: original.createdAt,
650
858
  metadata: { ...original.metadata },
651
859
  hidden: original.hidden,
652
- toolCall: undefined,
653
- toolResult: undefined,
654
- tokenUsage: undefined
860
+ toolCall,
861
+ toolResult,
862
+ tokenUsage: original.tokenUsage ? { ...original.tokenUsage } : undefined
655
863
  });
656
- const resolvedEnvironment = resolveConversationEnvironment(environment);
864
+ const resolvedEnvironment = resolveConversationEnvironment(env);
657
865
  const now = resolvedEnvironment.now();
658
866
  const next = {
659
867
  ...conversation,
@@ -661,70 +869,62 @@ function redactMessageAtPosition(conversation, position, placeholder = "[REDACTE
661
869
  messages: { ...conversation.messages, [redacted.id]: redacted },
662
870
  updatedAt: now
663
871
  };
664
- return toReadonly(next);
872
+ return ensureConversationSafe(toReadonly(next));
665
873
  }
666
- function migrateConversation(json) {
667
- if (typeof json !== "object" || json === null || Array.isArray(json)) {
668
- return {
669
- schemaVersion: CURRENT_SCHEMA_VERSION,
670
- id: "",
671
- status: "active",
672
- metadata: {},
673
- ids: [],
674
- messages: {},
675
- createdAt: new Date().toISOString(),
676
- updatedAt: new Date().toISOString()
677
- };
678
- }
679
- const data = json;
680
- const rawMessages = data.messages;
681
- let messages = {};
682
- let ids = [];
683
- const rawIds = data.ids;
684
- const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
685
- if (Array.isArray(rawMessages)) {
686
- const rawMessageArray = rawMessages;
687
- ids = rawMessageArray.map((message) => message.id);
688
- messages = Object.fromEntries(rawMessageArray.map((message) => [message.id, message]));
689
- } else if (rawMessages && typeof rawMessages === "object") {
690
- messages = { ...rawMessages };
691
- if (isStringArray(rawIds) && rawIds.length > 0) {
692
- ids = [...rawIds];
693
- } else {
694
- ids = Object.values(messages).sort((a, b) => a.position - b.position).map((message) => message.id);
695
- }
696
- }
697
- if (ids.length > 0) {
698
- ids = ids.filter((id) => (id in messages));
699
- const missing = Object.keys(messages).filter((id) => !ids.includes(id));
700
- if (missing.length > 0) {
701
- const sortedMissing = missing.sort((a, b) => (messages[a]?.position ?? 0) - (messages[b]?.position ?? 0));
702
- ids = [...ids, ...sortedMissing];
703
- }
704
- }
705
- if (!("schemaVersion" in json)) {
874
+ // src/conversation/serialization.ts
875
+ function normalizeToolResult(toolResult) {
876
+ if (!toolResult)
877
+ return;
878
+ return {
879
+ callId: toolResult.callId,
880
+ outcome: toolResult.outcome,
881
+ content: toolResult.content
882
+ };
883
+ }
884
+ function normalizeMessage(message) {
885
+ const base = {
886
+ id: message.id,
887
+ role: message.role,
888
+ content: message.content,
889
+ position: message.position,
890
+ createdAt: message.createdAt,
891
+ metadata: message.metadata,
892
+ hidden: message.hidden,
893
+ toolCall: message.toolCall ? { ...message.toolCall } : undefined,
894
+ toolResult: normalizeToolResult(message.toolResult),
895
+ tokenUsage: message.tokenUsage ? { ...message.tokenUsage } : undefined
896
+ };
897
+ if (isAssistantMessage(message)) {
706
898
  return {
707
- ...data,
708
- schemaVersion: CURRENT_SCHEMA_VERSION,
709
- ids,
710
- messages
899
+ ...base,
900
+ role: "assistant",
901
+ goalCompleted: message.goalCompleted
711
902
  };
712
903
  }
713
- return { ...data, ids, messages };
904
+ return base;
714
905
  }
715
906
  function deserializeConversation(json) {
716
- const migrated = migrateConversation(json);
907
+ const parsed = conversationSchema.safeParse(json);
908
+ if (!parsed.success) {
909
+ throw createSerializationError("failed to deserialize conversation: invalid data");
910
+ }
911
+ const data = parsed.data;
717
912
  try {
718
- const orderedMessages = migrated.ids.map((id, index) => {
719
- const message = migrated.messages[id];
913
+ const messageIds = new Set(Object.keys(data.messages));
914
+ const orderedMessages = data.ids.map((id, index) => {
915
+ const message = data.messages[id];
720
916
  if (!message) {
721
917
  throw createSerializationError(`missing message for id ${id}`);
722
918
  }
723
919
  if (message.position !== index) {
724
920
  throw createInvalidPositionError(index, message.position);
725
921
  }
726
- return message;
922
+ messageIds.delete(id);
923
+ return normalizeMessage(message);
727
924
  });
925
+ if (messageIds.size > 0) {
926
+ throw createSerializationError(`messages not listed in ids: ${[...messageIds].join(", ")}`);
927
+ }
728
928
  orderedMessages.reduce((state, message) => {
729
929
  if (message.role === "tool-use" && message.toolCall) {
730
930
  return {
@@ -736,24 +936,28 @@ function deserializeConversation(json) {
736
936
  }
737
937
  return state;
738
938
  }, { toolUses: new Map });
739
- const messageInstances = orderedMessages.map((m) => createMessage(m));
939
+ const messageInstances = orderedMessages.map((message) => createMessage(message));
740
940
  const conv = {
741
- schemaVersion: migrated.schemaVersion,
742
- id: migrated.id,
743
- title: migrated.title,
744
- status: migrated.status,
745
- metadata: { ...migrated.metadata },
941
+ schemaVersion: data.schemaVersion,
942
+ id: data.id,
943
+ title: data.title,
944
+ status: data.status,
945
+ metadata: { ...data.metadata },
746
946
  ids: orderedMessages.map((message) => message.id),
747
947
  messages: toIdRecord(messageInstances),
748
- createdAt: migrated.createdAt,
749
- updatedAt: migrated.updatedAt
948
+ createdAt: data.createdAt,
949
+ updatedAt: data.updatedAt
750
950
  };
751
- return toReadonly(conv);
951
+ const readonly = toReadonly(conv);
952
+ assertConversationIntegrity(readonly);
953
+ return readonly;
752
954
  } catch (error) {
753
955
  throw createSerializationError(`failed to deserialize conversation: ${error instanceof Error ? error.message : String(error)}`, error);
754
956
  }
755
957
  }
958
+ // src/conversation/transform.ts
756
959
  function toChatMessages(conversation) {
960
+ assertConversationSafe(conversation);
757
961
  const roleMap = {
758
962
  user: "user",
759
963
  assistant: "assistant",
@@ -775,6 +979,62 @@ function toChatMessages(conversation) {
775
979
  }
776
980
  return result;
777
981
  }
982
+ // src/conversation/tool-interactions.ts
983
+ function appendToolUse(conversation, toolCall, options, environment) {
984
+ const resolved = resolveConversationEnvironment(isConversationEnvironmentParameter(options) ? options : environment);
985
+ const resolvedOptions = isConversationEnvironmentParameter(options) ? undefined : options;
986
+ const callId = toolCall.callId ?? resolved.randomId();
987
+ const toolCallMeta = {
988
+ id: callId,
989
+ name: toolCall.toolId,
990
+ arguments: toolCall.args
991
+ };
992
+ return appendMessages(conversation, {
993
+ role: "tool-use",
994
+ content: resolvedOptions?.content ?? "",
995
+ metadata: resolvedOptions?.metadata,
996
+ hidden: resolvedOptions?.hidden,
997
+ toolCall: toolCallMeta,
998
+ tokenUsage: resolvedOptions?.tokenUsage
999
+ }, resolved);
1000
+ }
1001
+ function appendToolResult(conversation, toolResult, options, environment) {
1002
+ const resolvedOptions = isConversationEnvironmentParameter(options) ? undefined : options;
1003
+ const toolResultMeta = {
1004
+ callId: toolResult.callId,
1005
+ outcome: toolResult.outcome,
1006
+ content: toolResult.result
1007
+ };
1008
+ return appendMessages(conversation, {
1009
+ role: "tool-result",
1010
+ content: resolvedOptions?.content ?? "",
1011
+ metadata: resolvedOptions?.metadata,
1012
+ hidden: resolvedOptions?.hidden,
1013
+ toolResult: toolResultMeta,
1014
+ tokenUsage: resolvedOptions?.tokenUsage
1015
+ }, isConversationEnvironmentParameter(options) ? options : environment);
1016
+ }
1017
+ function getPendingToolCalls(conversation) {
1018
+ const ordered = getOrderedMessages(conversation);
1019
+ const completed = new Set;
1020
+ for (const message of ordered) {
1021
+ if (message.role === "tool-result" && message.toolResult) {
1022
+ completed.add(message.toolResult.callId);
1023
+ }
1024
+ }
1025
+ const pending = [];
1026
+ for (const message of ordered) {
1027
+ if (message.role === "tool-use" && message.toolCall) {
1028
+ if (!completed.has(message.toolCall.id)) {
1029
+ pending.push(message.toolCall);
1030
+ }
1031
+ }
1032
+ }
1033
+ return pending;
1034
+ }
1035
+ function getToolInteractions(conversation) {
1036
+ return pairToolCallsWithResults(getOrderedMessages(conversation));
1037
+ }
778
1038
  // src/guards.ts
779
1039
  function isSchema(schema, value) {
780
1040
  return schema.safeParse(value).success;
@@ -833,6 +1093,92 @@ var cloneMessageWithPosition = (message, position, content) => {
833
1093
  }
834
1094
  return createMessage(baseMessage);
835
1095
  };
1096
+ var createMessageBlock = (message, estimator) => ({
1097
+ messages: [message],
1098
+ minPosition: message.position,
1099
+ maxPosition: message.position,
1100
+ tokenCount: estimator(message)
1101
+ });
1102
+ var buildMessageBlocks = (messages, estimator, preserveToolPairs) => {
1103
+ if (!preserveToolPairs) {
1104
+ const blocks2 = messages.map((message) => createMessageBlock(message, estimator));
1105
+ const messageToBlock2 = new Map;
1106
+ for (const block of blocks2) {
1107
+ const message = block.messages[0];
1108
+ if (message) {
1109
+ messageToBlock2.set(message.id, block);
1110
+ }
1111
+ }
1112
+ return { blocks: blocks2, messageToBlock: messageToBlock2 };
1113
+ }
1114
+ const blocks = [];
1115
+ const toolUses = new Map;
1116
+ for (const message of messages) {
1117
+ if (message.role === "tool-use" && message.toolCall) {
1118
+ const block = createMessageBlock(message, estimator);
1119
+ toolUses.set(message.toolCall.id, block);
1120
+ blocks.push(block);
1121
+ continue;
1122
+ }
1123
+ if (message.role === "tool-result" && message.toolResult) {
1124
+ const existing = toolUses.get(message.toolResult.callId);
1125
+ if (existing) {
1126
+ existing.messages.push(message);
1127
+ existing.maxPosition = Math.max(existing.maxPosition, message.position);
1128
+ existing.tokenCount += estimator(message);
1129
+ continue;
1130
+ }
1131
+ const orphanBlock = createMessageBlock(message, estimator);
1132
+ orphanBlock.orphanToolResult = true;
1133
+ blocks.push(orphanBlock);
1134
+ continue;
1135
+ }
1136
+ blocks.push(createMessageBlock(message, estimator));
1137
+ }
1138
+ const filteredBlocks = blocks.filter((block) => !block.orphanToolResult);
1139
+ const messageToBlock = new Map;
1140
+ for (const block of filteredBlocks) {
1141
+ for (const message of block.messages) {
1142
+ messageToBlock.set(message.id, block);
1143
+ }
1144
+ }
1145
+ return { blocks: filteredBlocks, messageToBlock };
1146
+ };
1147
+ var collectBlocksForMessages = (messages, messageToBlock) => {
1148
+ const blocks = [];
1149
+ const seen = new Set;
1150
+ for (const message of messages) {
1151
+ const block = messageToBlock.get(message.id);
1152
+ if (block && !seen.has(block)) {
1153
+ seen.add(block);
1154
+ blocks.push(block);
1155
+ }
1156
+ }
1157
+ return blocks;
1158
+ };
1159
+ var collectMessagesFromBlocks = (blocks) => {
1160
+ const messages = [];
1161
+ const seen = new Set;
1162
+ for (const block of blocks) {
1163
+ for (const message of block.messages) {
1164
+ if (!seen.has(message.id)) {
1165
+ seen.add(message.id);
1166
+ messages.push(message);
1167
+ }
1168
+ }
1169
+ }
1170
+ messages.sort((a, b) => a.position - b.position);
1171
+ return messages;
1172
+ };
1173
+ var ensureTruncationSafe = (conversation, preserveToolPairs, operation) => {
1174
+ try {
1175
+ return ensureConversationSafe(conversation);
1176
+ } catch (error) {
1177
+ if (!preserveToolPairs && error instanceof ConversationalistError && error.code === "error:integrity")
1178
+ throw createIntegrityError(`${operation} produced invalid tool linkage; use preserveToolPairs: true to keep tool interactions intact`, { preserveToolPairs, issues: error.context?.["issues"] });
1179
+ throw error;
1180
+ }
1181
+ };
836
1182
  function estimateConversationTokens(conversation, estimateTokens, environment) {
837
1183
  let estimator = estimateTokens;
838
1184
  let env = environment;
@@ -845,6 +1191,7 @@ function estimateConversationTokens(conversation, estimateTokens, environment) {
845
1191
  return getOrderedMessages(conversation).reduce((total, message) => total + finalEstimator(message), 0);
846
1192
  }
847
1193
  function truncateToTokenLimit(conversation, maxTokens, optionsOrEstimator, environment) {
1194
+ assertConversationSafe(conversation);
848
1195
  let options = {};
849
1196
  let env = environment;
850
1197
  if (typeof optionsOrEstimator === "function") {
@@ -866,54 +1213,56 @@ function truncateToTokenLimit(conversation, maxTokens, optionsOrEstimator, envir
866
1213
  const estimator = options.estimateTokens ?? resolvedEnvironment.estimateTokens;
867
1214
  const preserveSystem = options.preserveSystemMessages ?? true;
868
1215
  const preserveLastN = options.preserveLastN ?? 0;
1216
+ const preserveToolPairs = options.preserveToolPairs ?? true;
869
1217
  const currentTokens = estimateConversationTokens(conversation, estimator, resolvedEnvironment);
870
1218
  if (currentTokens <= maxTokens) {
871
1219
  return conversation;
872
1220
  }
873
1221
  const now = resolvedEnvironment.now();
874
1222
  const orderedMessages = getOrderedMessages(conversation);
1223
+ const { blocks, messageToBlock } = buildMessageBlocks(orderedMessages, estimator, preserveToolPairs);
875
1224
  const systemMessages = preserveSystem ? orderedMessages.filter((m) => m.role === "system") : [];
876
1225
  const nonSystemMessages = orderedMessages.filter((m) => m.role !== "system");
877
1226
  const protectedMessages = preserveLastN > 0 ? nonSystemMessages.slice(-preserveLastN) : [];
878
- const removableMessages = preserveLastN > 0 ? nonSystemMessages.slice(0, -preserveLastN) : nonSystemMessages;
879
- const systemTokens = systemMessages.reduce((sum, m) => sum + estimator(m), 0);
880
- const protectedTokens = protectedMessages.reduce((sum, m) => sum + estimator(m), 0);
1227
+ const systemBlocks = collectBlocksForMessages(systemMessages, messageToBlock);
1228
+ const protectedBlocks = collectBlocksForMessages(protectedMessages, messageToBlock);
1229
+ const lockedBlocks = new Set([...systemBlocks, ...protectedBlocks]);
1230
+ const removableBlocks = blocks.filter((block) => !lockedBlocks.has(block));
1231
+ const systemTokens = systemBlocks.reduce((sum, block) => sum + block.tokenCount, 0);
1232
+ const protectedTokens = protectedBlocks.reduce((sum, block) => sum + block.tokenCount, 0);
881
1233
  const availableTokens = maxTokens - systemTokens - protectedTokens;
1234
+ let selectedBlocks = [];
882
1235
  if (availableTokens <= 0) {
883
- const allMessages2 = [...systemMessages, ...protectedMessages];
884
- const renumbered2 = allMessages2.map((message, index) => cloneMessageWithPosition(message, index, copyContent(message.content)));
885
- return toReadonly({
886
- ...conversation,
887
- ids: renumbered2.map((message) => message.id),
888
- messages: toIdRecord(renumbered2),
889
- updatedAt: now
890
- });
891
- }
892
- const keptRemovable = [];
893
- let usedTokens = 0;
894
- for (let i = removableMessages.length - 1;i >= 0; i--) {
895
- const message = removableMessages[i];
896
- const messageTokens = estimator(message);
897
- if (usedTokens + messageTokens <= availableTokens) {
898
- keptRemovable.unshift(message);
899
- usedTokens += messageTokens;
900
- } else {
901
- break;
1236
+ selectedBlocks = [...systemBlocks, ...protectedBlocks];
1237
+ } else {
1238
+ const sortedRemovable = [...removableBlocks].sort((a, b) => a.maxPosition - b.maxPosition);
1239
+ const keptRemovable = [];
1240
+ let usedTokens = 0;
1241
+ for (let i = sortedRemovable.length - 1;i >= 0; i--) {
1242
+ const block = sortedRemovable[i];
1243
+ if (usedTokens + block.tokenCount <= availableTokens) {
1244
+ keptRemovable.unshift(block);
1245
+ usedTokens += block.tokenCount;
1246
+ } else {
1247
+ break;
1248
+ }
902
1249
  }
1250
+ selectedBlocks = [...systemBlocks, ...keptRemovable, ...protectedBlocks];
903
1251
  }
904
- const allMessages = [...systemMessages, ...keptRemovable, ...protectedMessages];
905
- allMessages.sort((a, b) => a.position - b.position);
1252
+ const allMessages = collectMessagesFromBlocks(selectedBlocks);
906
1253
  const renumbered = allMessages.map((message, index) => cloneMessageWithPosition(message, index, copyContent(message.content)));
907
- return toReadonly({
1254
+ const next = toReadonly({
908
1255
  ...conversation,
909
1256
  ids: renumbered.map((message) => message.id),
910
1257
  messages: toIdRecord(renumbered),
911
1258
  updatedAt: now
912
1259
  });
1260
+ return ensureTruncationSafe(next, preserveToolPairs, "truncateToTokenLimit");
913
1261
  }
914
1262
  function getRecentMessages(conversation, count, options) {
915
1263
  const includeHidden = options?.includeHidden ?? false;
916
1264
  const includeSystem = options?.includeSystem ?? false;
1265
+ const preserveToolPairs = options?.preserveToolPairs ?? true;
917
1266
  const filtered = getOrderedMessages(conversation).filter((m) => {
918
1267
  if (!includeHidden && m.hidden)
919
1268
  return false;
@@ -921,23 +1270,35 @@ function getRecentMessages(conversation, count, options) {
921
1270
  return false;
922
1271
  return true;
923
1272
  });
924
- return filtered.slice(-count);
1273
+ if (!preserveToolPairs) {
1274
+ return filtered.slice(-count);
1275
+ }
1276
+ const { messageToBlock } = buildMessageBlocks(filtered, () => 0, preserveToolPairs);
1277
+ const tail = filtered.slice(-count);
1278
+ const blocks = collectBlocksForMessages(tail, messageToBlock);
1279
+ return collectMessagesFromBlocks(blocks);
925
1280
  }
926
1281
  function truncateFromPosition(conversation, position, options, environment) {
1282
+ assertConversationSafe(conversation);
927
1283
  const preserveSystem = options?.preserveSystemMessages ?? true;
1284
+ const preserveToolPairs = options?.preserveToolPairs ?? true;
928
1285
  const resolvedEnvironment = resolveConversationEnvironment(environment);
929
1286
  const now = resolvedEnvironment.now();
930
1287
  const ordered = getOrderedMessages(conversation);
1288
+ const { messageToBlock } = buildMessageBlocks(ordered, () => 0, preserveToolPairs);
931
1289
  const systemMessages = preserveSystem ? ordered.filter((m) => m.role === "system" && m.position < position) : [];
932
1290
  const keptMessages = ordered.filter((m) => m.position >= position);
933
- const allMessages = [...systemMessages, ...keptMessages];
1291
+ const systemBlocks = collectBlocksForMessages(systemMessages, messageToBlock);
1292
+ const keptBlocks = collectBlocksForMessages(keptMessages, messageToBlock);
1293
+ const allMessages = collectMessagesFromBlocks([...systemBlocks, ...keptBlocks]);
934
1294
  const renumbered = allMessages.map((message, index) => cloneMessageWithPosition(message, index, copyContent(message.content)));
935
- return toReadonly({
1295
+ const next = toReadonly({
936
1296
  ...conversation,
937
1297
  ids: renumbered.map((message) => message.id),
938
1298
  messages: toIdRecord(renumbered),
939
1299
  updatedAt: now
940
1300
  });
1301
+ return ensureTruncationSafe(next, preserveToolPairs, "truncateFromPosition");
941
1302
  }
942
1303
 
943
1304
  // src/streaming.ts
@@ -972,7 +1333,8 @@ function getStreamingMessage(conversation) {
972
1333
  return getOrderedMessages(conversation).find(isStreamingMessage);
973
1334
  }
974
1335
  function appendStreamingMessage(conversation, role, metadata, environment) {
975
- const resolvedEnvironment = resolveConversationEnvironment(environment);
1336
+ const resolvedEnvironment = resolveConversationEnvironment(isConversationEnvironmentParameter(metadata) ? metadata : environment);
1337
+ const resolvedMetadata = isConversationEnvironmentParameter(metadata) ? undefined : metadata;
976
1338
  const now = resolvedEnvironment.now();
977
1339
  const messageId = resolvedEnvironment.randomId();
978
1340
  const newMessage = createMessage({
@@ -981,7 +1343,7 @@ function appendStreamingMessage(conversation, role, metadata, environment) {
981
1343
  content: "",
982
1344
  position: conversation.ids.length,
983
1345
  createdAt: now,
984
- metadata: { ...metadata ?? {}, [STREAMING_KEY]: true },
1346
+ metadata: { ...resolvedMetadata ?? {}, [STREAMING_KEY]: true },
985
1347
  hidden: false,
986
1348
  toolCall: undefined,
987
1349
  toolResult: undefined,
@@ -993,14 +1355,14 @@ function appendStreamingMessage(conversation, role, metadata, environment) {
993
1355
  messages: { ...conversation.messages, [messageId]: newMessage },
994
1356
  updatedAt: now
995
1357
  });
996
- return { conversation: updatedConversation, messageId };
1358
+ return { conversation: ensureConversationSafe(updatedConversation), messageId };
997
1359
  }
998
1360
  function updateStreamingMessage(conversation, messageId, content, environment) {
999
1361
  const resolvedEnvironment = resolveConversationEnvironment(environment);
1000
1362
  const now = resolvedEnvironment.now();
1001
1363
  const original = conversation.messages[messageId];
1002
1364
  if (!original) {
1003
- return conversation;
1365
+ return ensureConversationSafe(conversation);
1004
1366
  }
1005
1367
  const overrides = {
1006
1368
  content: typeof content === "string" ? content : [...content]
@@ -1009,44 +1371,45 @@ function updateStreamingMessage(conversation, messageId, content, environment) {
1009
1371
  overrides.tokenUsage = { ...original.tokenUsage };
1010
1372
  }
1011
1373
  const updated = cloneMessage(original, overrides);
1012
- return toReadonly({
1374
+ return ensureConversationSafe(toReadonly({
1013
1375
  ...conversation,
1014
1376
  ids: [...conversation.ids],
1015
1377
  messages: { ...conversation.messages, [updated.id]: updated },
1016
1378
  updatedAt: now
1017
- });
1379
+ }));
1018
1380
  }
1019
1381
  function finalizeStreamingMessage(conversation, messageId, options, environment) {
1020
- const resolvedEnvironment = resolveConversationEnvironment(environment);
1382
+ const resolvedEnvironment = resolveConversationEnvironment(isConversationEnvironmentParameter(options) ? options : environment);
1383
+ const resolvedOptions = isConversationEnvironmentParameter(options) ? undefined : options;
1021
1384
  const now = resolvedEnvironment.now();
1022
1385
  const original = conversation.messages[messageId];
1023
1386
  if (!original) {
1024
- return conversation;
1387
+ return ensureConversationSafe(conversation);
1025
1388
  }
1026
1389
  const { [STREAMING_KEY]: _, ...restMetadata } = original.metadata;
1027
1390
  const finalMetadata = {
1028
1391
  ...restMetadata,
1029
- ...options?.metadata ?? {}
1392
+ ...resolvedOptions?.metadata ?? {}
1030
1393
  };
1031
1394
  const finalizeOverrides = {
1032
1395
  metadata: finalMetadata
1033
1396
  };
1034
- if (options?.tokenUsage) {
1035
- finalizeOverrides.tokenUsage = { ...options.tokenUsage };
1397
+ if (resolvedOptions?.tokenUsage) {
1398
+ finalizeOverrides.tokenUsage = { ...resolvedOptions.tokenUsage };
1036
1399
  }
1037
1400
  const updated = cloneMessage(original, finalizeOverrides);
1038
- return toReadonly({
1401
+ return ensureConversationSafe(toReadonly({
1039
1402
  ...conversation,
1040
1403
  ids: [...conversation.ids],
1041
1404
  messages: { ...conversation.messages, [updated.id]: updated },
1042
1405
  updatedAt: now
1043
- });
1406
+ }));
1044
1407
  }
1045
1408
  function cancelStreamingMessage(conversation, messageId, environment) {
1046
1409
  const resolvedEnvironment = resolveConversationEnvironment(environment);
1047
1410
  const now = resolvedEnvironment.now();
1048
1411
  if (!conversation.messages[messageId]) {
1049
- return conversation;
1412
+ return ensureConversationSafe(conversation);
1050
1413
  }
1051
1414
  const messages = getOrderedMessages(conversation).filter((m) => m.id !== messageId).map((message, index) => message.position === index ? message : (() => {
1052
1415
  const overrides = {
@@ -1057,12 +1420,12 @@ function cancelStreamingMessage(conversation, messageId, environment) {
1057
1420
  }
1058
1421
  return cloneMessage(message, overrides);
1059
1422
  })());
1060
- return toReadonly({
1423
+ return ensureConversationSafe(toReadonly({
1061
1424
  ...conversation,
1062
1425
  ids: messages.map((message) => message.id),
1063
1426
  messages: toIdRecord(messages),
1064
1427
  updatedAt: now
1065
- });
1428
+ }));
1066
1429
  }
1067
1430
 
1068
1431
  // src/with-conversation.ts
@@ -1100,8 +1463,8 @@ function createDraft(initial) {
1100
1463
  current = collapseSystemMessages(current);
1101
1464
  return draft;
1102
1465
  },
1103
- redactMessageAtPosition: (position, placeholder) => {
1104
- current = redactMessageAtPosition(current, position, placeholder);
1466
+ redactMessageAtPosition: (position, placeholderOrOptions) => {
1467
+ current = redactMessageAtPosition(current, position, placeholderOrOptions);
1105
1468
  return draft;
1106
1469
  },
1107
1470
  appendStreamingMessage: (role, metadata) => {
@@ -1133,15 +1496,16 @@ function createDraft(initial) {
1133
1496
  return draft;
1134
1497
  }
1135
1498
  function withConversation(conversation, fn) {
1136
- const draft = createDraft(conversation);
1499
+ const draft = createDraft(ensureConversationSafe(conversation));
1137
1500
  const maybePromise = fn(draft);
1138
1501
  if (maybePromise && typeof maybePromise === "object" && typeof maybePromise.then === "function") {
1139
- return maybePromise.then(() => draft.value);
1502
+ return maybePromise.then(() => ensureConversationSafe(draft.value));
1140
1503
  }
1141
- return draft.value;
1504
+ return ensureConversationSafe(draft.value);
1142
1505
  }
1143
1506
  function pipeConversation(conversation, ...fns) {
1144
- return fns.reduce((current, fn) => fn(current), conversation);
1507
+ const result = fns.reduce((current, fn) => fn(current), conversation);
1508
+ return ensureConversationSafe(result);
1145
1509
  }
1146
1510
  // node_modules/event-emission/dist/index.js
1147
1511
  var SymbolObservable = typeof Symbol === "function" && Symbol.observable || Symbol.for("@@observable");
@@ -2662,8 +3026,9 @@ class ConversationHistory extends EventTarget {
2662
3026
  super();
2663
3027
  this.environment = resolveConversationEnvironment(environment);
2664
3028
  this.events = createEventTarget();
3029
+ const safeInitial = ensureConversationSafe(initial);
2665
3030
  this.currentNode = {
2666
- conversation: initial,
3031
+ conversation: safeInitial,
2667
3032
  parent: null,
2668
3033
  children: []
2669
3034
  };
@@ -2861,8 +3226,8 @@ class ConversationHistory extends EventTarget {
2861
3226
  collapseSystemMessages() {
2862
3227
  this.push(collapseSystemMessages(this.current, this.env));
2863
3228
  }
2864
- redactMessageAtPosition(position, placeholder) {
2865
- this.push(redactMessageAtPosition(this.current, position, placeholder, this.env));
3229
+ redactMessageAtPosition(position, placeholderOrOptions) {
3230
+ this.push(redactMessageAtPosition(this.current, position, placeholderOrOptions, this.env));
2866
3231
  }
2867
3232
  truncateFromPosition(position, options) {
2868
3233
  this.push(truncateFromPosition(this.current, position, options, this.env));
@@ -2968,6 +3333,7 @@ function isConversation2(value) {
2968
3333
  export {
2969
3334
  withEnvironment,
2970
3335
  withConversation,
3336
+ validateConversationIntegrity,
2971
3337
  updateStreamingMessage,
2972
3338
  truncateToTokenLimit,
2973
3339
  truncateFromPosition,
@@ -3005,10 +3371,12 @@ export {
3005
3371
  isConversation,
3006
3372
  isAssistantMessage,
3007
3373
  hasSystemMessage,
3374
+ getToolInteractions,
3008
3375
  getSystemMessages,
3009
3376
  getStreamingMessage,
3010
3377
  getStatistics,
3011
3378
  getRecentMessages,
3379
+ getPendingToolCalls,
3012
3380
  getMessages,
3013
3381
  getMessageIds,
3014
3382
  getMessageById,
@@ -3026,6 +3394,7 @@ export {
3026
3394
  createInvalidPositionError,
3027
3395
  createInvalidInputError,
3028
3396
  createDuplicateIdError,
3397
+ createConversationUnsafe,
3029
3398
  createConversation,
3030
3399
  copyMultiModalContent,
3031
3400
  copyContent,
@@ -3033,7 +3402,11 @@ export {
3033
3402
  conversationSchema,
3034
3403
  collapseSystemMessages,
3035
3404
  cancelStreamingMessage,
3405
+ assertConversationIntegrity,
3036
3406
  appendUserMessage,
3407
+ appendUnsafeMessage,
3408
+ appendToolUse,
3409
+ appendToolResult,
3037
3410
  appendSystemMessage,
3038
3411
  appendStreamingMessage,
3039
3412
  appendMessages,
@@ -3042,4 +3415,4 @@ export {
3042
3415
  ConversationHistory
3043
3416
  };
3044
3417
 
3045
- //# debugId=684D5332D9139AFC64756E2164756E21
3418
+ //# debugId=F37C8EF457F39BC364756E2164756E21