9da7c517be1dec544511d1bff6868d0bad0e8df7
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/cmd/jp/main.go b/cmd/jp/main.go
2index 0da8cfa1ad958185d0d5b02001dc707aeb1662b6..c963a648cb69e449299a6b66b4a5602f1325c142 100644
3--- a/cmd/jp/main.go
4+++ b/cmd/jp/main.go
5@@ -56,7 +56,7 @@ func main() {
6 ctx, cancel := context.WithTimeout(context.Background(), warmupTimeout)
7 err := gameClient.Warmup(ctx, []string{llm.GameSystemPrompt(sc.Brief)})
8 if err == nil {
9- err = scratchClient.Warmup(ctx, []string{llm.JudgeSystemPrompt(), llm.CompactionPrompt()})
10+ err = scratchClient.Warmup(ctx, []string{llm.JudgeSystemPrompt(), llm.CompactionPrompt(), llm.SheetSystemPrompt()})
11 }
12 cancel()
13 if err != nil {
14@@ -77,6 +77,7 @@ func buildOrchestrator(cfg *config.Config, state *game.State, hc *http.Client, g
15 gameModel := &adapters.GameModel{Client: gameClient}
16 compactor := &adapters.Compactor{Client: scratchClient}
17 judgeModel := &adapters.JudgeModel{Client: scratchClient}
18+ sheetModel := &adapters.SheetModel{Client: scratchClient}
19
20 speechIn := &adapters.SpeechInput{
21 Recorder: stt.NewRecorder(cfg.RecordCommand, recordCap),
22@@ -87,7 +88,7 @@ func buildOrchestrator(cfg *config.Config, state *game.State, hc *http.Client, g
23 Player: tts.NewPlayer(),
24 }
25
26- return game.NewOrchestrator(state, gameModel, judgeModel, compactor, speechIn, speechOut)
27+ return game.NewOrchestrator(state, gameModel, judgeModel, compactor, sheetModel, speechIn, speechOut)
28 }
29
30 func fatalf(format string, args ...any) {
31diff --git a/internal/adapters/llm.go b/internal/adapters/llm.go
32index 378a097f779c095aa86459980c036cf15cfbd051..d80a5398fabed120959c4b16ea3ac628f41ca8b2 100644
33--- a/internal/adapters/llm.go
34+++ b/internal/adapters/llm.go
35@@ -58,6 +58,22 @@ func (m *Compactor) Summarize(ctx context.Context, location string, msgs []llm.M
36 return strings.TrimSpace(raw), nil
37 }
38
39+// SheetModel adapts an llm.Client to game.SheetModel. It generates one
40+// person's character sheet from their introducing reply and parses the
41+// six-field sheet contract.
42+type SheetModel struct {
43+ Client *llm.Client
44+}
45+
46+func (m *SheetModel) GenerateSheet(ctx context.Context, brief, location, talk, intro, firstLine string) (llm.Sheet, error) {
47+ raw, err := m.Client.Generate(ctx, llm.BuildSheetMessages(brief, location, talk, intro, firstLine))
48+ if err != nil {
49+ return llm.Sheet{}, err
50+ }
51+ dumpRaw("sheet:"+location+":"+talk, "", raw)
52+ return llm.ParseSheet(raw)
53+}
54+
55 // JudgeModel adapts an llm.Client to game.JudgeModel. It issues the separate,
56 // persona-free judge request and parses the strict three-field result.
57 type JudgeModel struct {
58diff --git a/internal/game/orchestrator.go b/internal/game/orchestrator.go
59index 1b0696a44dd87c49e6e645985fee000ec9bec517..256fc511244ec4c231915c4616eb1d7a69f70f0a 100644
60--- a/internal/game/orchestrator.go
61+++ b/internal/game/orchestrator.go
62@@ -38,6 +38,15 @@ type Compactor interface {
63 Summarize(ctx context.Context, location string, msgs []llm.Message) (string, error)
64 }
65
66+// SheetModel generates one person's character sheet from their introducing reply.
67+type SheetModel interface {
68+ GenerateSheet(ctx context.Context, brief, location, talk, intro, firstLine string) (llm.Sheet, error)
69+}
70+
71+// newPartnerNudge is the user message that pulls a new partner's first line
72+// after their character sheet is registered.
73+const newPartnerNudge = "(The scene continues. The person the player is now talking to speaks first.)"
74+
75 // ErrEmptyTranscript is returned when push-to-talk produced no usable transcript.
76 var ErrEmptyTranscript = errors.New("game: empty transcript")
77
78@@ -62,6 +71,7 @@ type Orchestrator struct {
79 game GameModel
80 judge JudgeModel
81 compactor Compactor
82+ sheet SheetModel
83 speechIn SpeechInput
84 speechOut SpeechOutput
85
86@@ -69,13 +79,14 @@ type Orchestrator struct {
87 budget int
88 }
89
90-// NewOrchestrator wires an orchestrator over a state and the five adapters.
91-func NewOrchestrator(state *State, g GameModel, j JudgeModel, c Compactor, in SpeechInput, out SpeechOutput) *Orchestrator {
92+// NewOrchestrator wires an orchestrator over a state and the six adapters.
93+func NewOrchestrator(state *State, g GameModel, j JudgeModel, c Compactor, sh SheetModel, in SpeechInput, out SpeechOutput) *Orchestrator {
94 return &Orchestrator{
95 state: state,
96 game: g,
97 judge: j,
98 compactor: c,
99+ sheet: sh,
100 speechIn: in,
101 speechOut: out,
102 staticChars: utf8.RuneCountInString(llm.GameSystemPrompt(state.Brief())),
103@@ -104,13 +115,14 @@ func (o *Orchestrator) ActionTurn(ctx context.Context, action string) TurnResult
104 }
105 res.Reply = reply
106 o.applyReplyAndCompact(ctx, reply)
107+ shown, desc := o.handleNewPartner(ctx, reply)
108
109- if hasSpeech(reply) {
110- if err := o.speechOut.Speak(ctx, reply.Kana); err != nil {
111+ if hasSpeech(shown) {
112+ if err := o.speechOut.Speak(ctx, shown.Kana); err != nil {
113 res.SpeakErr = err
114 }
115 }
116- o.state.RecordDisplay(displayFromReply(action, reply, llm.JudgeResult{}, false))
117+ o.state.RecordDisplay(displayFromTurn(action, shown, desc, llm.JudgeResult{}, false))
118 return res
119 }
120
121@@ -160,9 +172,10 @@ func (o *Orchestrator) FinishSpeak(ctx context.Context) TurnResult {
122 res.HasJudge = true
123 res.JudgeErr = jerr
124 o.applyReplyAndCompact(ctx, reply)
125+ shown, desc := o.handleNewPartner(ctx, reply)
126
127- if hasSpeech(reply) {
128- if err := o.speechOut.Speak(ctx, reply.Kana); err != nil {
129+ if hasSpeech(shown) {
130+ if err := o.speechOut.Speak(ctx, shown.Kana); err != nil {
131 res.SpeakErr = err
132 }
133 }
134@@ -170,7 +183,7 @@ func (o *Orchestrator) FinishSpeak(ctx context.Context) TurnResult {
135 if jerr == nil {
136 judge = jres
137 }
138- o.state.RecordDisplay(displayFromReply("", reply, judge, jerr == nil))
139+ o.state.RecordDisplay(displayFromTurn("", shown, desc, judge, jerr == nil))
140 return res
141 }
142
143@@ -217,8 +230,66 @@ func (o *Orchestrator) compactOpenIfOverBudget(ctx context.Context) error {
144 return nil
145 }
146
147-func displayFromReply(action string, r llm.GameReply, j llm.JudgeResult, hasJudge bool) DisplayEntry {
148- e := DisplayEntry{Action: action, Desc: r.Desc, Romaji: r.Romaji, English: r.English, HasSpeech: hasSpeech(r)}
149+// handleNewPartner runs the new-partner flow for an applied reply. When the
150+// reply introduces a conversation partner with no stored sheet, it generates
151+// and registers that sheet; if the intro carried no speech, it then records a
152+// nudge and fires one continuation advance so the person speaks from their
153+// sheet. It returns the reply to display and speak (the continuation when one
154+// fired) plus the merged narration desc for the whole turn. Sheet and
155+// continuation failures are best-effort: the intro stands alone.
156+func (o *Orchestrator) handleNewPartner(ctx context.Context, reply llm.GameReply) (llm.GameReply, string) {
157+ if !o.isNewPartner() {
158+ return reply, reply.Desc
159+ }
160+ o.generateSheet(ctx, reply)
161+ if hasSpeech(reply) {