diff --git a/api.yaml b/api.yaml index 552604d..fb336b3 100644 --- a/api.yaml +++ b/api.yaml @@ -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']." diff --git a/brain/history.go b/brain/history.go new file mode 100644 index 0000000..80148a8 --- /dev/null +++ b/brain/history.go @@ -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}) +} diff --git a/brain/mockthinker_test.go b/brain/mockthinker_test.go index c9c3699..c71b582 100644 --- a/brain/mockthinker_test.go +++ b/brain/mockthinker_test.go @@ -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) } diff --git a/brain/whole.go b/brain/whole.go index 001baf7..70a2626 100644 --- a/brain/whole.go +++ b/brain/whole.go @@ -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 @@ -114,6 +114,7 @@ type Thinker interface { type Request struct { T Signed G []Signed + Stateless bool } type Wakeup struct { @@ -305,6 +306,7 @@ type Whole struct { type Query struct { input string strategy []string + stateless bool C chan Decision } @@ -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 @@ -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 } @@ -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 { @@ -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 @@ -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 } @@ -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 @@ -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) @@ -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() @@ -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() } diff --git a/brain/whole_test.go b/brain/whole_test.go index b28e6f0..9167f68 100644 --- a/brain/whole_test.go +++ b/brain/whole_test.go @@ -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) @@ -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) @@ -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) @@ -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()) @@ -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) @@ -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) @@ -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) @@ -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()) @@ -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) @@ -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) @@ -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() @@ -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) @@ -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) @@ -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) }) @@ -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) }) @@ -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()) }) @@ -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()) }) @@ -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) }) @@ -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) }) @@ -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) }) } @@ -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) @@ -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()) @@ -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) diff --git a/claude/claude.go b/claude/claude.go index dc75d33..3deedd2 100644 --- a/claude/claude.go +++ b/claude/claude.go @@ -25,11 +25,10 @@ type MessageGenerator interface { } type Claude struct { - cl *anthropic.Client - generator MessageGenerator // The Sliver Interface - runner ScriptRunner - thinkingPrompts []brain.Signed - thinkingResponses []brain.Signed + cl *anthropic.Client + generator MessageGenerator // The Sliver Interface + runner ScriptRunner + history brain.ChatHistory } func (g *Claude) Model() string { @@ -92,6 +91,18 @@ func (m *Claude) Chime(ctx context.Context, sv brain.SignVerifier, c brain.Chime return brain.ParseChimeReponse(ctx, sv, resp.Text().Data) } +func (c *Claude) getHistory(sv brain.SignVerifier, start int) ([]anthropic.MessageParam, error) { + content := []anthropic.MessageParam{} + for i := min(len(c.history), start); i < len(c.history); i++ { + if err := c.history[i].Verify(sv); err != nil { + return nil, err + } + content = append(content, anthropic.NewUserMessage(anthropic.NewTextBlock(c.history[i].User.Data))) + content = append(content, anthropic.NewAssistantMessage(anthropic.NewTextBlock(c.history[i].Model.Data))) + } + return content, nil +} + // Think generates the initial response func (c *Claude) Think(ctx context.Context, sv brain.SignVerifier, input brain.Request) (brain.Response, error) { now := time.Now() @@ -101,17 +112,16 @@ func (c *Claude) Think(ctx context.Context, sv brain.SignVerifier, input brain.R if c.generator == nil { return &brain.ErrorResponse{E: errors.New("claude client not initialized")}, errors.New("claude client not initialized") } + chain, err := c.getHistory(sv, 0) + if err != nil { + return &ClaudeError{e: err}, err + } prompt := input.T prompt.Namespace = brain.TypePrompt - err := prompt.Sign(sv) + err = prompt.Sign(sv) if err != nil { return &ClaudeError{e: err}, err } - chain := []anthropic.MessageParam{} - for i := range c.thinkingPrompts { - chain = append(chain, anthropic.NewUserMessage(anthropic.NewTextBlock(c.thinkingPrompts[i].Data))) - chain = append(chain, anthropic.NewAssistantMessage(anthropic.NewTextBlock(c.thinkingResponses[i].Data))) - } chain = append(chain, anthropic.NewUserMessage(anthropic.NewTextBlock(input.Text().Data))) message, err := c.generator.New(ctx, anthropic.MessageNewParams{ MaxTokens: 1024 * 4, @@ -140,9 +150,8 @@ func (c *Claude) Think(ctx context.Context, sv brain.SignVerifier, input brain.R resp: message, model: string(message.Model), } err = resp.Sign(sv) - if err == nil { - c.thinkingPrompts = append(c.thinkingPrompts, resp.Prompt(true)) - c.thinkingResponses = append(c.thinkingResponses, resp.Thought(true)) + if err == nil && !input.Stateless { + c.history.AppendInPlace(resp.Prompt(true), resp.Thought(true)) } return resp, err } @@ -251,7 +260,7 @@ func commentsToCoT(sv brain.SignVerifier, message *anthropic.ToolUseBlock, previ } // Evaluate audits another brain's output -func (c *Claude) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput brain.Response) (brain.Decision, error) { +func (c *Claude) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput brain.Response, stateless bool) (brain.Decision, error) { now := time.Now() defer func() { log.Printf("Evaluate took: %v", time.Since(now)) @@ -261,10 +270,9 @@ func (c *Claude) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput } err := peerOutput.Sign(sv) instruction := brain.GenerateEvalPromptText(sv, false) // TODO programatic control for long prompts with extra validation - chain := []anthropic.MessageParam{} - for i := range c.thinkingPrompts { - chain = append(chain, anthropic.NewUserMessage(anthropic.NewTextBlock(c.thinkingPrompts[i].Data))) - chain = append(chain, anthropic.NewAssistantMessage(anthropic.NewTextBlock(c.thinkingResponses[i].Data))) + chain, err := c.getHistory(sv, 0) + if err != nil { + return &brain.ErrorDecision{E: err}, err } chain = append(chain, anthropic.NewUserMessage(anthropic.NewTextBlock(peerOutput.Describe(sv)))) params := anthropic.MessageNewParams{ @@ -433,11 +441,10 @@ func (c *Claude) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput if err != nil { log.Printf("failed to sign Decision: %s", err) } - if err == nil { + if err == nil && !stateless { gp := prev.GenesisPrompt() p := gp.NextUnsigned(peerOutput.Describe(sv)) - c.thinkingPrompts = append(c.thinkingPrompts, p) - c.thinkingResponses = append(c.thinkingResponses, textblock) + c.history.AppendInPlace(p, textblock) } return prev, err } diff --git a/claude/claude_test.go b/claude/claude_test.go index 2d234a6..65ee8c8 100644 --- a/claude/claude_test.go +++ b/claude/claude_test.go @@ -251,7 +251,7 @@ func TestClaude_Evaluate_ToolUseLoop(t *testing.T) { // mockRunner.On("Run", mock.Anything, "print(1+1)").Return("2\n", nil).Once() mockPeerResp.On("Verify", mockSV).Return(nil).Maybe() - decision, err := c.Evaluate(ctx, mockSV, mockPeerResp) + decision, err := c.Evaluate(ctx, mockSV, mockPeerResp, false) // ASSERTIONS assert.NoError(t, err) diff --git a/gemini/gemini.go b/gemini/gemini.go index aec8b5c..b6ac796 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -31,7 +31,7 @@ type Gemini struct { model string cfg *genai.ClientConfig generator ContentGenerator - chat *genai.Chat + history brain.ChatHistory } func (g *Gemini) Model() string { @@ -133,6 +133,24 @@ func (g *Gemini) genConfig() *genai.GenerateContentConfig { } } +func (g *Gemini) getHistory(sv brain.SignVerifier, start int) ([]*genai.Content, error) { + content := []*genai.Content{} + for i := min(len(g.history), start); i < len(g.history); i++ { + if err := g.history[i].Verify(sv); err != nil { + return nil, err + } + content = append(content, &genai.Content{ + Role: genai.RoleUser, + Parts: []*genai.Part{{Text: g.history[i].User.Data}}, + }) + content = append(content, &genai.Content{ + Role: genai.RoleModel, + Parts: []*genai.Part{{Text: g.history[i].Model.Data}}, + }) + } + return content, nil +} + // Think generates the initial response // Think is stateful (uses chat) where Evaluate is *not* func (g *Gemini) Think(ctx context.Context, sv brain.SignVerifier, input brain.Request) (brain.Response, error) { @@ -143,24 +161,14 @@ func (g *Gemini) Think(ctx context.Context, sv brain.SignVerifier, input brain.R if g.cl == nil || g.generator == nil { return &brain.ErrorResponse{E: errors.New("gemini client not initialized")}, errors.New("gemini client not initialized") } - if g.chat == nil { - cfg := g.genConfig() - cfg.Temperature = &MidTemp - cfg.TopP = &WideTopP - inst := genai.Text(brain.ThoughtInstructions) - if len(inst) == 1 { - cfg.SystemInstruction = inst[0] - } else { - log.Printf("could not generate single part system instruction, instead we got %#v", inst) - } - if g.cl.Chats == nil { - return &GeminiError{e: errors.New("gemini client not initialized")}, errors.New("gemini client not initialized") - } - chat, err := g.cl.Chats.Create(ctx, g.model, cfg, nil) // TODO: this could contain hydration prompts/etc - if err != nil { - return &GeminiError{e: err}, err - } - g.chat = chat + cfg := g.genConfig() + cfg.Temperature = &MidTemp + cfg.TopP = &WideTopP + inst := genai.Text(brain.ThoughtInstructions) + if len(inst) == 1 { + cfg.SystemInstruction = inst[0] + } else { + log.Printf("could not generate single part system instruction, instead we got %#v", inst) } prompt := input.T prompt.Namespace = brain.TypePrompt @@ -168,10 +176,15 @@ func (g *Gemini) Think(ctx context.Context, sv brain.SignVerifier, input brain.R if err != nil { return &GeminiError{e: err}, err } - - result, err := g.chat.SendMessage(ctx, genai.Part{ - Text: input.Text().Data, + content, err := g.getHistory(sv, 0) + if err != nil { + return &GeminiError{e: err}, err + } + content = append(content, &genai.Content{ + Role: genai.RoleUser, + Parts: []*genai.Part{{Text: input.Text().Data}}, }) + result, err := g.generator.GenerateContent(ctx, g.model, content, cfg) if err != nil { return &GeminiError{e: err}, err } @@ -183,12 +196,15 @@ func (g *Gemini) Think(ctx context.Context, sv brain.SignVerifier, input brain.R err = resp.Sign(sv) if err == nil { log.Printf("Response: %s\n", resp.Text().Data) + if !input.Stateless { + g.history.AppendInPlace(resp.Prompt(true), resp.Thought(true)) + } } return resp, err } // Evaluate audits another brain's output -func (g *Gemini) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput brain.Response) (brain.Decision, error) { +func (g *Gemini) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput brain.Response, stateless bool) (brain.Decision, error) { now := time.Now() defer func() { log.Printf("Evaluate took: %v", time.Since(now)) @@ -216,7 +232,15 @@ func (g *Gemini) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput // s, _ := json.MarshalIndent(cfg, " ", " ") // log.Printf("using model instructions: %s", s) - result, err := g.generator.GenerateContent(ctx, "gemini-pro-latest", genai.Text(peerOutput.Describe(sv)), cfg) + content, err := g.getHistory(sv, 0) + if err != nil { + return &brain.ErrorDecision{E: err}, err + } + content = append(content, &genai.Content{ + Role: genai.RoleUser, + Parts: []*genai.Part{{Text: peerOutput.Describe(sv)}}, + }) + result, err := g.generator.GenerateContent(ctx, "gemini-pro-latest", content, cfg) if err != nil { return &brain.ErrorDecision{E: err}, err } @@ -249,5 +273,10 @@ func (g *Gemini) Evaluate(ctx context.Context, sv brain.SignVerifier, peerOutput return prev, err } err = prev.Sign(sv) + if err == nil && !stateless { + gp := prev.GenesisPrompt() + p := gp.NextUnsigned(peerOutput.Describe(sv)) + g.history.AppendInPlace(p, textBlock) + } return prev, err } diff --git a/gemini/gemini_test.go b/gemini/gemini_test.go index de12ecb..06e422b 100644 --- a/gemini/gemini_test.go +++ b/gemini/gemini_test.go @@ -27,7 +27,7 @@ func TestGemini_Evaluate_NoNil(t *testing.T) { badG := &Gemini{cl: nil} mockPeer := new(MockResponse) - dec, err := badG.Evaluate(ctx, mockSV, mockPeer) + dec, err := badG.Evaluate(ctx, mockSV, mockPeer, false) assert.Error(t, err) assert.Contains(t, err.Error(), "client not initialized") @@ -76,7 +76,7 @@ func TestGemini_Evaluate_NoNil(t *testing.T) { // fails with our specific error. mockPeerResp.On("Verify", mock.Anything).Return(verifyErr) - dec, err := g.Evaluate(ctx, mockSV, mockPeerResp) + dec, err := g.Evaluate(ctx, mockSV, mockPeerResp, false) // ASSERTIONS assert.ErrorIs(t, err, verifyErr) @@ -123,7 +123,7 @@ func TestGemini_Evaluate_RevealTheNil(t *testing.T) { mockPeerResp.On("Verify", mock.Anything).Return(verifyErr) // 4. EXECUTION - decision, err := g.Evaluate(ctx, mockSV, mockPeerResp) + decision, err := g.Evaluate(ctx, mockSV, mockPeerResp, false) // THE REVEAL: If the bug exists, 'decision' will be nil here. assert.NotNil(t, decision, "THE BUG REVEALED: Evaluate returned (nil, err) because NewDecision failed!") @@ -303,7 +303,7 @@ func TestGemini_Evaluate_Advanced(t *testing.T) { mockGen.On("GenerateContent", ctx, mock.Anything, mock.Anything, mock.Anything). Return(nil, genErr).Once() - dec, err := g.Evaluate(ctx, mockSV, peerOutput) + dec, err := g.Evaluate(ctx, mockSV, peerOutput, false) assert.ErrorIs(t, err, genErr) assert.IsType(t, &brain.ErrorDecision{}, dec) }) @@ -336,7 +336,7 @@ func TestGemini_Evaluate_Advanced(t *testing.T) { peerOutput.On("Prompt").Return(brain.Signed{Signature: promptSig}).Maybe() mockSV.On("Verify", mock.Anything, mock.Anything).Return(nil).Maybe() - dec, err := g.Evaluate(ctx, mockSV, peerOutput) + dec, err := g.Evaluate(ctx, mockSV, peerOutput, false) assert.NoError(t, err) assert.NotNil(t, dec) @@ -392,7 +392,7 @@ func TestGemini_Evaluate_Advanced(t *testing.T) { mockSV.On("ExportPublicKey").Return("fakePubKey").Maybe() // 4. Execution - dec, err := g.Evaluate(ctx, mockSV, peerOutput) + dec, err := g.Evaluate(ctx, mockSV, peerOutput, false) assert.NoError(t, err) assert.NotNil(t, dec) @@ -430,7 +430,7 @@ func TestGemini_Evaluate_FinalBranches(t *testing.T) { mockSV.On("Sign", mock.Anything).Return("text", nil).Maybe() mockSV.On("Verify", mock.Anything, mock.Anything).Return(nil).Maybe() - _, _ = g.Evaluate(ctx, mockSV, peerOutput) + _, _ = g.Evaluate(ctx, mockSV, peerOutput, false) // Check logs for "could not generate single part system instruction" }) diff --git a/main.go b/main.go index 8ea0fdf..caa5aa4 100644 --- a/main.go +++ b/main.go @@ -118,7 +118,8 @@ func setupRouter(backend *brain.Whole, sv brain.SignVerifier) *chi.Mux { r.Post("/think", func(w http.ResponseWriter, r *http.Request) { var req struct { Prompt string `json:"prompt"` - Strategy []string `json:"strategy"` // 🛡️ [USER_CONTROL] + Strategy []string `json:"strategy"` + Stateless bool `json:"stateless"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request, check json body", http.StatusBadRequest) @@ -131,7 +132,7 @@ func setupRouter(backend *brain.Whole, sv brain.SignVerifier) *chi.Mux { } // Use the app context or request context - decision, err := backend.Debate(r.Context(), req.Prompt, req.Strategy...) + decision, err := backend.Debate(r.Context(), req.Prompt, req.Stateless, req.Strategy...) if err != nil { // log.Printf("got decision : %#v and error %s", decision, err) // Handle the singularity