Skip to content

Commit 090d1ba

Browse files
committed
fix: DeepSeek 400 on multi-turn tool calls — echo reasoning_content back
DeepSeek's API requires reasoning_content from the model response to be echoed back in subsequent assistant messages. Without this, the second iteration after any tool call would get 400 Bad Request: 'The reasoning_content in the thinking mode must be passed back.' Changes: - CallResult.ReasoningContent — capture from API response - Message.ReasoningContent — serialize in assistant messages - parseResponse — extract reasoning_content from choice message - loop.go — echo reasoning_content in both final answer and tool-call assistant messages Also improved error logging: 400+ responses now include the response body so API errors are visible without packet capture.
1 parent 87e9e2f commit 090d1ba

3 files changed

Lines changed: 170 additions & 9 deletions

File tree

docs/DAILY-WORKER.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# odek as Your Daily Worker — Integration Proposals
2+
3+
## Strengths Comparison
4+
5+
| Capability | Hermes (you) | odek |
6+
|---|---|---|
7+
| **Coding (refactor, debug, build)** | ✅ via `delegate_task` / tools | 🏆 Go-native, 3ms startup, sub-agent farm |
8+
| **Platform integration** (Telegram, email, GitHub, Notion) | 🏆 Native | ❌ None |
9+
| **Browser / Vision / TTS** | 🏆 Native | ❌ None |
10+
| **Sub-agent isolation** | Goroutine + context | 🏆 Real OS processes, `exec.Command` |
11+
| **Memory** | Per-session + persistent | 🏆 3-tier (facts, buffer, episodes) + vector |
12+
| **Skills auto-learning** | Manual skills | 🏆 Auto-detect patterns, trie-based trigger |
13+
| **Docker sandbox** | Manual | 🏆 Wired into agent loop |
14+
| **Cron / scheduling** | 🏆 Native | ❌ None |
15+
| **Web UI** || 🏆 `odek serve` |
16+
| **MCP bidirectional** | Client only (native-mcp) | 🏆 Server + Client |
17+
18+
## Proposed Integration: Hermes Orchestrates, odek Builds
19+
20+
Top 3 patterns, ordered by value:
21+
22+
---
23+
24+
### 1. Direct CLI delegation — `odek run` for coding tasks
25+
26+
Best for: focused coding work (refactor, implement, debug)
27+
28+
```
29+
You: "Refactor the auth module to use context-based middleware"
30+
31+
Hermes:
32+
1. Spawn → odek run --session "Refactor auth to context-based middleware"
33+
2. odek reads files, plans, rewrites, tests
34+
3. Hermes receives result, verifies, reports back to you
35+
```
36+
37+
**Setup:** none — odek is already installed. Just call from terminal.
38+
39+
**Pros:** Zero infra, odek's full power, session persistence
40+
**Cons:** No Hermes tool access during task (no browser, no search)
41+
42+
---
43+
44+
### 2. MCP bridge — share tools bidirectionally
45+
46+
Best for: giving odek access to Hermes' unique tools (browser, search, vision) during coding
47+
48+
```
49+
┌─────────────┐ MCP stdio ┌─────────────┐
50+
│ Hermes │◄──────────────────►│ odek │
51+
│ │ │ │
52+
│ Telegram │ │ shell/file │
53+
│ Browser │ │ sub-agents │
54+
│ Vision │ │ sandbox │
55+
│ Cron │ │ skills │
56+
└─────────────┘ └─────────────┘
57+
```
58+
59+
**Setup:**
60+
```bash
61+
# In one terminal (or background):
62+
odek mcp # odek serves its tools via MCP
63+
64+
# Configure Hermes to connect as MCP client:
65+
# ~/.hermes/config.yaml
66+
mcp_servers:
67+
odek:
68+
command: odek
69+
args: [mcp]
70+
```
71+
72+
Then Hermes can call `odek__shell`, `odek__readFile` etc. when it needs odek's sub-agent or sandbox features.
73+
74+
**Pros:** Bidirectional tool access, no context switching
75+
**Cons:** More moving parts, latency from MCP serialization
76+
77+
---
78+
79+
### 3. odek serve — Web UI for long-running sessions
80+
81+
Best for: complex multi-hour coding tasks you want to monitor visually
82+
83+
```bash
84+
odek serve --port 3001
85+
# Open http://localhost:3001 in browser
86+
# odek runs autonomously, shows live token stats, tool calls
87+
```
88+
89+
You can start it alongside Hermes and switch between Telegram (Hermes) and the Web UI (odek) for the same task.
90+
91+
**Pros:** Visual debugging, per-message token stats, drag-drop files
92+
**Cons:** Separate window, no Telegram integration
93+
94+
---
95+
96+
### 4. Hybrid: Hermes + odek sub-agent farm (recommended daily driver)
97+
98+
Combine all three:
99+
100+
```
101+
┌─────────────────────────────────────────────────────┐
102+
│ Hermes │
103+
│ │
104+
│ Telegram DM ◄──► You │
105+
│ │
106+
│ delegate_task ──► Spawns odek sub-agents via: │
107+
│ 1. odek run "Implement feature X" │
108+
│ 2. odek run "Fix bug in Y" │
109+
│ 3. odek run --sandbox "Test Z safely" │
110+
│ │
111+
│ odek skills dir shared: ~/.odek/skills/ │
112+
│ odek memory shared: ~/.odek/memory/ │
113+
└─────────────────────────────────────────────────────┘
114+
```
115+
116+
**Setup:**
117+
```bash
118+
# Ensure odek is on PATH
119+
which odek || go install github.com/BackendStack21/kode/cmd/odek@latest
120+
121+
# Optionally share the skills directory
122+
ln -s ~/.odek/skills ~/.hermes/skills # optional
123+
```
124+
125+
Then create a Hermes skill for it:
126+
127+
<skill name="odek-delegate">
128+
Trigger: user asks to refactor / implement / debug code
129+
Steps:
130+
1. Read the relevant files for context
131+
2. Call: odek run --session "task description with full context"
132+
3. Capture result, verify files were modified
133+
4. Report back to user
134+
</skill>
135+
136+
---
137+
138+
## What I Recommend
139+
140+
Start with **Pattern 1** (direct CLI delegation). It's zero-setup and gives you immediate value:
141+
142+
```bash
143+
# Example — I'd use this daily:
144+
odek run --session "Add E2E tests for the YOLO mode config"
145+
odek run "Refactor shell.go to extract a helper function"
146+
odek run --sandbox "Run the full test suite"
147+
```
148+
149+
Add **Pattern 4** (shared skills/memory) when you want odek to learn from repeated tasks. Add **Pattern 2** (MCP bridge) when you need odek to use browser/vision during a coding task.
150+
151+
Want me to implement Pattern 1 as a Hermes skill right now?

internal/llm/client.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type Message struct {
7070
Name string `json:"name,omitempty"` // tool name (for tool role)
7171
ToolCallID string `json:"tool_call_id,omitempty"`
7272
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // required for assistant role with tool calls
73+
ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek reasoning tokens, must be echoed back
7374
CacheControl *CacheControl `json:"cache_control,omitempty"` // Anthropic prompt caching marker
7475
}
7576

@@ -116,8 +117,9 @@ type ThinkingConfig struct {
116117

117118
// CallResult is the parsed response from /chat/completions.
118119
type CallResult struct {
119-
Content string // assistant text
120-
ToolCalls []ToolCall // tool calls requested by the model
120+
Content string // assistant text
121+
ReasoningContent string // DeepSeek reasoning/thinking tokens
122+
ToolCalls []ToolCall // tool calls requested by the model
121123
InputTokens int // prompt_tokens from API usage (0 = not reported)
122124
OutputTokens int // completion_tokens from API usage (0 = not reported)
123125

@@ -295,6 +297,10 @@ func (c *Client) Call(ctx context.Context, messages []Message, systemBlocks []Sy
295297
}
296298

297299
if resp.StatusCode != http.StatusOK {
300+
body := strings.TrimSpace(string(respBytes))
301+
if body != "" {
302+
return nil, fmt.Errorf("llm: %s (status %d): %s", resp.Status, resp.StatusCode, body)
303+
}
298304
return nil, fmt.Errorf("llm: %s (status %d)", resp.Status, resp.StatusCode)
299305
}
300306

@@ -305,7 +311,8 @@ func parseResponse(data []byte) (*CallResult, error) {
305311
var raw struct {
306312
Choices []struct {
307313
Message struct {
308-
Content string `json:"content"`
314+
Content string `json:"content"`
315+
ReasoningContent string `json:"reasoning_content"`
309316
ToolCalls []struct {
310317
ID string `json:"id"`
311318
Function struct {
@@ -336,7 +343,8 @@ func parseResponse(data []byte) (*CallResult, error) {
336343

337344
msg := raw.Choices[0].Message
338345
result := &CallResult{
339-
Content: msg.Content,
346+
Content: msg.Content,
347+
ReasoningContent: msg.ReasoningContent,
340348
}
341349
if raw.Usage != nil {
342350
result.InputTokens = raw.Usage.PromptTokens

internal/loop/loop.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,9 @@ func (e *Engine) runLoop(ctx context.Context, messages []llm.Message) (string, [
304304
// Append final assistant message so callers (e.g. WebUI) get
305305
// the final text in the messages slice and can stream it.
306306
messages = append(messages, llm.Message{
307-
Role: "assistant",
308-
Content: result.Content,
307+
Role: "assistant",
308+
Content: result.Content,
309+
ReasoningContent: result.ReasoningContent,
309310
})
310311
return result.Content, messages, nil
311312
}
@@ -317,9 +318,10 @@ func (e *Engine) runLoop(ctx context.Context, messages []llm.Message) (string, [
317318

318319
// Build assistant message with tool calls
319320
assistantMsg := llm.Message{
320-
Role: "assistant",
321-
Content: result.Content,
322-
ToolCalls: result.ToolCalls,
321+
Role: "assistant",
322+
Content: result.Content,
323+
ReasoningContent: result.ReasoningContent,
324+
ToolCalls: result.ToolCalls,
323325
}
324326
messages = append(messages, assistantMsg)
325327

0 commit comments

Comments
 (0)