fix(opencode): убрать смешение слоёв API — перейти целиком на experimental (/session)
Корень проблемы «не получаем результаты»: клиент смешивал два слоя opencode
serve. CreateSession ходил на /api/session (v2, ждал {data.id}), Verdict — на
/api/session/{id}/message?order=desc и ждал {data:[{type,content}]}, где поле
content[].type/text физически отсутствует, поэтому вердикт никогда не находился
и поллинг уходил в вечный таймаут. Abort и вовсе звал несуществующий /interrupt.
Теперь весь код на experimental-слое, как сверено с sst/opencode (ветка dev):
- CreateSession: POST /session → голая Session, id в .id.
- Send: блокирующий POST /session/{id}/message, тело {parts:[{type:text,text}]},
вердикт из частей parts[].type=="text" ответа. Это и есть результат — метод
Verdict и отдельный GET удалены.
- textCount (прогресс): GET /session/{id}/message → голый массив [{info, parts}].
- Abort: POST /session/{id}/abort.
Runner: блокирующий Send запускается в горутине (канал вердикта/ошибки),
параллельно поллим textCount (рост text-частей сбрасывает idle-таймер). При
idle/hard-таймауте или отмене контекста — Abort + cancel() Send-горутины → rc=-1.
Send ходит через отдельный http.Client без жёсткого Timeout (управляется ctx),
чтобы длинная генерация не обрывалась на 30s. Тесты/fakeAPIServer переведены на
экспериментальный формат. Версия → 0.2.2.
This commit is contained in:
@@ -44,17 +44,24 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// e2eFakeAPI поднимает фейковый opencode serve HTTP API v1.18 и возвращает URL.
|
||||
// По title сессии (ratatoskr-<agent>) определяет агента и возвращает его вердикт
|
||||
// как text-часть единственного assistant-сообщения.
|
||||
// e2eFakeAPI поднимает фейковый opencode serve experimental HTTP API (пути
|
||||
// БЕЗ /api) и возвращает URL. По title сессии (ratatoskr-<agent>) определяет
|
||||
// агента и возвращает его вердикт как text-часть ответа на POST /message.
|
||||
func e2eFakeAPI(t *testing.T) string {
|
||||
t.Helper()
|
||||
var mu sync.Mutex
|
||||
sessions := map[string]string{} // id → agent
|
||||
|
||||
verdictFor := func(agent string) string {
|
||||
if v, ok := e2eAgentVerdicts[agent]; ok {
|
||||
return v
|
||||
}
|
||||
return "unknown agent"
|
||||
}
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/session":
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/session":
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
@@ -64,35 +71,27 @@ func e2eFakeAPI(t *testing.T) string {
|
||||
id := fmt.Sprintf("e2e-%d", len(sessions)+1)
|
||||
sessions[id] = agent
|
||||
mu.Unlock()
|
||||
writeJSON(w, map[string]any{"data": map[string]any{"id": id}})
|
||||
// experimental: голая Session, id напрямую.
|
||||
writeJSON(w, map[string]any{"id": id, "agent": agent, "model": map[string]any{"id": "m"}})
|
||||
|
||||
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/message"):
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
|
||||
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/wait"):
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
|
||||
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/interrupt"):
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
|
||||
case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/message"):
|
||||
id := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/api/session/"), "/message")
|
||||
// блокирующий ответ: вердикт как text-часть.
|
||||
id := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/session/"), "/message")
|
||||
mu.Lock()
|
||||
agent := sessions[id]
|
||||
mu.Unlock()
|
||||
verdict := ""
|
||||
if v, ok := e2eAgentVerdicts[agent]; ok {
|
||||
verdict = v
|
||||
} else {
|
||||
verdict = "unknown agent"
|
||||
}
|
||||
writeJSON(w, map[string]any{"data": []map[string]any{{
|
||||
"type": "assistant",
|
||||
"content": []map[string]any{{"type": "text", "text": verdict}},
|
||||
}}})
|
||||
writeJSON(w, map[string]any{"info": map[string]any{"role": "assistant"}, "parts": []map[string]any{{"type": "text", "text": verdictFor(agent)}}})
|
||||
|
||||
case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/message"):
|
||||
// поллинг прогресса: голый массив [{info, parts}].
|
||||
id := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/session/"), "/message")
|
||||
mu.Lock()
|
||||
agent := sessions[id]
|
||||
mu.Unlock()
|
||||
writeJSON(w, []map[string]any{{"info": map[string]any{"role": "assistant"}, "parts": []map[string]any{{"type": "text", "text": verdictFor(agent)}}}})
|
||||
|
||||
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/abort"):
|
||||
writeJSON(w, map[string]any{"ok": true})
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
|
||||
Reference in New Issue
Block a user