Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions docs/prompt-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,25 +299,30 @@ OpenAI 的文件上传现在不再是“只传文件本体”的通用路径,
- 全局 completion runtime 应用点:
[internal/completionruntime/nonstream.go](../internal/completionruntime/nonstream.go)

当前输入转文件启用并触发时,上传的历史文件名由 `filename_policy` 决定,文件内容是完整 `messages` 上下文;`natural_context` 策略会生成自然化 conversation context(不再注入文件边界标签):
当前输入转文件启用并触发时,上传的历史文件名由 `filename_policy` 决定,文件内容来自完整 `messages` 上下文;默认 `hybrid_recent` 策略会生成带语义压缩的 conversation context(不再注入文件边界标签):

```text
[uploaded filename]: conversation-notes-a1b2c3.txt
# Conversation Context
This note captures the working conversation state for the current request.
This note summarizes earlier context and preserves the most recent turns needed for the current reply.

## Conversation
## Current Task
最新用户请求原文。

1. System guidance:
...
## Non-Negotiable Instructions

2. User:
...
- system / developer 约束摘要。

## Earlier Context Summary

- 较早历史的短摘要、已确认决策、已完成工具结果。

## Recent Exact Context

3. Assistant:
1. User:
...

4. Tool result (name: search, call id: call-1):
2. Tool result (name: search, call id: call-1):
...
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func TestApplyCurrentInputFileGoldenTranscriptStability(t *testing.T) {
if len(ds.uploads) != 2 {
t.Fatalf("expected history and tools uploads, got %d", len(ds.uploads))
}
const wantHistory = "# Conversation Context\nThis note captures the working conversation state for the current request.\n\n## Conversation\n\n1. System guidance:\nsystem rule\n\n2. User:\nsearch docs\n"
const wantHistory = "# Conversation Context\nThis note summarizes earlier context and preserves the most recent turns needed for the current reply.\n\n## Current Task\nsearch docs\n\n## Non-Negotiable Instructions\n\n- system rule\n\n## Recent Exact Context\n\n1. System guidance:\nsystem rule\n\n2. User:\nsearch docs\n"
if got := string(ds.uploads[0].Data); got != wantHistory {
t.Fatalf("history transcript mismatch:\n--- got ---\n%s\n--- want ---\n%s", got, wantHistory)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/httpapi/openai/history/current_input_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func currentInputFilenamePolicy(store CurrentInputConfigReader) string {
func currentInputContextStrategy(store CurrentInputConfigReader) string {
reader, ok := store.(currentInputContextStrategyReader)
if !ok {
return "natural_context"
return "hybrid_recent"
}
return config.NormalizeContextEngineStrategy(reader.ContextEngineStrategy())
}
Expand Down
158 changes: 150 additions & 8 deletions internal/promptcompat/history_transcript.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ const historyTranscriptTitle = "# DS2API_HISTORY.txt"
const historyTranscriptSummary = "Prior conversation history and tool progress."
const naturalContextTitle = "# Conversation Context"
const naturalContextSummary = "This note captures the working conversation state for the current request."
const hybridRecentExactEntries = 8
const hybridSummaryMaxEntries = 12
const hybridSummarySnippetRunes = 260

type contextHistoryEntry struct {
Role string
Label string
Content string
}

func BuildOpenAIHistoryTranscript(messages []any) string {
return buildOpenAIHistoryTranscript(messages)
Expand All @@ -33,8 +42,12 @@ func BuildOpenAICurrentInputContextTranscriptWithStrategy(messages []any, strate
switch strings.ToLower(strings.TrimSpace(strategy)) {
case "raw_transcript":
return buildOpenAIHistoryTranscript(messages)
default:
case "natural_context":
return buildOpenAINaturalContextTranscript(messages)
case "context_capsule":
return buildOpenAIHybridRecentContextTranscript(messages, false)
default:
return buildOpenAIHybridRecentContextTranscript(messages, true)
}
}

Expand Down Expand Up @@ -75,7 +88,8 @@ func buildOpenAIHistoryTranscript(messages []any) string {
}

func buildOpenAINaturalContextTranscript(messages []any) string {
if len(messages) == 0 {
entries := collectContextHistoryEntries(messages)
if len(entries) == 0 {
return ""
}
var b strings.Builder
Expand All @@ -86,6 +100,53 @@ func buildOpenAINaturalContextTranscript(messages []any) string {
b.WriteString("## Conversation\n\n")

entry := 0
for _, item := range entries {
entry++
fmt.Fprintf(&b, "%d. %s:\n%s\n\n", entry, item.Label, item.Content)
}

transcript := strings.TrimSpace(b.String())
if transcript == "" {
return ""
}
return transcript + "\n"
}

func buildOpenAIHybridRecentContextTranscript(messages []any, includeRecentExact bool) string {
entries := collectContextHistoryEntries(messages)
if len(entries) == 0 {
return ""
}
var b strings.Builder
b.WriteString(naturalContextTitle)
b.WriteString("\n")
b.WriteString("This note summarizes earlier context and preserves the most recent turns needed for the current reply.")
b.WriteString("\n\n")

if latest := latestUserContextEntry(entries); latest.Content != "" {
b.WriteString("## Current Task\n")
b.WriteString(latest.Content)
b.WriteString("\n\n")
}

writeSystemGuidanceSection(&b, entries)
writeEarlierSummarySection(&b, entries, includeRecentExact)
if includeRecentExact {
writeRecentExactSection(&b, entries)
}

transcript := strings.TrimSpace(b.String())
if transcript == "" {
return ""
}
return transcript + "\n"
}

func collectContextHistoryEntries(messages []any) []contextHistoryEntry {
if len(messages) == 0 {
return nil
}
entries := make([]contextHistoryEntry, 0, len(messages))
for _, raw := range messages {
msg, ok := raw.(map[string]any)
if !ok {
Expand All @@ -96,15 +157,96 @@ func buildOpenAINaturalContextTranscript(messages []any) string {
if content == "" {
continue
}
entry++
fmt.Fprintf(&b, "%d. %s:\n%s\n\n", entry, naturalRoleLabelForHistory(role, msg), content)
entries = append(entries, contextHistoryEntry{
Role: role,
Label: naturalRoleLabelForHistory(role, msg),
Content: content,
})
}
return entries
}

transcript := strings.TrimSpace(b.String())
if transcript == "" {
return ""
func latestUserContextEntry(entries []contextHistoryEntry) contextHistoryEntry {
for i := len(entries) - 1; i >= 0; i-- {
if entries[i].Role == "user" {
return entries[i]
}
}
return transcript + "\n"
return contextHistoryEntry{}
}

func writeSystemGuidanceSection(b *strings.Builder, entries []contextHistoryEntry) {
wroteHeader := false
for _, entry := range entries {
if entry.Role != "system" {
continue
}
if !wroteHeader {
b.WriteString("## Non-Negotiable Instructions\n\n")
wroteHeader = true
}
b.WriteString("- ")
b.WriteString(oneLineSnippet(entry.Content, hybridSummarySnippetRunes))
b.WriteString("\n")
}
if wroteHeader {
b.WriteString("\n")
}
}

func writeEarlierSummarySection(b *strings.Builder, entries []contextHistoryEntry, includeRecentExact bool) {
summaryEnd := len(entries)
if includeRecentExact {
summaryEnd -= hybridRecentExactEntries
if summaryEnd < 0 {
summaryEnd = 0
}
}
if summaryEnd <= 0 {
return
}
start := 0
if summaryEnd > hybridSummaryMaxEntries {
start = summaryEnd - hybridSummaryMaxEntries
}
b.WriteString("## Earlier Context Summary\n\n")
if start > 0 {
fmt.Fprintf(b, "- %d older messages omitted after summarization.\n", start)
}
for _, entry := range entries[start:summaryEnd] {
if entry.Role == "system" {
continue
}
fmt.Fprintf(b, "- %s: %s\n", entry.Label, oneLineSnippet(entry.Content, hybridSummarySnippetRunes))
}
b.WriteString("\n")
}

func writeRecentExactSection(b *strings.Builder, entries []contextHistoryEntry) {
start := 0
if len(entries) > hybridRecentExactEntries {
start = len(entries) - hybridRecentExactEntries
}
recent := entries[start:]
if len(recent) == 0 {
return
}
b.WriteString("## Recent Exact Context\n\n")
for i, entry := range recent {
fmt.Fprintf(b, "%d. %s:\n%s\n\n", i+1, entry.Label, entry.Content)
}
}

func oneLineSnippet(text string, maxRunes int) string {
text = strings.Join(strings.Fields(strings.TrimSpace(text)), " ")
if maxRunes <= 0 {
return text
}
runes := []rune(text)
if len(runes) <= maxRunes {
return text
}
return string(runes[:maxRunes]) + "..."
}

func buildOpenAIHistoryEntry(role string, msg map[string]any) string {
Expand Down
62 changes: 62 additions & 0 deletions internal/promptcompat/history_transcript_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package promptcompat

import (
"fmt"
"strings"
"testing"
)

func TestBuildOpenAICurrentInputContextTranscriptHybridRecentCompressesOlderMessages(t *testing.T) {
messages := []any{
map[string]any{"role": "system", "content": "keep user constraints exact"},
}
for i := 1; i <= 12; i++ {
messages = append(messages, map[string]any{"role": "user", "content": fmt.Sprintf("older detail %02d with repeated background", i)})
messages = append(messages, map[string]any{"role": "assistant", "content": fmt.Sprintf("assistant response %02d with repeated explanation", i)})
}
messages = append(messages, map[string]any{"role": "user", "content": "latest request must be exact"})

got := BuildOpenAICurrentInputContextTranscriptWithStrategy(messages, "hybrid_recent")
for _, want := range []string{
"# Conversation Context",
"## Current Task\nlatest request must be exact",
"## Non-Negotiable Instructions",
"## Earlier Context Summary",
"## Recent Exact Context",
"latest request must be exact",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected hybrid context to contain %q, got %q", want, got)
}
}
if strings.Contains(got, "DS2API") {
t.Fatalf("hybrid context should not expose implementation terms, got %q", got)
}
if strings.Contains(got, "older detail 01 with repeated background\n\n") {
t.Fatalf("oldest messages should be summarized, not kept as exact turns: %q", got)
}
}

func TestBuildOpenAICurrentInputContextTranscriptStrategyShapes(t *testing.T) {
messages := []any{
map[string]any{"role": "system", "content": "system rule"},
map[string]any{"role": "user", "content": "first turn"},
map[string]any{"role": "assistant", "content": "first answer"},
map[string]any{"role": "user", "content": "latest request"},
}

raw := BuildOpenAICurrentInputContextTranscriptWithStrategy(messages, "raw_transcript")
if !strings.Contains(raw, "# DS2API_HISTORY.txt") || !strings.Contains(raw, "=== 1. SYSTEM ===") {
t.Fatalf("expected raw transcript shape, got %q", raw)
}

natural := BuildOpenAICurrentInputContextTranscriptWithStrategy(messages, "natural_context")
if !strings.Contains(natural, "## Conversation") || strings.Contains(natural, "Earlier Context Summary") {
t.Fatalf("expected full natural context shape, got %q", natural)
}

capsule := BuildOpenAICurrentInputContextTranscriptWithStrategy(messages, "context_capsule")
if !strings.Contains(capsule, "## Current Task") || strings.Contains(capsule, "## Recent Exact Context") {
t.Fatalf("expected context capsule without recent exact section, got %q", capsule)
}
}