feat: add history messages, reasoning content, and adaptive language support

- Add optional history parameter to Run/RunStream for conversation context
- Add Reasoning/ReasoningDelta fields to completion and stream types
- Update llama.cpp adapter to propagate reasoning content from responses
- Default persona language now adapts to the user's language dynamically
This commit is contained in:
Victor Hugo Vargas Servin 2026-07-05 16:17:37 -07:00
parent cd13f79bb3
commit a38f63683b
8 changed files with 46 additions and 31 deletions

View file

@ -206,8 +206,8 @@ Bucle iterativo entre LLM y ejecución de tools. Es el "cerebro" que orquesta to
```go
type Loop interface {
Run(ctx context.Context, input string) (Response, error)
RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error]
Run(ctx context.Context, input string, history ...Message) (Response, error)
RunStream(ctx context.Context, input string, history ...Message) iter.Seq2[Chunk, error]
}
type Config struct {

View file

@ -206,8 +206,8 @@ Iterative loop between LLM and tool execution. It's the "brain" that orchestrate
```go
type Loop interface {
Run(ctx context.Context, input string) (Response, error)
RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error]
Run(ctx context.Context, input string, history ...Message) (Response, error)
RunStream(ctx context.Context, input string, history ...Message) iter.Seq2[Chunk, error]
}
type Config struct {

View file

@ -23,8 +23,8 @@ while iteration < MaxIterations:
```go
type Loop interface {
Run(ctx context.Context, input string) (Response, error)
RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error]
Run(ctx context.Context, input string, history ...llm.Message) (Response, error)
RunStream(ctx context.Context, input string, history ...llm.Message) iter.Seq2[Chunk, error]
}
type Config struct {

View file

@ -20,8 +20,8 @@ while iteration < MaxIterations:
```go
type Loop interface {
Run(ctx context.Context, input string) (Response, error)
RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error]
Run(ctx context.Context, input string, history ...llm.Message) (Response, error)
RunStream(ctx context.Context, input string, history ...llm.Message) iter.Seq2[Chunk, error]
}
type Config struct {

View file

@ -75,10 +75,12 @@ func New(cfg Config) *Loop {
}
// Run executes the agent loop and returns the final response.
func (l *Loop) Run(ctx context.Context, input string) (Response, error) {
// Optional history messages are appended after the system prompt and before
// the new user input.
func (l *Loop) Run(ctx context.Context, input string, history ...llm.Message) (Response, error) {
start := time.Now()
messages := l.buildInitialMessages(input)
messages := l.buildInitialMessages(input, history)
var finalContent string
var allToolCalls []llm.ToolCall
var totalUsage llm.TokenUsage
@ -137,9 +139,11 @@ func (l *Loop) Run(ctx context.Context, input string) (Response, error) {
}
// RunStream executes the agent loop with streaming output.
func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.StreamChunk, error] {
// Optional history messages are appended after the system prompt and before
// the new user input.
func (l *Loop) RunStream(ctx context.Context, input string, history ...llm.Message) iter.Seq2[llm.StreamChunk, error] {
return func(yield func(llm.StreamChunk, error) bool) {
messages := l.buildInitialMessages(input)
messages := l.buildInitialMessages(input, history)
iterations := 0
for iterations < l.cfg.MaxIters {
@ -177,7 +181,7 @@ func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.Stream
}
}
if !hasToolCalls && chunk.Delta != "" {
if !hasToolCalls && (chunk.Delta != "" || chunk.ReasoningDelta != "") {
responseBuilder.WriteString(chunk.Delta)
if !yield(chunk, nil) {
return
@ -194,12 +198,13 @@ func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.Stream
}
}
func (l *Loop) buildInitialMessages(input string) []llm.Message {
func (l *Loop) buildInitialMessages(input string, history []llm.Message) []llm.Message {
systemPrompt := persona.AssembleSystemPrompt(l.cfg.Persona, "")
return []llm.Message{
{Role: llm.RoleSystem, Content: systemPrompt},
{Role: llm.RoleUser, Content: input},
}
messages := make([]llm.Message, 0, len(history)+2)
messages = append(messages, llm.Message{Role: llm.RoleSystem, Content: systemPrompt})
messages = append(messages, history...)
messages = append(messages, llm.Message{Role: llm.RoleUser, Content: input})
return messages
}
func (l *Loop) getToolSchemas() []json.RawMessage {

View file

@ -125,6 +125,7 @@ func (c *Client) Stream(ctx context.Context, req llm.CompletionRequest) iter.Seq
for _, choice := range event.Choices {
chunk := llm.StreamChunk{
Delta: choice.Delta.Content,
ReasoningDelta: choice.Delta.ReasoningContent,
}
if choice.FinishReason != "" {
chunk.FinishReason = choice.FinishReason
@ -210,6 +211,7 @@ func (c *Client) toResponse(resp llamaChatResponse) llm.CompletionResponse {
ID: resp.ID,
Model: resp.Model,
Content: choice.Message.Content,
Reasoning: choice.Message.ReasoningContent,
StopReason: choice.FinishReason,
}
@ -272,6 +274,7 @@ type llamaChoice struct {
type llamaMessageResult struct {
Role string `json:"role"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
ToolCalls []llamaToolCall `json:"tool_calls"`
}
@ -307,6 +310,7 @@ type llamaStreamChoice struct {
type llamaStreamDelta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
Role string `json:"role"`
ToolCalls []llamaStreamToolCall `json:"tool_calls"`
}

View file

@ -82,6 +82,7 @@ type CompletionResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Content string `json:"content"`
Reasoning string `json:"reasoning,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
StopReason string `json:"stop_reason"`
Usage TokenUsage `json:"usage,omitempty"`
@ -106,6 +107,7 @@ type ToolCall struct {
// StreamChunk is emitted by the iterator returned from Stream().
type StreamChunk struct {
Delta string `json:"delta"`
ReasoningDelta string `json:"reasoning_delta,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
Usage TokenUsage `json:"usage,omitempty"` // only present on final chunk

View file

@ -37,7 +37,7 @@ func DefaultPersona() Persona {
Name: "Rony",
Tone: "professional and helpful",
Style: "clear and concise",
Language: "en",
Language: "the user's language",
}
}
@ -59,8 +59,12 @@ func AssembleSystemPrompt(p Persona, agentsMD string) string {
parts = append(parts, fmt.Sprintf("Write in a %s style.", p.Style))
}
if p.Language != "" {
if strings.EqualFold(p.Language, "the user's language") {
parts = append(parts, "Respond in the same language as the user's messages.")
} else {
parts = append(parts, fmt.Sprintf("Respond in %s.", p.Language))
}
}
for _, c := range p.Constraints {
parts = append(parts, fmt.Sprintf("CONSTRAINT: %s", c))
}