@agentforge-io/core 2.2.3 → 2.2.4

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.
@@ -27,11 +27,23 @@ class ConversationService {
27
27
  });
28
28
  }
29
29
  async addMessage(params) {
30
+ // Coerce empty/whitespace-only content to a sentinel so we don't
31
+ // poison the history with rows that getAnthropicMessages would
32
+ // have to filter (and that Anthropic would 400 on replay).
33
+ // Assistant rows with structured metadata (approval bubbles,
34
+ // blocked-tool cards, etc.) keep their original empty content
35
+ // because `getAnthropicMessages` excludes them indirectly when
36
+ // the metadata kind doesn't map to natural prose.
37
+ const content = typeof params.content === 'string' && params.content.trim().length > 0
38
+ ? params.content
39
+ : params.role === 'assistant'
40
+ ? '(no response)'
41
+ : params.content;
30
42
  const msg = await this.msgRepo.create({
31
43
  conversationId: params.conversationId,
32
44
  userId: params.userId,
33
45
  role: params.role,
34
- content: params.content,
46
+ content,
35
47
  toolCalls: params.toolCalls,
36
48
  usage: params.usage,
37
49
  metadata: params.metadata,
@@ -70,8 +82,18 @@ class ConversationService {
70
82
  async getAnthropicMessages(conversationId, userId) {
71
83
  const conv = await this.loadOwned(conversationId, userId);
72
84
  const messages = await this.msgRepo.listForConversation(conv.id, userId);
85
+ // Drop system rows AND any message whose content is empty /
86
+ // whitespace-only. Anthropic rejects empty user/assistant content
87
+ // with a 400 ("messages.N: ... must have non-empty content"),
88
+ // which used to crash the chat the moment the next turn replayed
89
+ // a malformed history row (e.g. a tool-only assistant turn
90
+ // persisted with empty `content`, or a streaming aborted before
91
+ // the user message text settled). Filtering is the cheapest fix —
92
+ // we don't lose semantic information because Anthropic's API
93
+ // wouldn't accept those rows anyway.
73
94
  return messages
74
95
  .filter((m) => m.role !== 'system')
96
+ .filter((m) => typeof m.content === 'string' && m.content.trim().length > 0)
75
97
  .map((m) => ({ role: m.role, content: m.content }));
76
98
  }
77
99
  async listForUser(userId, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/core",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Framework-free AI runtime SDK. Owns: agent loop (Anthropic), conversations, tools, streaming, agent-job queue, SdkHooks. Identity, billing, infra (email/uploads/secrets) live in the host's modules — not here.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",