conversationalist 0.0.6 → 0.0.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 (99) hide show
  1. package/README.md +227 -58
  2. package/dist/adapters/anthropic/index.d.ts +11 -6
  3. package/dist/adapters/anthropic/index.d.ts.map +1 -1
  4. package/dist/adapters/anthropic/index.js +16 -3
  5. package/dist/adapters/anthropic/index.js.map +5 -4
  6. package/dist/adapters/gemini/index.d.ts +2 -2
  7. package/dist/adapters/gemini/index.d.ts.map +1 -1
  8. package/dist/adapters/gemini/index.js +52 -9
  9. package/dist/adapters/gemini/index.js.map +5 -4
  10. package/dist/adapters/openai/index.d.ts +32 -5
  11. package/dist/adapters/openai/index.d.ts.map +1 -1
  12. package/dist/adapters/openai/index.js +30 -8
  13. package/dist/adapters/openai/index.js.map +5 -4
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/conversation/append.d.ts +4 -4
  16. package/dist/conversation/append.d.ts.map +1 -1
  17. package/dist/conversation/create.d.ts +2 -3
  18. package/dist/conversation/create.d.ts.map +1 -1
  19. package/dist/conversation/index.d.ts +2 -2
  20. package/dist/conversation/index.d.ts.map +1 -1
  21. package/dist/conversation/modify.d.ts.map +1 -1
  22. package/dist/conversation/query.d.ts +9 -5
  23. package/dist/conversation/query.d.ts.map +1 -1
  24. package/dist/conversation/serialization.d.ts +21 -5
  25. package/dist/conversation/serialization.d.ts.map +1 -1
  26. package/dist/conversation/system-messages.d.ts +3 -3
  27. package/dist/conversation/system-messages.d.ts.map +1 -1
  28. package/dist/conversation/transform.d.ts.map +1 -1
  29. package/dist/conversation.d.ts +84 -14
  30. package/dist/conversation.d.ts.map +1 -1
  31. package/dist/export/index.d.ts +7 -0
  32. package/dist/export/index.d.ts.map +1 -0
  33. package/dist/export/index.js +3762 -0
  34. package/dist/export/index.js.map +62 -0
  35. package/dist/history.d.ts +102 -24
  36. package/dist/history.d.ts.map +1 -1
  37. package/dist/index.d.ts +8 -8
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +440 -3982
  40. package/dist/index.js.map +15 -57
  41. package/dist/markdown/index.d.ts +15 -0
  42. package/dist/markdown/index.d.ts.map +1 -0
  43. package/dist/markdown/index.js +4969 -0
  44. package/dist/markdown/index.js.map +69 -0
  45. package/dist/message.d.ts +1 -1
  46. package/dist/message.d.ts.map +1 -1
  47. package/dist/multi-modal.d.ts +3 -0
  48. package/dist/multi-modal.d.ts.map +1 -1
  49. package/dist/plugins/index.d.ts +1 -1
  50. package/dist/plugins/index.d.ts.map +1 -1
  51. package/dist/plugins/index.js +59 -0
  52. package/dist/plugins/index.js.map +10 -0
  53. package/dist/plugins/pii-redaction.d.ts +10 -1
  54. package/dist/plugins/pii-redaction.d.ts.map +1 -1
  55. package/dist/redaction/index.d.ts +2 -0
  56. package/dist/redaction/index.d.ts.map +1 -0
  57. package/dist/redaction/index.js +59 -0
  58. package/dist/redaction/index.js.map +10 -0
  59. package/dist/schemas/index.d.ts +2 -0
  60. package/dist/schemas/index.d.ts.map +1 -0
  61. package/dist/schemas/index.js +114 -0
  62. package/dist/schemas/index.js.map +10 -0
  63. package/dist/schemas.d.ts +325 -15
  64. package/dist/schemas.d.ts.map +1 -1
  65. package/dist/sort/index.d.ts +2 -0
  66. package/dist/sort/index.d.ts.map +1 -0
  67. package/dist/sort/index.js +32 -0
  68. package/dist/sort/index.js.map +10 -0
  69. package/dist/streaming.d.ts +3 -3
  70. package/dist/streaming.d.ts.map +1 -1
  71. package/dist/types.d.ts +107 -37
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/utilities/deterministic.d.ts +37 -0
  74. package/dist/utilities/deterministic.d.ts.map +1 -0
  75. package/dist/utilities/index.d.ts +3 -3
  76. package/dist/utilities/index.d.ts.map +1 -1
  77. package/dist/utilities/line-endings.d.ts +5 -0
  78. package/dist/utilities/line-endings.d.ts.map +1 -0
  79. package/dist/utilities/markdown.d.ts +47 -21
  80. package/dist/utilities/markdown.d.ts.map +1 -1
  81. package/dist/utilities/message-store.d.ts +6 -0
  82. package/dist/utilities/message-store.d.ts.map +1 -0
  83. package/dist/utilities/message.d.ts +9 -3
  84. package/dist/utilities/message.d.ts.map +1 -1
  85. package/dist/utilities/tool-calls.d.ts +4 -4
  86. package/dist/utilities/tool-calls.d.ts.map +1 -1
  87. package/dist/utilities/tool-results.d.ts +10 -0
  88. package/dist/utilities/tool-results.d.ts.map +1 -0
  89. package/dist/utilities/transient.d.ts +47 -0
  90. package/dist/utilities/transient.d.ts.map +1 -0
  91. package/dist/utilities.d.ts +6 -4
  92. package/dist/utilities.d.ts.map +1 -1
  93. package/dist/versioning/index.d.ts +3 -0
  94. package/dist/versioning/index.d.ts.map +1 -0
  95. package/dist/versioning/index.js +58 -0
  96. package/dist/versioning/index.js.map +11 -0
  97. package/dist/with-conversation.d.ts +8 -8
  98. package/dist/with-conversation.d.ts.map +1 -1
  99. package/package.json +26 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A TypeScript-first library for managing LLM conversation state with **immutable updates**, **type-safe APIs**, and **provider-agnostic adapters**.
4
4
 
5
- [![Tests](https://github.com/stevekinney/conversationalist/actions/workflows/test.yml/badge.svg)](https://github.com/stevekinney/conversationalist/actions/workflows/test.yml)
5
+ [![CI](https://github.com/stevekinney/conversationalist/actions/workflows/ci.yml/badge.svg)](https://github.com/stevekinney/conversationalist/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  ## What is Conversationalist?
@@ -13,7 +13,7 @@ In a modern AI application, a conversation is more than just a list of strings.
13
13
 
14
14
  - **Tool Use**: Pairing function calls with their results and ensuring they stay in sync.
15
15
  - **Hidden Logic**: Internal "thought" messages or snapshots that should be saved but never sent to the provider.
16
- - **Metadata**: Tracking tags, custom IDs, and tokens across different steps.
16
+ - **Metadata**: Tracking custom IDs and tokens across different steps.
17
17
  - **Streaming**: Gracefully handling partial messages in a UI without messy state transitions.
18
18
 
19
19
  Conversationalist handles these complexities through a robust, type-safe API that treats your conversation as the "Single Source of Truth."
@@ -25,7 +25,7 @@ Managing LLM conversations manually often leads to "provider lock-in" or fragile
25
25
  - **Decoupling Logic from Providers**: Write your business logic once using Conversationalist's message model, and use adapters to talk to OpenAI, Anthropic, or Gemini.
26
26
  - **Built-in Context Management**: Automatically handle context window limits by truncating history while preserving critical system instructions or recent messages.
27
27
  - **Type Safety Out-of-the-Box**: Built with Zod and TypeScript, ensuring that your conversation data is valid at runtime and compile-time.
28
- - **Unified Serialization**: One standard format (`ConversationJSON`) for your database, your frontend, and your backend.
28
+ - **Unified Serialization**: One standard format (`Conversation`) for your database, your frontend, and your backend.
29
29
 
30
30
  ## The Immutable Advantage
31
31
 
@@ -65,7 +65,6 @@ import {
65
65
  appendAssistantMessage,
66
66
  appendUserMessage,
67
67
  createConversation,
68
- serializeConversation,
69
68
  } from 'conversationalist';
70
69
  import { toOpenAIMessages } from 'conversationalist/openai';
71
70
 
@@ -82,17 +81,14 @@ conversation = appendAssistantMessage(conversation, 'Let me check that for you.'
82
81
  // 3. Adapt for a provider
83
82
  const openAIMessages = toOpenAIMessages(conversation);
84
83
  // [{ role: 'user', content: 'Where is my order?' }, ...]
85
-
86
- // 4. Save to your database
87
- const data = serializeConversation(conversation);
88
- // db.save(data.id, JSON.stringify(data));
89
84
  ```
90
85
 
91
86
  ## Core Concepts
92
87
 
93
88
  ### Conversations
94
89
 
95
- A conversation is an immutable record with metadata, tags, timestamps, and ordered messages.
90
+ A conversation is an immutable record with metadata, timestamps, a `messages` record keyed
91
+ by message ID, and an `ids` array that preserves order.
96
92
 
97
93
  ```ts
98
94
  import { createConversation } from 'conversationalist';
@@ -101,14 +97,20 @@ const conversation = createConversation({
101
97
  title: 'My Chat',
102
98
  status: 'active',
103
99
  metadata: { customerId: 'cus_123' },
104
- tags: ['support', 'vip'],
105
100
  });
106
101
  ```
107
102
 
103
+ Conversations track message order via `conversation.ids`. Every mutation keeps `ids` in sync
104
+ with `messages`. Use `getMessages(conversation)` for ordered arrays, or
105
+ `getMessageIds()` if you just need the IDs.
106
+
108
107
  ### Messages
109
108
 
110
109
  Messages have roles and can contain text or multi-modal content. Optional fields include
111
- `metadata`, `hidden`, `tokenUsage`, `toolCall`, `toolResult`, and `goalCompleted`.
110
+ `metadata`, `hidden`, `tokenUsage`, `toolCall`, and `toolResult`. Assistant messages can also
111
+ include `goalCompleted` (see `AssistantMessage`).
112
+ Use `isAssistantMessage` to narrow when you need `goalCompleted`.
113
+ Metadata and tool payloads are typed as `JSONValue` so conversations remain JSON-serializable.
112
114
 
113
115
  **Roles**: `user`, `assistant`, `system`, `developer`, `tool-use`, `tool-result`, `snapshot`.
114
116
  The `snapshot` role is for internal state and is skipped by adapters.
@@ -220,19 +222,15 @@ boundTruncate(4000); // Uses tiktokenEstimator automatically
220
222
 
221
223
  ### Markdown Conversion
222
224
 
223
- Convert conversations to human-readable Markdown format, or parse Markdown back into a conversation object.
225
+ Convert conversations to human-readable Markdown format, or parse Markdown back into a conversation object. These helpers live in `conversationalist/markdown`.
224
226
 
225
227
  #### Basic Usage (Clean Markdown)
226
228
 
227
229
  By default, `toMarkdown` produces clean, readable Markdown without metadata:
228
230
 
229
231
  ```ts
230
- import {
231
- toMarkdown,
232
- fromMarkdown,
233
- createConversation,
234
- appendMessages,
235
- } from 'conversationalist';
232
+ import { appendMessages, createConversation } from 'conversationalist';
233
+ import { fromMarkdown, toMarkdown } from 'conversationalist/markdown';
236
234
 
237
235
  let conversation = createConversation({ id: 'conv-1' });
238
236
  conversation = appendMessages(
@@ -272,7 +270,6 @@ const markdown = toMarkdown(conversation, { includeMetadata: true });
272
270
  // id: conv-1
273
271
  // status: active
274
272
  // metadata: {}
275
- // tags: []
276
273
  // createdAt: '2024-01-15T10:00:00.000Z'
277
274
  // updatedAt: '2024-01-15T10:01:00.000Z'
278
275
  // messages:
@@ -298,7 +295,7 @@ const markdown = toMarkdown(conversation, { includeMetadata: true });
298
295
  // Parse back with all metadata preserved
299
296
  const restored = fromMarkdown(markdown);
300
297
  // restored.id === 'conv-1'
301
- // restored.messages[0].id === 'msg-1'
298
+ // restored.ids[0] === 'msg-1'
302
299
  ```
303
300
 
304
301
  #### Multi-Modal Content
@@ -326,18 +323,15 @@ const md = toMarkdown(conversation);
326
323
 
327
324
  ### PII Redaction Plugin
328
325
 
329
- The library includes a built-in `piiRedactionPlugin` that can automatically redact emails, phone numbers, and common API key patterns.
326
+ The library includes a built-in `redactPii` plugin that can automatically redact emails, phone numbers, and common API key patterns.
330
327
 
331
328
  ```ts
332
- import {
333
- appendUserMessage,
334
- createConversation,
335
- piiRedactionPlugin,
336
- } from 'conversationalist';
329
+ import { appendUserMessage, createConversation, getMessages } from 'conversationalist';
330
+ import { redactPii } from 'conversationalist/redaction';
337
331
 
338
332
  // 1. Enable by adding to your environment
339
333
  const env = {
340
- plugins: [piiRedactionPlugin],
334
+ plugins: [redactPii],
341
335
  };
342
336
 
343
337
  // 2. Use the environment when appending messages
@@ -349,7 +343,7 @@ conversation = appendUserMessage(
349
343
  env,
350
344
  );
351
345
 
352
- console.log(conversation.messages[0].content);
346
+ console.log(getMessages(conversation)[0]?.content);
353
347
  // "Contact me at [EMAIL_REDACTED]"
354
348
  ```
355
349
 
@@ -357,7 +351,7 @@ When using `ConversationHistory`, you only need to provide the plugin once durin
357
351
 
358
352
  ```ts
359
353
  const history = new ConversationHistory(createConversation(), {
360
- plugins: [piiRedactionPlugin],
354
+ plugins: [redactPii],
361
355
  });
362
356
 
363
357
  const appendUser = history.bind(appendUserMessage);
@@ -374,6 +368,7 @@ import { toAnthropicMessages } from 'conversationalist/anthropic';
374
368
  import { toGeminiMessages } from 'conversationalist/gemini';
375
369
  ```
376
370
 
371
+ - Adapter outputs are SDK-compatible (OpenAI `ChatCompletionMessageParam[]`, Anthropic `MessageParam[]`, Gemini `Content[]`).
377
372
  - **OpenAI**: Supports `toOpenAIMessages` and `toOpenAIMessagesGrouped` (which groups consecutive tool calls).
378
373
  - **Anthropic**: Maps system messages and tool blocks to Anthropic's specific format.
379
374
  - **Gemini**: Handles Gemini's unique content/part structure.
@@ -533,6 +528,8 @@ history.truncateToTokenLimit(4000);
533
528
  const messages = history.getMessages();
534
529
  const stats = history.getStatistics();
535
530
  const tokens = history.estimateTokens();
531
+ const ids = history.ids;
532
+ const firstMessage = history.get(ids[0]!);
536
533
  ```
537
534
 
538
535
  ### Event Subscription
@@ -587,10 +584,10 @@ history.undo();
587
584
  history.appendUserMessage('Path B');
588
585
 
589
586
  console.log(history.branchCount); // 2
590
- console.log(history.current.messages[0].content); // "Path B"
587
+ console.log(history.getMessages()[0]?.content); // "Path B"
591
588
 
592
589
  history.switchToBranch(0);
593
- console.log(history.current.messages[0].content); // "Path A"
590
+ console.log(history.getMessages()[0]?.content); // "Path A"
594
591
  ```
595
592
 
596
593
  ### Serialization
@@ -598,26 +595,118 @@ console.log(history.current.messages[0].content); // "Path A"
598
595
  You can serialize the entire history tree (including all branches) to JSON and reconstruct it later.
599
596
 
600
597
  ```ts
601
- // 1. Save to JSON
602
- const json = history.toJSON();
603
- // localStorage.setItem('chat_history', JSON.stringify(json));
598
+ // 1. Capture a snapshot
599
+ const snapshot = history.snapshot();
600
+ // localStorage.setItem('chat_history', JSON.stringify(snapshot));
604
601
 
605
- // 2. Restore from JSON
606
- const restored = ConversationHistory.from(json);
602
+ // 2. Restore from a snapshot
603
+ const restored = ConversationHistory.from(snapshot);
607
604
 
608
605
  // You can also provide a new environment (e.g. with fresh token counters)
609
- const restoredWithEnv = ConversationHistory.from(json, {
606
+ const restoredWithEnv = ConversationHistory.from(snapshot, {
610
607
  estimateTokens: myNewEstimator,
611
608
  });
612
609
  ```
613
610
 
611
+ ## Advanced Serialization
612
+
613
+ ### Schema Versioning
614
+
615
+ Conversations include a `schemaVersion` field for forward compatibility. When loading older data, use `migrateConversation` to upgrade it to the current schema:
616
+
617
+ ```ts
618
+ import { deserializeConversation } from 'conversationalist';
619
+ import {
620
+ migrateConversation,
621
+ CURRENT_SCHEMA_VERSION,
622
+ } from 'conversationalist/versioning';
623
+
624
+ // Old data without schemaVersion
625
+ const legacyData = JSON.parse(oldStorage);
626
+ const migrated = migrateConversation(legacyData);
627
+ // migrated.schemaVersion === CURRENT_SCHEMA_VERSION
628
+
629
+ const conversation = deserializeConversation(migrated);
630
+ ```
631
+
632
+ Conversations are already JSON-serializable; persist them directly and apply utilities
633
+ like `stripTransientMetadata` or `redactMessageAtPosition` when you need to sanitize data.
634
+
635
+ ### Transient Metadata Convention
636
+
637
+ Keys prefixed with `_` are considered transient—temporary UI state that shouldn't be persisted:
638
+
639
+ ```ts
640
+ import {
641
+ isTransientKey,
642
+ stripTransientFromRecord,
643
+ stripTransientMetadata,
644
+ } from 'conversationalist';
645
+
646
+ // Check if a key is transient
647
+ isTransientKey('_tempId'); // true
648
+ isTransientKey('source'); // false
649
+
650
+ // Strip transient keys from a metadata object
651
+ stripTransientFromRecord({ _loading: true, source: 'web' });
652
+ // { source: 'web' }
653
+
654
+ // Strip transient metadata from an entire conversation
655
+ const cleaned = stripTransientMetadata(conversation);
656
+ ```
657
+
658
+ ### Sort Utilities
659
+
660
+ For reproducible snapshots or tests, use the sort utilities:
661
+
662
+ ```ts
663
+ import { sortObjectKeys, sortMessagesByPosition } from 'conversationalist/sort';
664
+
665
+ // Sort object keys alphabetically (recursive)
666
+ const sorted = sortObjectKeys({ z: 1, a: 2, nested: { b: 3, a: 4 } });
667
+ // { a: 2, nested: { a: 4, b: 3 }, z: 1 }
668
+
669
+ // Sort messages by position, createdAt, then id
670
+ const orderedMessages = sortMessagesByPosition(messages);
671
+ ```
672
+
673
+ ### Role Labels
674
+
675
+ Export human-readable labels for message roles:
676
+
677
+ ```ts
678
+ import {
679
+ ROLE_LABELS,
680
+ LABEL_TO_ROLE,
681
+ getRoleLabel,
682
+ getRoleFromLabel,
683
+ } from 'conversationalist/markdown';
684
+
685
+ // Get display label for a role
686
+ getRoleLabel('tool-use'); // 'Tool Use'
687
+ getRoleLabel('assistant'); // 'Assistant'
688
+
689
+ // Get role from a label
690
+ getRoleFromLabel('Tool Result'); // 'tool-result'
691
+ getRoleFromLabel('Unknown'); // undefined
692
+
693
+ // Access the mappings directly
694
+ ROLE_LABELS['developer']; // 'Developer'
695
+ LABEL_TO_ROLE['System']; // 'system'
696
+ ```
697
+
614
698
  ### Markdown Serialization
615
699
 
616
700
  You can also convert a conversation to Markdown format for human-readable storage or export, and restore it later.
617
701
 
618
702
  ```ts
703
+ import { ConversationHistory } from 'conversationalist';
704
+ import { historyFromMarkdown, historyToMarkdown } from 'conversationalist/markdown';
705
+
706
+ const history = new ConversationHistory();
707
+
619
708
  // Export to clean, readable Markdown
620
- const markdown = history.toMarkdown();
709
+ const markdown = historyToMarkdown(history);
621
710
  // ### User
622
711
  //
623
712
  // Hello!
@@ -627,10 +716,30 @@ const markdown = history.toMarkdown();
627
716
  // Hi there!
628
717
 
629
718
  // Export with full metadata (lossless round-trip)
630
- const markdownWithMetadata = history.toMarkdown({ includeMetadata: true });
719
+ const markdownWithMetadata = historyToMarkdown(history, { includeMetadata: true });
720
+
721
+ // Export with additional controls (redaction, transient stripping, hidden handling)
722
+ const markdownSafe = historyToMarkdown(history, {
723
+ includeMetadata: true,
724
+ stripTransient: true,
725
+ redactToolArguments: true,
726
+ redactToolResults: true,
727
+ includeHidden: false,
728
+ });
631
729
 
632
730
  // Restore from Markdown
633
- const restored = ConversationHistory.fromMarkdown(markdownWithMetadata);
731
+ const restored = historyFromMarkdown(markdownWithMetadata);
732
+ ```
733
+
734
+ ### Export Helpers
735
+
736
+ For markdown export workflows, use the built-in helpers:
737
+
738
+ ```ts
739
+ import { exportMarkdown, normalizeLineEndings } from 'conversationalist/export';
740
+
741
+ const normalizedMarkdown = exportMarkdown(conversation, { includeMetadata: true });
742
+ const normalized = normalizeLineEndings('line1\r\nline2');
634
743
  ```
635
744
 
636
745
  ## Integration
@@ -641,7 +750,7 @@ Because **Conversationalist** is immutable, it works perfectly with React's `use
641
750
 
642
751
  ```tsx
643
752
  import { useState } from 'react';
644
- import { createConversation, appendUserMessage } from 'conversationalist';
753
+ import { appendUserMessage, createConversation, getMessages } from 'conversationalist';
645
754
 
646
755
  export function ChatApp() {
647
756
  const [conversation, setConversation] = useState(() => createConversation());
@@ -653,7 +762,7 @@ export function ChatApp() {
653
762
 
654
763
  return (
655
764
  <div>
656
- {conversation.messages.map((m) => (
765
+ {getMessages(conversation).map((m) => (
657
766
  <div key={m.id}>{String(m.content)}</div>
658
767
  ))}
659
768
  <button onClick={() => handleSend('Hello!')}>Send</button>
@@ -668,7 +777,7 @@ For more complex applications, you can wrap the logic into a custom hook. This e
668
777
 
669
778
  ```tsx
670
779
  import { useState, useCallback, useEffect } from 'react';
671
- import { createConversation, ConversationHistory } from 'conversationalist';
780
+ import { ConversationHistory, createConversation, getMessages } from 'conversationalist';
672
781
 
673
782
  export function useChat(initialTitle?: string) {
674
783
  // 1. Initialize history (this could also come from context or props)
@@ -712,7 +821,7 @@ export function useChat(initialTitle?: string) {
712
821
 
713
822
  return {
714
823
  conversation,
715
- messages: conversation.messages,
824
+ messages: getMessages(conversation),
716
825
  loading,
717
826
  sendMessage,
718
827
  undo: () => history.undo(),
@@ -756,7 +865,11 @@ In Svelte 5, you can manage conversation state using the `$state` rune. Since **
756
865
 
757
866
  ```svelte
758
867
  <script lang="ts">
759
- import { createConversation, appendUserMessage } from 'conversationalist';
868
+ import {
869
+ appendUserMessage,
870
+ createConversation,
871
+ getMessages,
872
+ } from 'conversationalist';
760
873
 
761
874
  let conversation = $state(createConversation());
762
875
 
@@ -766,7 +879,7 @@ In Svelte 5, you can manage conversation state using the `$state` rune. Since **
766
879
  </script>
767
880
 
768
881
  <div>
769
- {#each conversation.messages as m (m.id)}
882
+ {#each getMessages(conversation) as m (m.id)}
770
883
  <div>{String(m.content)}</div>
771
884
  {/each}
772
885
  <button onclick={() => handleSend('Hello!')}>Send</button>
@@ -779,14 +892,14 @@ Svelte 5's runes pair perfectly with **Conversationalist**. You can use the `Con
779
892
 
780
893
  ```svelte
781
894
  <script lang="ts">
782
- import { ConversationHistory } from 'conversationalist';
895
+ import { ConversationHistory, getMessages } from 'conversationalist';
783
896
 
784
897
  // history implements the Svelte store contract
785
898
  const history = new ConversationHistory();
786
899
  </script>
787
900
 
788
901
  <div>
789
- {#each $history.messages as m (m.id)}
902
+ {#each getMessages($history) as m (m.id)}
790
903
  <div>{String(m.content)}</div>
791
904
  {/each}
792
905
  <button onclick={() => history.appendUserMessage('Hello!')}>
@@ -799,16 +912,72 @@ Svelte 5's runes pair perfectly with **Conversationalist**. You can use the `Con
799
912
 
800
913
  ## API Overview
801
914
 
802
- | Category | Key Functions |
803
- | :--------------- | :------------------------------------------------------------------------------------------------------- |
804
- | **Creation** | `createConversation`, `serializeConversation`, `deserializeConversation` |
805
- | **Appending** | `appendUserMessage`, `appendAssistantMessage`, `appendSystemMessage`, `appendMessages` |
806
- | **Streaming** | `appendStreamingMessage`, `updateStreamingMessage`, `finalizeStreamingMessage`, `cancelStreamingMessage` |
807
- | **Modification** | `redactMessageAtPosition`, `replaceSystemMessage`, `collapseSystemMessages` |
808
- | **Context** | `truncateToTokenLimit`, `getRecentMessages`, `estimateConversationTokens` |
809
- | **Querying** | `getConversationMessages`, `getMessageByIdentifier`, `computeConversationStatistics` |
810
- | **Conversion** | `toMarkdown`, `fromMarkdown`, `toChatMessages`, `pairToolCallsWithResults` |
811
- | **History** | `ConversationHistory`, `bindToConversationHistory` |
915
+ | Category | Key Functions |
916
+ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
917
+ | **Creation** | `createConversation`, `deserializeConversation` |
918
+ | **Appending** | `appendUserMessage`, `appendAssistantMessage`, `appendSystemMessage`, `appendMessages` |
919
+ | **Streaming** | `appendStreamingMessage`, `updateStreamingMessage`, `finalizeStreamingMessage`, `cancelStreamingMessage` |
920
+ | **Modification** | `redactMessageAtPosition`, `replaceSystemMessage`, `collapseSystemMessages` |
921
+ | **Context** | `truncateToTokenLimit`, `getRecentMessages`, `estimateConversationTokens` |
922
+ | **Querying** | `getMessages`, `getMessageIds`, `getMessageById`, `getStatistics` |
923
+ | **Conversion** | `toChatMessages`, `pairToolCallsWithResults` |
924
+ | **Markdown** | `toMarkdown`, `fromMarkdown`, `historyToMarkdown`, `historyFromMarkdown` (from `conversationalist/markdown`) |
925
+ | **Export** | `exportMarkdown`, `normalizeLineEndings` (from `conversationalist/export`) |
926
+ | **Schemas** | `conversationSchema`, `messageJSONSchema`, `messageInputSchema`, `messageRoleSchema`, `multiModalContentSchema`, `jsonValueSchema`, `toolCallSchema`, `toolResultSchema`, `tokenUsageSchema` (from `conversationalist/schemas`) |
927
+ | **Role Labels** | `ROLE_LABELS`, `LABEL_TO_ROLE`, `getRoleLabel`, `getRoleFromLabel` (from `conversationalist/markdown`) |
928
+ | **Transient** | `isTransientKey`, `stripTransientFromRecord`, `stripTransientMetadata` |
929
+ | **Redaction** | `redactPii`, `createPIIRedactionPlugin`, `createPIIRedaction`, `DEFAULT_PII_RULES` (from `conversationalist/redaction`) |
930
+ | **Versioning** | `migrateConversation`, `CURRENT_SCHEMA_VERSION` (from `conversationalist/versioning`) |
931
+ | **Sort** | `sortObjectKeys`, `sortMessagesByPosition` (from `conversationalist/sort`) |
932
+ | **History** | `ConversationHistory` |
933
+
934
+ ## Standard Schema Compliance
935
+
936
+ All exported Zod schemas implement the [Standard Schema](https://standardschema.dev/) specification via Zod's built-in support. This means they can be used with any Standard Schema-compatible tool without library-specific adapters.
937
+
938
+ ### Exported Schemas
939
+
940
+ | Schema | Purpose |
941
+ | :------------------------ | :---------------------------------- |
942
+ | `conversationSchema` | Complete conversation with metadata |
943
+ | `jsonValueSchema` | JSON-serializable values |
944
+ | `messageJSONSchema` | Serialized message format |
945
+ | `messageInputSchema` | Input for creating messages |
946
+ | `messageRoleSchema` | Valid message roles enum |
947
+ | `multiModalContentSchema` | Text or image content |
948
+ | `toolCallSchema` | Tool function calls |
949
+ | `toolResultSchema` | Tool execution results |
950
+ | `tokenUsageSchema` | Token usage statistics |
951
+
952
+ ### Usage with Standard Schema Consumers
953
+
954
+ ```ts
955
+ import { conversationSchema } from 'conversationalist/schemas';
956
+
957
+ // Access the Standard Schema interface
958
+ const standardSchema = conversationSchema['~standard'];
959
+
960
+ // Use with any Standard Schema consumer
961
+ const result = standardSchema.validate(unknownData);
962
+ if (result.issues) {
963
+ console.error('Validation failed:', result.issues);
964
+ } else {
965
+ console.log('Valid conversation:', result.value);
966
+ }
967
+ ```
968
+
969
+ ### Type Inference
970
+
971
+ Standard Schema preserves type information:
972
+
973
+ ```ts
974
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
975
+ import { conversationSchema } from 'conversationalist/schemas';
976
+
977
+ // Type is inferred correctly
978
+ type ConversationInput = StandardSchemaV1.InferInput<typeof conversationSchema>;
979
+ type ConversationOutput = StandardSchemaV1.InferOutput<typeof conversationSchema>;
980
+ ```
812
981
 
813
982
  ## Deterministic Environments (Testing)
814
983
 
@@ -9,14 +9,19 @@ export interface AnthropicTextBlock {
9
9
  /**
10
10
  * Anthropic image content block.
11
11
  */
12
+ export interface AnthropicBase64ImageSource {
13
+ type: 'base64';
14
+ media_type: string;
15
+ data: string;
16
+ }
17
+ export interface AnthropicUrlImageSource {
18
+ type: 'url';
19
+ url: string;
20
+ }
21
+ export type AnthropicImageSource = AnthropicBase64ImageSource | AnthropicUrlImageSource;
12
22
  export interface AnthropicImageBlock {
13
23
  type: 'image';
14
- source: {
15
- type: 'base64' | 'url';
16
- media_type?: string;
17
- data?: string;
18
- url?: string;
19
- };
24
+ source: AnthropicImageSource;
20
25
  }
21
26
  /**
22
27
  * Anthropic tool use content block.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/anthropic/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAiC,MAAM,aAAa,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;QACvB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,aAAa,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAC7B,kBAAkB,GAClB,mBAAmB,GACnB,qBAAqB,GACrB,wBAAwB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AA+GD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,YAAY,GAAG,qBAAqB,CAiFrF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/anthropic/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAiC,MAAM,aAAa,CAAC;AAG/E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,oBAAoB,GAAG,0BAA0B,GAAG,uBAAuB,CAAC;AAExF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,aAAa,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAC7B,kBAAkB,GAClB,mBAAmB,GACnB,qBAAqB,GACrB,wBAAwB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AA+GD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,YAAY,GAAG,qBAAqB,CAkFrF"}
@@ -1,3 +1,15 @@
1
+ // src/utilities/message-store.ts
2
+ function getOrderedMessages(conversation) {
3
+ const ordered = [];
4
+ for (const id of conversation.ids) {
5
+ const message = conversation.messages[id];
6
+ if (message) {
7
+ ordered.push(message);
8
+ }
9
+ }
10
+ return ordered;
11
+ }
12
+
1
13
  // src/adapters/anthropic/index.ts
2
14
  function toAnthropicContent(content) {
3
15
  if (typeof content === "string") {
@@ -75,7 +87,8 @@ function extractSystemContent(messages) {
75
87
  `);
76
88
  }
77
89
  function toAnthropicMessages(conversation) {
78
- const system = extractSystemContent(conversation.messages);
90
+ const ordered = getOrderedMessages(conversation);
91
+ const system = extractSystemContent(ordered);
79
92
  const messages = [];
80
93
  let currentRole = null;
81
94
  let currentBlocks = [];
@@ -89,7 +102,7 @@ function toAnthropicMessages(conversation) {
89
102
  }
90
103
  currentRole = null;
91
104
  };
92
- for (const message of conversation.messages) {
105
+ for (const message of ordered) {
93
106
  if (message.hidden)
94
107
  continue;
95
108
  if (message.role === "system" || message.role === "developer") {
@@ -144,4 +157,4 @@ export {
144
157
  toAnthropicMessages
145
158
  };
146
159
 
147
- //# debugId=46DDFFB8D921BA7364756E2164756E21
160
+ //# debugId=77199835081EDC2864756E2164756E21
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/adapters/anthropic/index.ts"],
3
+ "sources": ["../../../src/utilities/message-store.ts", "../../../src/adapters/anthropic/index.ts"],
4
4
  "sourcesContent": [
5
- "import type { MultiModalContent } from '@lasercat/homogenaize';\n\nimport type { Conversation, Message, ToolCall, ToolResult } from '../../types';\n\n/**\n * Anthropic text content block.\n */\nexport interface AnthropicTextBlock {\n type: 'text';\n text: string;\n}\n\n/**\n * Anthropic image content block.\n */\nexport interface AnthropicImageBlock {\n type: 'image';\n source: {\n type: 'base64' | 'url';\n media_type?: string;\n data?: string;\n url?: string;\n };\n}\n\n/**\n * Anthropic tool use content block.\n */\nexport interface AnthropicToolUseBlock {\n type: 'tool_use';\n id: string;\n name: string;\n input: unknown;\n}\n\n/**\n * Anthropic tool result content block.\n */\nexport interface AnthropicToolResultBlock {\n type: 'tool_result';\n tool_use_id: string;\n content: string;\n is_error?: boolean;\n}\n\n/**\n * Anthropic content block union type.\n */\nexport type AnthropicContentBlock =\n | AnthropicTextBlock\n | AnthropicImageBlock\n | AnthropicToolUseBlock\n | AnthropicToolResultBlock;\n\n/**\n * Anthropic message format for the Messages API.\n */\nexport interface AnthropicMessage {\n role: 'user' | 'assistant';\n content: string | AnthropicContentBlock[];\n}\n\n/**\n * Result of converting a conversation to Anthropic format.\n * System messages are extracted separately since Anthropic uses a top-level system parameter.\n */\nexport interface AnthropicConversation {\n system?: string;\n messages: AnthropicMessage[];\n}\n\n/**\n * Converts internal multi-modal content to Anthropic content blocks.\n */\nfunction toAnthropicContent(\n content: string | ReadonlyArray<MultiModalContent>,\n): string | AnthropicContentBlock[] {\n if (typeof content === 'string') {\n return content;\n }\n\n const blocks: AnthropicContentBlock[] = [];\n for (const part of content) {\n if (part.type === 'text') {\n blocks.push({ type: 'text', text: part.text ?? '' });\n } else if (part.type === 'image') {\n // Anthropic supports both URL and base64\n const url = part.url ?? '';\n if (url.startsWith('data:')) {\n // Base64 data URL\n const matches = url.match(/^data:([^;]+);base64,(.+)$/);\n if (matches && matches[1] && matches[2]) {\n blocks.push({\n type: 'image',\n source: {\n type: 'base64',\n media_type: matches[1],\n data: matches[2],\n },\n });\n }\n } else {\n // Regular URL\n blocks.push({\n type: 'image',\n source: {\n type: 'url',\n url,\n },\n });\n }\n }\n }\n\n return blocks.length === 1 && blocks[0]?.type === 'text' ? blocks[0].text : blocks;\n}\n\n/**\n * Converts an internal ToolCall to Anthropic tool_use block.\n */\nfunction toToolUseBlock(toolCall: ToolCall): AnthropicToolUseBlock {\n return {\n type: 'tool_use',\n id: toolCall.id,\n name: toolCall.name,\n input:\n typeof toolCall.arguments === 'string'\n ? JSON.parse(toolCall.arguments)\n : toolCall.arguments,\n };\n}\n\n/**\n * Converts an internal ToolResult to Anthropic tool_result block.\n */\nfunction toToolResultBlock(toolResult: ToolResult): AnthropicToolResultBlock {\n const result: AnthropicToolResultBlock = {\n type: 'tool_result',\n tool_use_id: toolResult.callId,\n content:\n typeof toolResult.content === 'string'\n ? toolResult.content\n : JSON.stringify(toolResult.content),\n };\n\n if (toolResult.outcome === 'error') {\n result.is_error = true;\n }\n\n return result;\n}\n\n/**\n * Collects system message content from a conversation.\n */\nfunction extractSystemContent(messages: ReadonlyArray<Message>): string | undefined {\n const systemMessages = messages.filter(\n (m) => (m.role === 'system' || m.role === 'developer') && !m.hidden,\n );\n\n if (systemMessages.length === 0) {\n return undefined;\n }\n\n const parts: string[] = [];\n for (const msg of systemMessages) {\n if (typeof msg.content === 'string') {\n parts.push(msg.content);\n } else {\n for (const part of msg.content) {\n if (part.type === 'text') {\n parts.push(part.text ?? '');\n }\n }\n }\n }\n\n return parts.join('\\n\\n');\n}\n\n/**\n * Converts a conversation to Anthropic Messages API format.\n * System messages are extracted to the top-level `system` field.\n * Tool calls become tool_use blocks, tool results become tool_result blocks.\n *\n * @example\n * ```ts\n * import { toAnthropicMessages } from 'conversationalist/anthropic';\n *\n * const { system, messages } = toAnthropicMessages(conversation);\n * const response = await anthropic.messages.create({\n * model: 'claude-3-opus-20240229',\n * system,\n * messages,\n * });\n * ```\n */\nexport function toAnthropicMessages(conversation: Conversation): AnthropicConversation {\n const system = extractSystemContent(conversation.messages);\n const messages: AnthropicMessage[] = [];\n\n // Track pending content blocks to merge consecutive same-role messages\n let currentRole: 'user' | 'assistant' | null = null;\n let currentBlocks: AnthropicContentBlock[] = [];\n\n const flushCurrent = () => {\n if (currentRole && currentBlocks.length > 0) {\n messages.push({\n role: currentRole,\n content:\n currentBlocks.length === 1 && currentBlocks[0]?.type === 'text'\n ? currentBlocks[0].text\n : currentBlocks,\n });\n currentBlocks = [];\n }\n currentRole = null;\n };\n\n for (const message of conversation.messages) {\n if (message.hidden) continue;\n\n // Skip system messages (already extracted)\n if (message.role === 'system' || message.role === 'developer') {\n continue;\n }\n\n // Skip snapshots\n if (message.role === 'snapshot') {\n continue;\n }\n\n let targetRole: 'user' | 'assistant';\n let blocks: AnthropicContentBlock[] = [];\n\n if (message.role === 'user') {\n targetRole = 'user';\n const content = toAnthropicContent(message.content);\n if (typeof content === 'string') {\n blocks = [{ type: 'text', text: content }];\n } else {\n blocks = content;\n }\n } else if (message.role === 'assistant') {\n targetRole = 'assistant';\n const content = toAnthropicContent(message.content);\n if (typeof content === 'string') {\n blocks = [{ type: 'text', text: content }];\n } else {\n blocks = content;\n }\n } else if (message.role === 'tool-use' && message.toolCall) {\n targetRole = 'assistant';\n blocks = [toToolUseBlock(message.toolCall)];\n } else if (message.role === 'tool-result' && message.toolResult) {\n targetRole = 'user';\n blocks = [toToolResultBlock(message.toolResult)];\n } else {\n continue;\n }\n\n // Merge with current or start new\n if (currentRole === targetRole) {\n currentBlocks.push(...blocks);\n } else {\n flushCurrent();\n currentRole = targetRole;\n currentBlocks = blocks;\n }\n }\n\n flushCurrent();\n\n const result: AnthropicConversation = { messages };\n if (system !== undefined) {\n result.system = system;\n }\n return result;\n}\n"
5
+ "import type { Conversation, Message } from '../types';\n\nexport function getOrderedMessages(conversation: Conversation): Message[] {\n const ordered: Message[] = [];\n for (const id of conversation.ids) {\n const message = conversation.messages[id];\n if (message) {\n ordered.push(message);\n }\n }\n return ordered;\n}\n\nexport function toIdRecord<T extends { id: string }>(\n items: readonly T[],\n): Record<string, T> {\n const record: Record<string, T> = {};\n for (const item of items) {\n record[item.id] = item;\n }\n return record;\n}\n",
6
+ "import type { MultiModalContent } from '@lasercat/homogenaize';\n\nimport type { Conversation, Message, ToolCall, ToolResult } from '../../types';\nimport { getOrderedMessages } from '../../utilities/message-store';\n\n/**\n * Anthropic text content block.\n */\nexport interface AnthropicTextBlock {\n type: 'text';\n text: string;\n}\n\n/**\n * Anthropic image content block.\n */\nexport interface AnthropicBase64ImageSource {\n type: 'base64';\n media_type: string;\n data: string;\n}\n\nexport interface AnthropicUrlImageSource {\n type: 'url';\n url: string;\n}\n\nexport type AnthropicImageSource = AnthropicBase64ImageSource | AnthropicUrlImageSource;\n\nexport interface AnthropicImageBlock {\n type: 'image';\n source: AnthropicImageSource;\n}\n\n/**\n * Anthropic tool use content block.\n */\nexport interface AnthropicToolUseBlock {\n type: 'tool_use';\n id: string;\n name: string;\n input: unknown;\n}\n\n/**\n * Anthropic tool result content block.\n */\nexport interface AnthropicToolResultBlock {\n type: 'tool_result';\n tool_use_id: string;\n content: string;\n is_error?: boolean;\n}\n\n/**\n * Anthropic content block union type.\n */\nexport type AnthropicContentBlock =\n | AnthropicTextBlock\n | AnthropicImageBlock\n | AnthropicToolUseBlock\n | AnthropicToolResultBlock;\n\n/**\n * Anthropic message format for the Messages API.\n */\nexport interface AnthropicMessage {\n role: 'user' | 'assistant';\n content: string | AnthropicContentBlock[];\n}\n\n/**\n * Result of converting a conversation to Anthropic format.\n * System messages are extracted separately since Anthropic uses a top-level system parameter.\n */\nexport interface AnthropicConversation {\n system?: string;\n messages: AnthropicMessage[];\n}\n\n/**\n * Converts internal multi-modal content to Anthropic content blocks.\n */\nfunction toAnthropicContent(\n content: string | ReadonlyArray<MultiModalContent>,\n): string | AnthropicContentBlock[] {\n if (typeof content === 'string') {\n return content;\n }\n\n const blocks: AnthropicContentBlock[] = [];\n for (const part of content) {\n if (part.type === 'text') {\n blocks.push({ type: 'text', text: part.text ?? '' });\n } else if (part.type === 'image') {\n // Anthropic supports both URL and base64\n const url = part.url ?? '';\n if (url.startsWith('data:')) {\n // Base64 data URL\n const matches = url.match(/^data:([^;]+);base64,(.+)$/);\n if (matches && matches[1] && matches[2]) {\n blocks.push({\n type: 'image',\n source: {\n type: 'base64',\n media_type: matches[1],\n data: matches[2],\n },\n });\n }\n } else {\n // Regular URL\n blocks.push({\n type: 'image',\n source: {\n type: 'url',\n url,\n },\n });\n }\n }\n }\n\n return blocks.length === 1 && blocks[0]?.type === 'text' ? blocks[0].text : blocks;\n}\n\n/**\n * Converts an internal ToolCall to Anthropic tool_use block.\n */\nfunction toToolUseBlock(toolCall: ToolCall): AnthropicToolUseBlock {\n return {\n type: 'tool_use',\n id: toolCall.id,\n name: toolCall.name,\n input:\n typeof toolCall.arguments === 'string'\n ? JSON.parse(toolCall.arguments)\n : toolCall.arguments,\n };\n}\n\n/**\n * Converts an internal ToolResult to Anthropic tool_result block.\n */\nfunction toToolResultBlock(toolResult: ToolResult): AnthropicToolResultBlock {\n const result: AnthropicToolResultBlock = {\n type: 'tool_result',\n tool_use_id: toolResult.callId,\n content:\n typeof toolResult.content === 'string'\n ? toolResult.content\n : JSON.stringify(toolResult.content),\n };\n\n if (toolResult.outcome === 'error') {\n result.is_error = true;\n }\n\n return result;\n}\n\n/**\n * Collects system message content from a conversation.\n */\nfunction extractSystemContent(messages: ReadonlyArray<Message>): string | undefined {\n const systemMessages = messages.filter(\n (m) => (m.role === 'system' || m.role === 'developer') && !m.hidden,\n );\n\n if (systemMessages.length === 0) {\n return undefined;\n }\n\n const parts: string[] = [];\n for (const msg of systemMessages) {\n if (typeof msg.content === 'string') {\n parts.push(msg.content);\n } else {\n for (const part of msg.content) {\n if (part.type === 'text') {\n parts.push(part.text ?? '');\n }\n }\n }\n }\n\n return parts.join('\\n\\n');\n}\n\n/**\n * Converts a conversation to Anthropic Messages API format.\n * System messages are extracted to the top-level `system` field.\n * Tool calls become tool_use blocks, tool results become tool_result blocks.\n *\n * @example\n * ```ts\n * import { toAnthropicMessages } from 'conversationalist/anthropic';\n *\n * const { system, messages } = toAnthropicMessages(conversation);\n * const response = await anthropic.messages.create({\n * model: 'claude-3-opus-20240229',\n * system,\n * messages,\n * });\n * ```\n */\nexport function toAnthropicMessages(conversation: Conversation): AnthropicConversation {\n const ordered = getOrderedMessages(conversation);\n const system = extractSystemContent(ordered);\n const messages: AnthropicMessage[] = [];\n\n // Track pending content blocks to merge consecutive same-role messages\n let currentRole: 'user' | 'assistant' | null = null;\n let currentBlocks: AnthropicContentBlock[] = [];\n\n const flushCurrent = () => {\n if (currentRole && currentBlocks.length > 0) {\n messages.push({\n role: currentRole,\n content:\n currentBlocks.length === 1 && currentBlocks[0]?.type === 'text'\n ? currentBlocks[0].text\n : currentBlocks,\n });\n currentBlocks = [];\n }\n currentRole = null;\n };\n\n for (const message of ordered) {\n if (message.hidden) continue;\n\n // Skip system messages (already extracted)\n if (message.role === 'system' || message.role === 'developer') {\n continue;\n }\n\n // Skip snapshots\n if (message.role === 'snapshot') {\n continue;\n }\n\n let targetRole: 'user' | 'assistant';\n let blocks: AnthropicContentBlock[] = [];\n\n if (message.role === 'user') {\n targetRole = 'user';\n const content = toAnthropicContent(message.content);\n if (typeof content === 'string') {\n blocks = [{ type: 'text', text: content }];\n } else {\n blocks = content;\n }\n } else if (message.role === 'assistant') {\n targetRole = 'assistant';\n const content = toAnthropicContent(message.content);\n if (typeof content === 'string') {\n blocks = [{ type: 'text', text: content }];\n } else {\n blocks = content;\n }\n } else if (message.role === 'tool-use' && message.toolCall) {\n targetRole = 'assistant';\n blocks = [toToolUseBlock(message.toolCall)];\n } else if (message.role === 'tool-result' && message.toolResult) {\n targetRole = 'user';\n blocks = [toToolResultBlock(message.toolResult)];\n } else {\n continue;\n }\n\n // Merge with current or start new\n if (currentRole === targetRole) {\n currentBlocks.push(...blocks);\n } else {\n flushCurrent();\n currentRole = targetRole;\n currentBlocks = blocks;\n }\n }\n\n flushCurrent();\n\n const result: AnthropicConversation = { messages };\n if (system !== undefined) {\n result.system = system;\n }\n return result;\n}\n"
6
7
  ],
7
- "mappings": "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",
8
- "debugId": "46DDFFB8D921BA7364756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "77199835081EDC2864756E2164756E21",
9
10
  "names": []
10
11
  }