molecule-core/workspace-server/internal/handlers/hermes_messages.go
Hongming Wang 479a027e4b chore: open-source restructure — rename dirs, remove internal files, scrub secrets
Renames:
- platform/ → workspace-server/ (Go module path stays as "platform" for
  external dep compat — will update after plugin module republish)
- workspace-template/ → workspace/

Removed (moved to separate repos or deleted):
- PLAN.md — internal roadmap (move to private project board)
- HANDOFF.md, AGENTS.md — one-time internal session docs
- .claude/ — gitignored entirely (local agent config)
- infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy
- org-templates/molecule-dev/ → standalone template repo
- .mcp-eval/ → molecule-mcp-server repo
- test-results/ — ephemeral, gitignored

Security scrubbing:
- Cloudflare account/zone/KV IDs → placeholders
- Real EC2 IPs → <EC2_IP> in all docs
- CF token prefix, Neon project ID, Fly app names → redacted
- Langfuse dev credentials → parameterized
- Personal runner username/machine name → generic

Community files:
- CONTRIBUTING.md — build, test, branch conventions
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1

All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml,
README, CLAUDE.md updated for new directory names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:24:44 -07:00

77 lines
2.6 KiB
Go

package handlers
// mergeSystemMessages collapses consecutive leading system messages into a
// single system message before the payload is forwarded to a Hermes/vLLM
// endpoint.
//
// Background
// ----------
// The OpenAI-compatible vLLM server (used by Nous Hermes and similar models)
// accepts only ONE system message. When the platform constructs a messages
// array from multiple sources — e.g. a base system prompt, a workspace-level
// config block, and a per-session user override — and these are all emitted as
// consecutive {"role":"system","content":"..."} entries, vLLM either rejects
// the request or silently drops all but the first.
//
// This function is a stateless pre-flight transform that resolves the
// collision before any HTTP call is made.
//
// Rules
// -----
// 1. Scan from the front of the slice.
// 2. Collect every consecutive {"role":"system"} entry.
// 3. Join their "content" strings with "\n\n" into one system message.
// 4. Prepend the merged message to the remaining (non-system) messages.
// 5. If there is only one leading system message, the slice is returned
// unchanged (no allocation, no copy).
// 6. Non-system messages that appear BETWEEN two system messages are NOT
// considered — the merge only applies to the uninterrupted leading run.
// 7. If there are no system messages at all, the slice is returned as-is.
//
// Content types
// -------------
// "content" may be a string (the common case) or any other JSON-decoded type
// (e.g. []interface{} for multi-modal content arrays). Only string values
// are merged textually; non-string values are skipped during concatenation.
//
// Example
//
// In: [{system,"A"}, {system,"B"}, {user,"Q"}]
// Out: [{system,"A\n\nB"}, {user,"Q"}]
func mergeSystemMessages(messages []map[string]interface{}) []map[string]interface{} {
// Find the end of the leading system-message run.
end := 0
for end < len(messages) {
role, _ := messages[end]["role"].(string)
if role != "system" {
break
}
end++
}
// Zero or one system message — nothing to merge.
if end <= 1 {
return messages
}
// Concatenate content strings from the leading system messages.
var merged string
for i := 0; i < end; i++ {
content, _ := messages[i]["content"].(string)
if i == 0 {
merged = content
} else {
merged += "\n\n" + content
}
}
// Build result: one merged system message + the remaining messages.
result := make([]map[string]interface{}, 0, 1+len(messages)-end)
result = append(result, map[string]interface{}{
"role": "system",
"content": merged,
})
result = append(result, messages[end:]...)
return result
}