-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem: The Feedback() function returns an error type, but feedback is not an error - it's a normal part of the agent refinement loop. This violates Go error semantics where errors represent exceptional failures, not expected control flow.
Current Implementation (codec.go):
// Feedback creates a feedback message for the LLM, packaged as an error type
func Feedback(note string, text ...string) error {
return chatter.Feedback{Note: note, Text: text}
}Problems with Current Approach:
- Violates Go conventions: Errors should represent failures, not expected feedback loops
- Confusing API: Users must use
errors.As()to distinguish feedback from real errors - Error handling ambiguity:
if err != nildoesn't mean failure - it might mean "refinement needed" - Leaks implementation detail: The decoder-agent feedback mechanism leaks into error handling
Design Decision: Replace with an explicit Result type that clearly separates success, feedback, and error states. This makes the API intention-clear and follows Go idioms.
Required Changes:
- Define Result type in
codec.go:
// DecodeResult represents the outcome of decoding an LLM response.
// It uses a discriminated union pattern to clearly separate success, feedback, and error cases.
type DecodeResult[T any] struct {
// Value contains the successfully decoded result (only valid if IsSuccess)
Value T
// Confidence is a score from 0.0 to 1.0 indicating decoder confidence in the result
Confidence float64
// Feedback contains refinement instructions for the LLM (only valid if IsFeedback)
Feedback chatter.Content
// Err contains the actual error (only valid if IsError)
Err error
state resultState
}
type resultState uint8
const (
resultSuccess resultState = iota
resultFeedback
resultError
)
// Success creates a successful decode result
func Success[T any](confidence float64, value T) DecodeResult[T] {
return DecodeResult[T]{
Value: value,
Confidence: confidence,
state: resultSuccess,
}
}
// Feedback creates a feedback result (not an error!)
func FeedbackResult[T any](note string, text ...string) DecodeResult[T] {
var content chatter.Prompt
content.With(chatter.Content{Note: note, Text: text})
return DecodeResult[T]{
Feedback: &content,
state: resultFeedback,
}
}
// Error creates an error result (actual failure)
func DecoderError[T any](err error) DecodeResult[T] {
return DecodeResult[T]{
Err: err,
state: resultError,
}
}
// Query methods
func (r DecodeResult[T]) IsSuccess() bool { return r.state == resultSuccess }
func (r DecodeResult[T]) IsFeedback() bool { return r.state == resultFeedback }
func (r DecodeResult[T]) IsError() bool { return r.state == resultError }- Update Decoder interface in
codec.go:
// Decoder converts LLM's reply into structured object.
type Decoder[T any] interface {
// Decode transforms an LLM reply into type T.
//
// Returns:
// - Success: LLM output was successfully decoded
// - Feedback: LLM output needs refinement (agent will retry with feedback)
// - Error: Unrecoverable failure (agent will abort)
Decode(*chatter.Reply) DecodeResult[T]
}- Update FromDecoder helper in
codec/from.go:
// FromDecoder creates a Decoder[B] from a function.
// The function should return DecodeResult[B] with one of:
// - Success(confidence, value) for successful decode
// - FeedbackResult[B](note, text...) when refinement is needed
// - DecoderError[B](err) for actual errors
func FromDecoder[B any](f func(*chatter.Reply) DecodeResult[B]) thinker.Decoder[B] {
return fDecoder[B](f)
}
type fDecoder[T any] func(*chatter.Reply) DecodeResult[T]
func (f fDecoder[T]) Decode(reply *chatter.Reply) DecodeResult[T] {
return f(reply)
}- Update DecoderID in
codec/identity.go:
// Identity decoder, passes LLM reply directly as result
var DecoderID = FromDecoder(
func(reply *chatter.Reply) DecodeResult[*chatter.Reply] {
return Success(1.0, reply)
},
)
// Identity decoder, passes LLM reply as string object
var DecoderString = FromDecoder(
func(reply *chatter.Reply) DecodeResult[string] {
return Success(1.0, reply.String())
},
)- Update Automata to use Result in
agent/automata.go:
func (automata *Automata[A, B]) Prompt(ctx context.Context, input A, opt ...chatter.Opt) (B, error) {
automata.mu.Lock()
defer automata.mu.Unlock()
var nul B
state := thinker.State[B]{Phase: thinker.AGENT_ASK, Epoch: 0}
// ... LLM setup code ...
for {
reply, err := automata.llm.Prompt(ctx, shortMemory)
if err != nil {
return nul, thinker.ErrLLM.With(err)
}
// Decode result - now returns Result type
result := automata.decoder.Decode(reply)
switch {
case result.IsSuccess():
state.Confidence = result.Confidence
state.Reply = result.Value
state.Feedback = nil
case result.IsFeedback():
state.Confidence = 0.0
state.Feedback = result.Feedback
// Continue to reasoner with feedback
case result.IsError():
return nul, thinker.ErrCodec.With(result.Err)
}
state.Epoch++
if state.Phase != thinker.AGENT_RETRY {
automata.memory.Commit(thinker.NewObservation(prompt, reply))
}
phase, request, err := automata.reasoner.Deduct(state)
if err != nil {
return nul, err
}
switch phase {
case thinker.AGENT_RETURN:
return state.Reply, nil
// ... other cases
}
}
}- Update Manifold in
agent/manifold.go:
func (manifold *Manifold[A, B]) Prompt(ctx context.Context, input A, opt ...chatter.Opt) (B, error) {
manifold.mu.Lock()
defer manifold.mu.Unlock()
var nul B
// ... setup code ...
for {
reply, err := manifold.llm.Prompt(ctx, memory, opt...)
if err != nil {
return nul, thinker.ErrLLM.With(err)
}
switch reply.Stage {
case chatter.LLM_RETURN:
result := manifold.decoder.Decode(reply)
if result.IsError() {
return nul, thinker.ErrCodec.With(result.Err)
}
if result.IsFeedback() {
memory = append(memory, reply, result.Feedback)
continue
}
return result.Value, nil
// ... other cases
}
}
}- Update all examples to use new Result type:
Example (examples/02_rainbow/rainbow.go):
// OLD:
func validate(seq []string) error {
for _, x := range seq {
if strings.ToLower(x) == "ultraviolet" {
return nil
}
}
return thinker.Feedback(
"Improve the response based on feedback:",
"Missing colors from invisible spectrum",
)
}
func decode(reply *chatter.Reply) (float64, []string, error) {
var seq []string
if err := json.Unmarshal(reply.Bytes(), &seq); err != nil {
return 0.0, nil, err
}
if err := validate(seq); err != nil {
return 0.1, nil, err
}
return 1.0, seq, nil
}
// NEW:
func validate(seq []string) bool {
for _, x := range seq {
if strings.ToLower(x) == "ultraviolet" {
return true
}
}
return false
}
func decode(reply *chatter.Reply) thinker.DecodeResult[[]string] {
var seq []string
if err := json.Unmarshal(reply.Bytes(), &seq); err != nil {
return thinker.DecoderError[[]string](err)
}
if !validate(seq) {
return thinker.FeedbackResult[[]string](
"Improve the response based on feedback:",
"Missing colors from invisible spectrum",
)
}
return thinker.Success(1.0, seq)
}- Update jsonify helper in
prompt/jsonify/jsonify.go:
func (strings) Decode(reply *chatter.Reply, seq any) thinker.DecodeResult[any] {
matches := re.FindStringSubmatch(string(reply.String()))
if len(matches) == 0 {
return thinker.FeedbackResult[any](
"Improve the response based on feedback:",
"The output does not contain valid JSON list of strings.",
"No pattern [ \"string\", \"string\", ... ] is found in the output.",
)
}
if err := json.Unmarshal([]byte(matches[0]), &seq); err != nil {
return thinker.FeedbackResult[any](
"Improve the response based on feedback.",
"The output does not contain valid JSON list of strings.",
"JSON parsing of included list of strings has failed with an error "+err.Error(),
)
}
return thinker.Success(1.0, seq)
}- Update documentation in README.md and godoc:
Add section explaining Result type:
### Decoder Results
Decoders return a `DecodeResult[T]` that explicitly represents three states:
```go
// Success - LLM output decoded correctly
func decode(reply *chatter.Reply) DecodeResult[Person] {
var person Person
if err := json.Unmarshal(reply.Bytes(), &person); err != nil {
return DecoderError[Person](err) // Real error
}
if person.Age < 0 {
return FeedbackResult[Person](
"Improve response:",
"Age must be positive",
) // Agent will retry with feedback
}
return Success(1.0, person) // Success!
}Benefits:
- ✅ Clear intent: Success, Feedback, or Error
- ✅ Type-safe: Compiler ensures you handle all cases
- ✅ No confusion: Feedback is not an error
- ✅ Idiomatic: Result types are common in Go for discriminated unions
- Add comprehensive tests in
codec/result_test.go:
func TestDecodeResultSuccess(t *testing.T) {
result := Success(0.95, "test value")
it.Then(t).Should(
it.True(result.IsSuccess()),
it.False(result.IsFeedback()),
it.False(result.IsError()),
it.Equal(result.Value, "test value"),
it.Equal(result.Confidence, 0.95),
)
}
func TestDecodeResultFeedback(t *testing.T) {
result := FeedbackResult[string]("Improve", "Add more details")
it.Then(t).Should(
it.False(result.IsSuccess()),
it.True(result.IsFeedback()),
it.False(result.IsError()),
it.True(result.Feedback != nil),
)
}
func TestDecodeResultError(t *testing.T) {
err := errors.New("parse failed")
result := DecoderError[string](err)
it.Then(t).Should(
it.False(result.IsSuccess()),
it.False(result.IsFeedback()),
it.True(result.IsError()),
it.Equal(result.Err, err),
)
}Estimated Effort: 6 hours
Skills Required:
- Go type design
- Refactoring
- Understanding of discriminated unions / sum types
- Testing
Breaking Changes:
Migration Guide:
// Before:
func decode(reply *chatter.Reply) (float64, Result, error) {
if invalid {
return 0.0, Result{}, thinker.Feedback("Fix this", "Details")
}
if err != nil {
return 0.0, Result{}, err
}
return 1.0, result, nil
}
// After:
func decode(reply *chatter.Reply) thinker.DecodeResult[Result] {
if invalid {
return thinker.FeedbackResult[Result]("Fix this", "Details")
}
if err != nil {
return thinker.DecoderError[Result](err)
}
return thinker.Success(1.0, result)
}Rationale:
- ✅ Idiomatic Go: Result types clearly express intent
- ✅ Type safety: Compiler ensures all cases are handled
- ✅ Clear semantics: Feedback ≠ Error ≠ Success
- ✅ Better errors: Real errors remain errors, feedback is separate
- ✅ Maintainability: Future readers understand the flow immediately
- ✅ No magic: No need for
errors.As()type assertions
Alternative Considered: Keep current design but document better
- ❌ Rejected: Documentation doesn't fix the semantic violation
- ❌ Rejected: Still requires users to understand the error-that's-not-an-error pattern
Version Strategy:
- Introduce in v0.11.0 as breaking change
- Mark old
Feedback(string, ...string) erroras deprecated - Provide migration guide in CHANGELOG
- Consider keeping compatibility shim for one version if needed