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
5 changes: 5 additions & 0 deletions api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ paths:
prompt:
type: string
example: "Analyze the Vagal Kernel bottleneck."
stateless:
type: boolean
example: true
default: false
description: "force chat depth of 0, each prompt carries no context"
strategy:
type: array
description: "Ordered list of participants (e.g., ['right', 'left'] or ['right', 'right']). Defaults to ['right', 'left']."
Expand Down
28 changes: 28 additions & 0 deletions brain/history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package brain

type ChatHistory []HistoryNode

type HistoryNode struct {
User Signed
Model Signed
}

func (h *HistoryNode) Verify(sv SignVerifier) error {
if err := h.User.Verify(sv); err != nil {
return err
}
return h.Model.Verify(sv)
}

func (ch ChatHistory) Verify(sv SignVerifier) error {
for i := range ch {
if err := ch[i].Verify(sv); err != nil {
return err
}
}
return nil
}

func (ch *ChatHistory) AppendInPlace(user, model Signed) {
*ch = append(*ch, HistoryNode{User: user, Model: model})
}
2 changes: 1 addition & 1 deletion brain/mockthinker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (m *MockThinker) Think(ctx context.Context, sv SignVerifier, input Request)
return resp, args.Error(1)
}

func (m *MockThinker) Evaluate(ctx context.Context, sv SignVerifier, peerOutput Response) (Decision, error) {
func (m *MockThinker) Evaluate(ctx context.Context, sv SignVerifier, peerOutput Response, stateless bool) (Decision, error) {
args := m.Called(ctx, sv, peerOutput)
return args.Get(0).(Decision), args.Error(1)
}
Expand Down
29 changes: 17 additions & 12 deletions brain/whole.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ type Thinker interface {
// Think generates the initial response
Think(ctx context.Context, sv SignVerifier, input Request) (Response, error)
// Evaluate audits another brain's output
Evaluate(ctx context.Context, sv SignVerifier, peerOutput Response) (Decision, error)
Evaluate(ctx context.Context, sv SignVerifier, peerOutput Response, stateless bool) (Decision, error)
// Generate a Hydration message
Dream(ctx context.Context, spec *openapi3.T, sv SignVerifier) ([]SignedHydration, error)
// Wake rehydrate from the dream in a new context
Expand All @@ -114,6 +114,7 @@ type Thinker interface {
type Request struct {
T Signed
G []Signed
Stateless bool
}

type Wakeup struct {
Expand Down Expand Up @@ -305,6 +306,7 @@ type Whole struct {
type Query struct {
input string
strategy []string
stateless bool
C chan Decision
}

Expand Down Expand Up @@ -387,7 +389,7 @@ func (b *Whole) Ready() error {
return nil
}

func (b *Whole) debateCycle(ctx context.Context, strategy []string, prompt Signed, genesis ...Signed) (Decision, error) {
func (b *Whole) debateCycle(ctx context.Context, strategy []string, prompt Signed, stateless bool, genesis ...Signed) (Decision, error) {
var think Thinker
var eval Thinker
var ok bool
Expand All @@ -404,7 +406,7 @@ func (b *Whole) debateCycle(ctx context.Context, strategy []string, prompt Signe
think = b.thinkers["left"]
}
}
resp, err := think.Think(ctx, b.signVerifier, Request{T: prompt, G: genesis})
resp, err := think.Think(ctx, b.signVerifier, Request{T: prompt, G: genesis, Stateless: stateless})
if err != nil {
return &ErrorDecision{E: err}, err
}
Expand All @@ -414,7 +416,7 @@ func (b *Whole) debateCycle(ctx context.Context, strategy []string, prompt Signe
if resp.IsError() != nil {
return &ErrorDecision{E: resp.IsError()}, resp.IsError()
}
return eval.Evaluate(ctx, b.signVerifier, resp)
return eval.Evaluate(ctx, b.signVerifier, resp, false)
}

func (b *Whole) HasThinkers() int {
Expand Down Expand Up @@ -498,7 +500,7 @@ func (b *Whole) Hydrate(ctx context.Context, d SignedHydration, half string) err
return ErrUnknownHalf
}

func (b *Whole) Think(ctx context.Context, prompt string, parser *DecisionParser, strategy ...string) (Decision, error) {
func (b *Whole) Think(ctx context.Context, prompt string, parser *DecisionParser, stateless bool, strategy ...string) (Decision, error) {
var dec Decision
var cycleErr error

Expand All @@ -509,7 +511,10 @@ func (b *Whole) Think(ctx context.Context, prompt string, parser *DecisionParser
var think Thinker
for _, think = range b.thinkers {
}
resp, err := think.Think(ctx, b.signVerifier, Request{T: Signed{Data: prompt}})
resp, err := think.Think(ctx, b.signVerifier, Request{
T: Signed{Data: prompt},
Stateless: stateless,
})
if err != nil {
return &ErrorDecision{E: err}, err
}
Expand All @@ -519,9 +524,9 @@ func (b *Whole) Think(ctx context.Context, prompt string, parser *DecisionParser
if resp.IsError() != nil {
return &ErrorDecision{E: resp.IsError()}, resp.IsError()
}
dec, cycleErr = think.Evaluate(ctx, b.signVerifier, resp)
dec, cycleErr = think.Evaluate(ctx, b.signVerifier, resp, false)
default:
dec, cycleErr = b.debateCycle(ctx, strategy, Signed{Data: prompt})
dec, cycleErr = b.debateCycle(ctx, strategy, Signed{Data: prompt}, false)
}
if cycleErr != nil {
return dec, cycleErr
Expand Down Expand Up @@ -553,7 +558,7 @@ func (b *Whole) Think(ctx context.Context, prompt string, parser *DecisionParser
log.Printf("recursion failure: next prompt error: %s\n", err)
return decisions.Fold(b.signVerifier), err
}
dec, cycleErr := b.debateCycle(ctx, strategy, pp, p)
dec, cycleErr := b.debateCycle(ctx, strategy, pp, stateless, p)
if cycleErr != nil {
log.Printf("genesis prompt: %#v", p)
log.Printf("recursion failure: cycle error: %s\n", cycleErr)
Expand Down Expand Up @@ -733,7 +738,7 @@ func (b *Whole) Start(appCtx context.Context, wg *sync.WaitGroup) {
ctx, cancel := context.WithTimeout(appCtx, b.maxQueryTime*time.Duration(b.maxRecursions))
defer cancel()
log.Printf("got prompt: %s\n", q.input)
d, err := b.Think(ctx, q.input, &DecisionParser{}, q.strategy...)
d, err := b.Think(ctx, q.input, &DecisionParser{}, q.stateless, q.strategy...)
defer func() {
if d != nil {
now := time.Now()
Expand Down Expand Up @@ -809,10 +814,10 @@ func (b *Whole) Learn(ctx context.Context, req ChimetricCorrection) error {

// Debate sends a query to the brain and waits for the decision.
// It respects the internal maxQueryTime for the think loop.
func (b *Whole) Debate(ctx context.Context, input string, strategy ...string) (Decision, error) {
func (b *Whole) Debate(ctx context.Context, input string, stateless bool, strategy ...string) (Decision, error) {
c := make(chan Decision, 1)
select {
case b.queries <- Query{input: input, strategy: strategy, C: c}:
case b.queries <- Query{input: input, strategy: strategy, C: c, stateless: stateless}:
case <-ctx.Done():
return nil, ctx.Err()
}
Expand Down
47 changes: 23 additions & 24 deletions brain/whole_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestWhole_Orchestration(t *testing.T) {
mockLeft.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(resp, nil).Once()
mockLeft.On("Evaluate", ctx, mockSV, resp, mock.Anything).Return(dec, nil).Once()

result, err := b.Think(ctx, prompt, &DecisionParser{})
result, err := b.Think(ctx, prompt, &DecisionParser{}, false)

assert.NoError(t, err)
assert.Equal(t, dec, result)
Expand All @@ -96,7 +96,7 @@ func TestWhole_Orchestration(t *testing.T) {
mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(resp, nil).Once()
mockLeft.On("Evaluate", ctx, mockSV, resp, mock.Anything).Return(dec, nil).Once()

result, err := b.Think(ctx, prompt, &DecisionParser{})
result, err := b.Think(ctx, prompt, &DecisionParser{}, false)

assert.NoError(t, err)
assert.Equal(t, dec, result)
Expand Down Expand Up @@ -127,7 +127,7 @@ func TestWhole_Orchestration(t *testing.T) {
b.Start(appCtx, &wg)

// Push a query
result, err := b.Debate(appCtx, "Ping")
result, err := b.Debate(appCtx, "Ping", false)

assert.NoError(t, err)
assert.Equal(t, dec, result)
Expand All @@ -151,9 +151,8 @@ func TestWhole_Think_BranchCoverage(t *testing.T) {
signVerifier: mockSV,
}

decision, err := b.Think(ctx, prompt, &DecisionParser{})
decision, err := b.Think(ctx, prompt, &DecisionParser{}, false)

// Assertions for the &ErrorDecision return
assert.ErrorIs(t, err, ErrNoLLMBrain)
assert.IsType(t, &ErrorDecision{}, decision)
assert.Equal(t, ErrNoLLMBrain, decision.IsError())
Expand All @@ -178,7 +177,7 @@ func TestWhole_Think_BranchCoverage(t *testing.T) {
mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(resp, nil).Once()
mockRight.On("Evaluate", ctx, mockSV, resp, mock.Anything).Return(dec, nil).Once()

result, err := b.Think(ctx, prompt, &DecisionParser{})
result, err := b.Think(ctx, prompt, &DecisionParser{}, false)

assert.NoError(t, err)
assert.Equal(t, dec, result)
Expand All @@ -197,7 +196,7 @@ func TestWhole_Think_BranchCoverage(t *testing.T) {
thinkErr := errors.New("api_timeout_from_gemini")
mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return((*MockResponse)(nil), thinkErr).Once()

decision, err := b.Think(ctx, prompt, &DecisionParser{})
decision, err := b.Think(ctx, prompt, &DecisionParser{}, false)

// Verify the error is wrapped in ErrorDecision and returned
assert.ErrorIs(t, err, thinkErr)
Expand Down Expand Up @@ -227,7 +226,7 @@ func TestWhole_Think_AdvancedBranches(t *testing.T) {
leftErr := errors.New("claude_api_error")
mockLeft.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return((*MockResponse)(nil), leftErr).Once()

decision, err := b.Think(ctx, prompt, &DecisionParser{})
decision, err := b.Think(ctx, prompt, &DecisionParser{}, false)

assert.ErrorIs(t, err, leftErr)
assert.IsType(t, &ErrorDecision{}, decision)
Expand All @@ -250,7 +249,7 @@ func TestWhole_Think_AdvancedBranches(t *testing.T) {
// The code attempts Right.Think first in the dual-brain scenario
mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return((*MockResponse)(nil), rightErr).Once()

decision, err := b.Think(ctx, prompt, &DecisionParser{})
decision, err := b.Think(ctx, prompt, &DecisionParser{}, false)

assert.ErrorIs(t, err, rightErr)
assert.Equal(t, rightErr, decision.IsError())
Expand Down Expand Up @@ -283,7 +282,7 @@ func TestWhole_Think_AdvancedBranches(t *testing.T) {
// Ensure mockResp from Right is the exact object passed to Left.Evaluate
mockLeft.On("Evaluate", ctx, mockSV, resp).Return(dec, nil).Once()

result, err := b.Think(ctx, prompt, &DecisionParser{})
result, err := b.Think(ctx, prompt, &DecisionParser{}, false)

assert.NoError(t, err)
assert.Equal(t, dec, result)
Expand Down Expand Up @@ -318,7 +317,7 @@ func TestWhole_Think_ContextSanity_Fixed(t *testing.T) {
return c.Err() != nil // Verify the context passed is indeed canceled
}), mockSV, resp).Return(&ErrorDecision{E: expectedErr}, expectedErr).Once()

decision, err := b.Think(ctx, prompt, &DecisionParser{})
decision, err := b.Think(ctx, prompt, &DecisionParser{}, false)

// ASSERTIONS
assert.ErrorIs(t, err, expectedErr)
Expand Down Expand Up @@ -358,7 +357,7 @@ func TestBrain_ErrorBranches(t *testing.T) {
deadCtx, cancel := context.WithCancel(t.Context())
cancel() // Context is already dead

_, err := b.Debate(deadCtx, "test input")
_, err := b.Debate(deadCtx, "test input", false)
assert.ErrorIs(t, err, context.Canceled, "Should reach the first ctx.Done() in Push")
})
cancelApp()
Expand Down Expand Up @@ -391,7 +390,7 @@ func TestBrain_LifecycleAndErrorBranches(t *testing.T) {
mockRight.On("Think", mock.Anything, mock.Anything, mock.Anything).
Return(sentinelResponse, baseErr).Once()

res, err := b.Debate(appCtx, "trigger error")
res, err := b.Debate(appCtx, "trigger error", false)

// Note: Push returns d.Verify(). If d is ErrorDecision, it returns the internal error.
assert.Error(t, err)
Expand Down Expand Up @@ -449,7 +448,7 @@ func TestBrain_LifecycleAndErrorBranches(t *testing.T) {
reqCtx, cancelReq := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancelReq()

_, err := b.Debate(reqCtx, "slow query")
_, err := b.Debate(reqCtx, "slow query", false)

// This hits the second <-ctx.Done() in Push
assert.ErrorIs(t, err, context.DeadlineExceeded)
Expand All @@ -474,7 +473,7 @@ func TestWhole_Think_MoreUnhappyBranches(t *testing.T) {
// Branch: if resp == nil { return &ErrorDecision{E: ErrNoLLMBrain}, err }
mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(nil, nil).Once()

dec, err := b.Think(ctx, prompt, parser)
dec, err := b.Think(ctx, prompt, parser, false)
assert.ErrorIs(t, err, ErrNoLLMBrain)
assert.NotNil(t, dec)
})
Expand All @@ -486,7 +485,7 @@ func TestWhole_Think_MoreUnhappyBranches(t *testing.T) {
// Branch: if resp == nil { return &ErrorDecision{E: ErrNoLLMBrain}, err }
mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(nil, nil).Once()

dec, err := b.Think(ctx, prompt, parser)
dec, err := b.Think(ctx, prompt, parser, false)
assert.ErrorIs(t, err, ErrNoLLMBrain)
assert.NotNil(t, dec)
})
Expand All @@ -502,7 +501,7 @@ func TestWhole_Think_MoreUnhappyBranches(t *testing.T) {

mockLeft.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(resp, nil).Once()

dec, err := b.Think(ctx, prompt, parser)
dec, err := b.Think(ctx, prompt, parser, false)
assert.ErrorIs(t, err, sentinelErr)
assert.Equal(t, sentinelErr, dec.IsError())
})
Expand All @@ -518,7 +517,7 @@ func TestWhole_Think_MoreUnhappyBranches(t *testing.T) {

mockLeft.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(resp, nil).Once()

dec, err := b.Think(ctx, prompt, parser)
dec, err := b.Think(ctx, prompt, parser, false)
assert.ErrorIs(t, err, sentinelErr)
assert.Equal(t, sentinelErr, dec.IsError())
})
Expand All @@ -540,7 +539,7 @@ func TestWhole_Think_MoreUnhappyBranches(t *testing.T) {
mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(resp, nil).Once()
mockLeft.On("Evaluate", ctx, mockSV, resp).Return(dec, nil).Once()

result, err := b.Think(ctx, prompt, parser)
result, err := b.Think(ctx, prompt, parser, false)
assert.Equal(t, decSentinel, result.IsError())
assert.NoError(t, err) // Matches current logic: return dec, cycleErr (where cycleErr is nil)
})
Expand All @@ -560,7 +559,7 @@ func TestWhole_Think_MoreUnhappyBranches(t *testing.T) {
mockRight.On("Evaluate", ctx, mockSV, resp).Return(dec, nil).Once()

// Branch: if err != nil { return dec, err } (e.g., no terminal decision found)
_, err := b.Think(ctx, prompt, parser)
_, err := b.Think(ctx, prompt, parser, false)
assert.Equal(t, err, ErrNoConsensus)
})

Expand All @@ -582,7 +581,7 @@ func TestWhole_Think_MoreUnhappyBranches(t *testing.T) {
mockLeft.On("Evaluate", ctx, mockSV, resp).Return(dec, nil).Once()

// Branch: return dec, ErrNoConsensus
_, err := b.Think(ctx, prompt, parser)
_, err := b.Think(ctx, prompt, parser, false)
assert.ErrorIs(t, err, ErrNoConsensus)
})
}
Expand All @@ -605,7 +604,7 @@ func TestWhole_Think_DefaultBrainFailures(t *testing.T) {

mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(nil, nil).Once()

dec, err := b.Think(ctx, prompt, parser)
dec, err := b.Think(ctx, prompt, parser, false)

assert.ErrorIs(t, err, ErrNoLLMBrain)
assert.IsType(t, &ErrorDecision{}, dec)
Expand All @@ -624,7 +623,7 @@ func TestWhole_Think_DefaultBrainFailures(t *testing.T) {

mockRight.On("Think", ctx, mockSV, Request{T: Signed{Data: prompt}}).Return(resp, nil).Once()

dec, err := b.Think(ctx, prompt, parser)
dec, err := b.Think(ctx, prompt, parser, false)

assert.ErrorIs(t, err, sentinelErr)
assert.Equal(t, sentinelErr, dec.IsError())
Expand Down Expand Up @@ -696,7 +695,7 @@ func TestWhole_FinalTerminalBranches(t *testing.T) {
cancel() // Immediate cancellation

// Branch: case <-ctx.Done(): return nil, ctx.Err()
dec, err := b.Debate(ctx, "This send should fail")
dec, err := b.Debate(ctx, "This send should fail", false)

assert.Nil(t, dec)
assert.ErrorIs(t, err, context.Canceled)
Expand Down
Loading
Loading