@pi-in-go/pigpen-acp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CREDITS.md +23 -0
  2. package/LICENSE +22 -0
  3. package/README.md +110 -0
  4. package/extensions/acp/cmd/pig-acp/e2e_llm_test.go +168 -0
  5. package/extensions/acp/cmd/pig-acp/e2e_test.go +541 -0
  6. package/extensions/acp/cmd/pig-acp/go.mod +6 -0
  7. package/extensions/acp/cmd/pig-acp/internal/acp/agent.go +784 -0
  8. package/extensions/acp/cmd/pig-acp/internal/acp/agent_builtin_test.go +536 -0
  9. package/extensions/acp/cmd/pig-acp/internal/acp/agent_commands_test.go +199 -0
  10. package/extensions/acp/cmd/pig-acp/internal/acp/agent_config_test.go +530 -0
  11. package/extensions/acp/cmd/pig-acp/internal/acp/agent_helpers_test.go +215 -0
  12. package/extensions/acp/cmd/pig-acp/internal/acp/agent_load_test.go +333 -0
  13. package/extensions/acp/cmd/pig-acp/internal/acp/agent_prompt.go +289 -0
  14. package/extensions/acp/cmd/pig-acp/internal/acp/agent_session_test.go +429 -0
  15. package/extensions/acp/cmd/pig-acp/internal/acp/auth.go +61 -0
  16. package/extensions/acp/cmd/pig-acp/internal/acp/bench_test.go +47 -0
  17. package/extensions/acp/cmd/pig-acp/internal/acp/helpers_test.go +421 -0
  18. package/extensions/acp/cmd/pig-acp/internal/acp/main_test.go +28 -0
  19. package/extensions/acp/cmd/pig-acp/internal/acp/mutation_extra_test.go +142 -0
  20. package/extensions/acp/cmd/pig-acp/internal/acp/paths.go +77 -0
  21. package/extensions/acp/cmd/pig-acp/internal/acp/pisessions.go +317 -0
  22. package/extensions/acp/cmd/pig-acp/internal/acp/protocol.go +188 -0
  23. package/extensions/acp/cmd/pig-acp/internal/acp/server.go +218 -0
  24. package/extensions/acp/cmd/pig-acp/internal/acp/server_test.go +386 -0
  25. package/extensions/acp/cmd/pig-acp/internal/acp/session.go +1068 -0
  26. package/extensions/acp/cmd/pig-acp/internal/acp/session_diff_test.go +183 -0
  27. package/extensions/acp/cmd/pig-acp/internal/acp/session_events_test.go +443 -0
  28. package/extensions/acp/cmd/pig-acp/internal/acp/session_extra_test.go +547 -0
  29. package/extensions/acp/cmd/pig-acp/internal/acp/session_usage_test.go +198 -0
  30. package/extensions/acp/cmd/pig-acp/internal/acp/settings.go +79 -0
  31. package/extensions/acp/cmd/pig-acp/internal/acp/slash.go +278 -0
  32. package/extensions/acp/cmd/pig-acp/internal/acp/slash_test.go +128 -0
  33. package/extensions/acp/cmd/pig-acp/internal/acp/startup.go +156 -0
  34. package/extensions/acp/cmd/pig-acp/internal/acp/store.go +118 -0
  35. package/extensions/acp/cmd/pig-acp/internal/acp/store_paths_test.go +466 -0
  36. package/extensions/acp/cmd/pig-acp/internal/acp/translate.go +352 -0
  37. package/extensions/acp/cmd/pig-acp/internal/acp/translate_test.go +105 -0
  38. package/extensions/acp/cmd/pig-acp/internal/acp/twin_test.go +28 -0
  39. package/extensions/acp/cmd/pig-acp/internal/acp/types.go +202 -0
  40. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/bench_test.go +24 -0
  41. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc.go +302 -0
  42. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc_test.go +320 -0
  43. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/twin_test.go +28 -0
  44. package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_unix.go +19 -0
  45. package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_windows.go +11 -0
  46. package/extensions/acp/cmd/pig-acp/internal/pirpc/process.go +530 -0
  47. package/extensions/acp/cmd/pig-acp/internal/pirpc/process_test.go +355 -0
  48. package/extensions/acp/cmd/pig-acp/internal/pirpc/twin_test.go +28 -0
  49. package/extensions/acp/cmd/pig-acp/main.go +177 -0
  50. package/extensions/acp/cmd/pig-acp/main_test.go +213 -0
  51. package/extensions/acp/cmd/pig-acp/sigpipe_unix.go +10 -0
  52. package/extensions/acp/cmd/pig-acp/sigpipe_windows.go +5 -0
  53. package/extensions/acp/cmd/pig-acp/testdata/ask/extension.go +28 -0
  54. package/extensions/acp/cmd/pig-acp/testdata/ask/go.mod +7 -0
  55. package/extensions/acp/cmd/pig-acp/testdata/ask/go.sum +2 -0
  56. package/extensions/acp/cmd/pig-acp/testdata/gate/extension.go +37 -0
  57. package/extensions/acp/cmd/pig-acp/testdata/gate/go.mod +7 -0
  58. package/extensions/acp/cmd/pig-acp/testdata/gate/go.sum +2 -0
  59. package/extensions/acp/cmd/pig-acp/twin_test.go +28 -0
  60. package/extensions/acp/extension.go +63 -0
  61. package/extensions/acp/extension_test.go +111 -0
  62. package/extensions/acp/fakehost_test.go +548 -0
  63. package/extensions/acp/go.mod +6 -0
  64. package/extensions/acp/go.sum +2 -0
  65. package/package.json +41 -0
  66. package/port/PORT.md +147 -0
  67. package/port/interop/package-lock.json +89 -0
  68. package/port/interop/package.json +13 -0
  69. package/port/interop/run.mjs +229 -0
  70. package/port/mutate.mjs +110 -0
  71. package/port/mutation-results.txt +107 -0
  72. package/port/mutations.json +748 -0
  73. package/port/oracle/pi-acp/LICENSE +21 -0
  74. package/port/oracle/pi-acp/README.md +209 -0
  75. package/port/oracle/pi-acp/package.json +65 -0
  76. package/port/oracle/pi-acp/src/acp/agent.ts +1661 -0
  77. package/port/oracle/pi-acp/src/acp/auth-required.ts +37 -0
  78. package/port/oracle/pi-acp/src/acp/auth.ts +59 -0
  79. package/port/oracle/pi-acp/src/acp/paths.ts +15 -0
  80. package/port/oracle/pi-acp/src/acp/pi-commands.ts +59 -0
  81. package/port/oracle/pi-acp/src/acp/pi-sessions.ts +333 -0
  82. package/port/oracle/pi-acp/src/acp/pi-settings.ts +75 -0
  83. package/port/oracle/pi-acp/src/acp/session-store.ts +68 -0
  84. package/port/oracle/pi-acp/src/acp/session.ts +1078 -0
  85. package/port/oracle/pi-acp/src/acp/slash-commands.ts +197 -0
  86. package/port/oracle/pi-acp/src/acp/translate/bash.ts +100 -0
  87. package/port/oracle/pi-acp/src/acp/translate/pi-messages.ts +17 -0
  88. package/port/oracle/pi-acp/src/acp/translate/pi-tools.ts +51 -0
  89. package/port/oracle/pi-acp/src/acp/translate/prompt.ts +71 -0
  90. package/port/oracle/pi-acp/src/index.ts +82 -0
  91. package/port/oracle/pi-acp/src/pi-rpc/command.ts +16 -0
  92. package/port/oracle/pi-acp/src/pi-rpc/process.ts +432 -0
  93. package/port/oracle/pi-acp/test/component/agent-steering-followup-modes.test.ts +143 -0
  94. package/port/oracle/pi-acp/test/component/session-diff.test.ts +214 -0
  95. package/port/oracle/pi-acp/test/component/session-events.test.ts +1102 -0
  96. package/port/oracle/pi-acp/test/component/session-list-and-load.test.ts +129 -0
  97. package/port/oracle/pi-acp/test/component/session-list-custom-session-dir.test.ts +52 -0
  98. package/port/oracle/pi-acp/test/component/session-list-scoped.test.ts +76 -0
  99. package/port/oracle/pi-acp/test/component/session-load-toolresult.test.ts +68 -0
  100. package/port/oracle/pi-acp/test/component/session-queue-cancel.test.ts +43 -0
  101. package/port/oracle/pi-acp/test/component/session-slash-commands.test.ts +37 -0
  102. package/port/oracle/pi-acp/test/component/session-thinking-modes.test.ts +22 -0
  103. package/port/oracle/pi-acp/test/component/session-title-long-session.test.ts +56 -0
  104. package/port/oracle/pi-acp/test/component/session-updatedAt-message-only.test.ts +57 -0
  105. package/port/oracle/pi-acp/test/helpers/fakes.ts +87 -0
  106. package/port/oracle/pi-acp/test/unit/auth-methods-terminal-auth-meta.test.ts +22 -0
  107. package/port/oracle/pi-acp/test/unit/builtin-commands.test.ts +60 -0
  108. package/port/oracle/pi-acp/test/unit/context-usage.test.ts +204 -0
  109. package/port/oracle/pi-acp/test/unit/merge-commands.test.ts +19 -0
  110. package/port/oracle/pi-acp/test/unit/model-thinking-levels.test.ts +268 -0
  111. package/port/oracle/pi-acp/test/unit/new-session-auth-required-when-no-models.test.ts +51 -0
  112. package/port/oracle/pi-acp/test/unit/new-session-pi-not-found.test.ts +26 -0
  113. package/port/oracle/pi-acp/test/unit/new-session-runtime-startup-errors.test.ts +148 -0
  114. package/port/oracle/pi-acp/test/unit/pi-command.test.ts +44 -0
  115. package/port/oracle/pi-acp/test/unit/pi-commands.test.ts +32 -0
  116. package/port/oracle/pi-acp/test/unit/pi-enable-embed-context-flag.test.ts +35 -0
  117. package/port/oracle/pi-acp/test/unit/pi-messages.test.ts +29 -0
  118. package/port/oracle/pi-acp/test/unit/pi-rpc-request-timeout.test.ts +137 -0
  119. package/port/oracle/pi-acp/test/unit/pi-rpc-session-path.test.ts +93 -0
  120. package/port/oracle/pi-acp/test/unit/pi-tools.test.ts +40 -0
  121. package/port/oracle/pi-acp/test/unit/prompt-to-pi-message.test.ts +74 -0
  122. package/port/oracle/pi-acp/test/unit/session-config-options.test.ts +217 -0
  123. package/port/oracle/pi-acp/test/unit/session-delete.test.ts +170 -0
  124. package/port/oracle/pi-acp/test/unit/session-restore.test.ts +239 -0
  125. package/port/oracle/pi-acp/test/unit/slash-commands.test.ts +36 -0
  126. package/port/oracle/pi-acp/test/unit/startup-info-env.test.ts +84 -0
  127. package/port/oracle/pi-acp/test/unit/startup-info-load-session.test.ts +53 -0
  128. package/port/oracle/pi-acp/test/unit/startup-info-project-packages.test.ts +74 -0
  129. package/port/oracle/pi-acp/test/unit/stdout-destroyed-does-not-crash.test.ts +32 -0
  130. package/port/oracle/pi-acp/test/unit/thinking-level-rpc.test.ts +43 -0
  131. package/port/schema/LICENSE +191 -0
  132. package/port/schema/schema.json +8015 -0
  133. package/port/upstream-tests.json +201 -0
  134. package/provenance.json +28 -0
@@ -0,0 +1,530 @@
1
+ package acp
2
+
3
+ // Twins of test/unit/session-config-options.test.ts, test/unit/context-usage.test.ts (agent
4
+ // parts) and test/unit/model-thinking-levels.test.ts.
5
+
6
+ import (
7
+ "errors"
8
+ "fmt"
9
+ "reflect"
10
+ "testing"
11
+ )
12
+
13
+ var allLevels = []string{"off", "minimal", "low", "medium", "high", "xhigh", "max"}
14
+
15
+ func models(pairs ...[3]string) map[string]any {
16
+ var list []any
17
+ for _, p := range pairs {
18
+ m := map[string]any{"provider": p[0], "id": p[1]}
19
+ if p[2] != "" {
20
+ m["name"] = p[2]
21
+ }
22
+ list = append(list, m)
23
+ }
24
+ return map[string]any{"models": list}
25
+ }
26
+
27
+ func newSessionReq(cwd string) NewSessionRequest {
28
+ return NewSessionRequest{Cwd: cwd, McpServers: []any{}}
29
+ }
30
+
31
+ func TestSessionConfigOptions(t *testing.T) {
32
+ tw(t, "unit/session-config-options", "PiAcpAgent: newSession returns configOptions for model and thinking selectors", func(t *testing.T) {
33
+ conn := newFakeConn()
34
+ proc := newFakeProc()
35
+ proc.getLevelsFn = func() ([]string, error) { return allLevels, nil }
36
+ proc.getModelsFn = func() (map[string]any, error) {
37
+ return models([3]string{"test", "alpha", "Alpha"}, [3]string{"test", "beta", "Beta"}), nil
38
+ }
39
+ proc.getStateFn = func() (map[string]any, error) {
40
+ return map[string]any{"thinkingLevel": "high", "model": map[string]any{"provider": "test", "id": "beta"}}, nil
41
+ }
42
+ a, _ := testAgent(conn)
43
+ a.sessions = &fakeSessions{session: &fakeSession{id: "s1", cwd: cwdNow(t), proc: proc}}
44
+ result, err := a.NewSession(newSessionReq(cwdNow(t)))
45
+ if err != nil {
46
+ t.Fatal(err)
47
+ }
48
+ if result.Models == nil || result.Models.CurrentModelID != "test/beta" || result.Modes.CurrentModeID != "high" {
49
+ t.Fatalf("models=%+v modes=%+v", result.Models, result.Modes)
50
+ }
51
+ var thinking []any
52
+ for _, l := range allLevels {
53
+ thinking = append(thinking, map[string]any{"value": l, "name": "Thinking: " + l, "description": nil})
54
+ }
55
+ jsonEqual(t, result.ConfigOptions, []any{
56
+ map[string]any{"type": "select", "id": "model", "category": "model", "name": "Model", "description": "Select the model for this session", "currentValue": "test/beta",
57
+ "options": []any{
58
+ map[string]any{"value": "test/alpha", "name": "test/Alpha", "description": nil},
59
+ map[string]any{"value": "test/beta", "name": "test/Beta", "description": nil}}},
60
+ map[string]any{"type": "select", "id": "thought_level", "category": "thought_level", "name": "Thinking", "description": "Set the reasoning effort for this session", "currentValue": "high", "options": thinking},
61
+ })
62
+ })
63
+
64
+ tw(t, "unit/session-config-options", "PiAcpAgent: setSessionConfigOption maps model changes to pi and emits config_option_update", func(t *testing.T) {
65
+ conn := newFakeConn()
66
+ state := map[string]any{"thinkingLevel": "medium", "model": map[string]any{"provider": "test", "id": "alpha"}}
67
+ var calls []map[string]string
68
+ proc := newFakeProc()
69
+ proc.getLevelsFn = func() ([]string, error) { return allLevels, nil }
70
+ proc.getModelsFn = func() (map[string]any, error) {
71
+ return models([3]string{"test", "alpha", "Alpha"}, [3]string{"test", "beta", "Beta"}), nil
72
+ }
73
+ proc.getStateFn = func() (map[string]any, error) { return state, nil }
74
+ proc.setModelFn = func(p, id string) error {
75
+ calls = append(calls, map[string]string{"provider": p, "modelId": id})
76
+ state["model"] = map[string]any{"provider": p, "id": id}
77
+ return nil
78
+ }
79
+ a, _ := testAgent(conn)
80
+ a.sessions = &fakeSessions{session: &fakeSession{id: "s1", cwd: cwdNow(t), proc: proc}}
81
+ result, err := a.SetSessionConfigOption(SetSessionConfigOptionRequest{SessionID: "s1", ConfigID: "model", Value: "test/beta"})
82
+ if err != nil {
83
+ t.Fatal(err)
84
+ }
85
+ jsonEqual(t, calls, []any{map[string]any{"provider": "test", "modelId": "beta"}})
86
+ if v := optionValue(result.ConfigOptions, "model"); v != "test/beta" {
87
+ t.Errorf("model = %q", v)
88
+ }
89
+ var got []Update
90
+ for _, u := range conn.all() {
91
+ got = append(got, u.Update)
92
+ }
93
+ jsonEqual(t, got, []any{
94
+ map[string]any{"sessionUpdate": "current_mode_update", "currentModeId": "medium"},
95
+ map[string]any{"sessionUpdate": "config_option_update", "configOptions": result.ConfigOptions},
96
+ })
97
+ for _, u := range conn.all() {
98
+ if u.SessionID != "s1" {
99
+ t.Errorf("sessionId = %q", u.SessionID)
100
+ }
101
+ }
102
+ })
103
+
104
+ tw(t, "unit/session-config-options", "PiAcpAgent: setSessionConfigOption maps thought level changes to pi and emits sync updates", func(t *testing.T) {
105
+ conn := newFakeConn()
106
+ state := map[string]any{"thinkingLevel": "medium", "model": map[string]any{"provider": "test", "id": "alpha"}}
107
+ var levels []string
108
+ proc := newFakeProc()
109
+ proc.getLevelsFn = func() ([]string, error) { return allLevels, nil }
110
+ proc.getModelsFn = func() (map[string]any, error) { return models([3]string{"test", "alpha", "Alpha"}), nil }
111
+ proc.getStateFn = func() (map[string]any, error) { return state, nil }
112
+ proc.setThinkingFn = func(l string) error { levels = append(levels, l); state["thinkingLevel"] = l; return nil }
113
+ a, _ := testAgent(conn)
114
+ a.sessions = &fakeSessions{session: &fakeSession{id: "s1", cwd: cwdNow(t), proc: proc}}
115
+ result, err := a.SetSessionConfigOption(SetSessionConfigOptionRequest{SessionID: "s1", ConfigID: "thought_level", Value: "xhigh"})
116
+ if err != nil {
117
+ t.Fatal(err)
118
+ }
119
+ if !reflect.DeepEqual(levels, []string{"xhigh"}) {
120
+ t.Errorf("levels = %v", levels)
121
+ }
122
+ if v := optionValue(result.ConfigOptions, "thought_level"); v != "xhigh" {
123
+ t.Errorf("thought_level = %q", v)
124
+ }
125
+ var got []Update
126
+ for _, u := range conn.all() {
127
+ got = append(got, u.Update)
128
+ }
129
+ jsonEqual(t, got, []any{
130
+ map[string]any{"sessionUpdate": "current_mode_update", "currentModeId": "xhigh"},
131
+ map[string]any{"sessionUpdate": "config_option_update", "configOptions": result.ConfigOptions},
132
+ })
133
+ })
134
+ }
135
+
136
+ func optionValue(opts []ConfigOption, id string) string {
137
+ for _, o := range opts {
138
+ if o.ID == id {
139
+ return o.CurrentValue
140
+ }
141
+ }
142
+ return ""
143
+ }
144
+
145
+ // context-usage.test.ts
146
+ func TestContextUsage(t *testing.T) {
147
+ agentWith := func(t *testing.T, proc *fakeProc, conn *fakeConn) (*Agent, *scheduler, *Session) {
148
+ s := newTestSession(cwdNow(t), proc, conn)
149
+ a, sch := testAgent(conn)
150
+ a.sessions = &fakeSessions{session: s, anyID: true}
151
+ return a, sch, s
152
+ }
153
+ // The session's id in these tests is "s1".
154
+ tw(t, "unit/context-usage", "PiAcpSession: context usage request specifies the auxiliary timeout", func(t *testing.T) {
155
+ conn, proc := newFakeConn(), newFakeProc()
156
+ var requested = -1
157
+ proc.getSessionStatsFn = func(timeoutMs int) (SessionStats, error) {
158
+ requested = timeoutMs
159
+ return SessionStats{"contextUsage": map[string]any{"tokens": 100, "contextWindow": 100000}}, nil
160
+ }
161
+ newTestSession(cwdNow(t), proc, conn).PublishContextUsage()
162
+ if requested != SessionStatsTimeoutMs {
163
+ t.Errorf("timeout = %d", requested)
164
+ }
165
+ jsonEqual(t, usageUpdates(conn), []any{map[string]any{"sessionUpdate": "usage_update", "used": 100, "size": 100000}})
166
+ })
167
+
168
+ tw(t, "unit/context-usage", "PiAcpAgent: newSession publishes context usage only after the response is returned", func(t *testing.T) {
169
+ conn, proc := newFakeConn(), newFakeProc()
170
+ proc.sessionStats = SessionStats{"contextUsage": map[string]any{"tokens": 1234, "contextWindow": 100000}}
171
+ proc.getStateFn = func() (map[string]any, error) { return map[string]any{"thinkingLevel": "medium"}, nil }
172
+ a, sch, _ := agentWith(t, proc, conn)
173
+ res, err := a.NewSession(newSessionReq(cwdNow(t)))
174
+ if err != nil || res.SessionID != "s1" {
175
+ t.Fatalf("res=%+v err=%v", res, err)
176
+ }
177
+ if len(usageUpdates(conn)) != 0 {
178
+ t.Fatal("usage_update sent before the response was returned")
179
+ }
180
+ sch.drain()
181
+ jsonEqual(t, usageUpdates(conn), []any{map[string]any{"sessionUpdate": "usage_update", "used": 1234, "size": 100000}})
182
+ })
183
+
184
+ tw(t, "unit/context-usage", "PiAcpAgent: newSession tolerates a failing get_session_stats", func(t *testing.T) {
185
+ conn, proc := newFakeConn(), newFakeProc()
186
+ proc.sessionStatsError = errors.New("pi get_session_stats failed: unsupported")
187
+ proc.getStateFn = func() (map[string]any, error) { return map[string]any{"thinkingLevel": "medium"}, nil }
188
+ a, sch, _ := agentWith(t, proc, conn)
189
+ res, err := a.NewSession(newSessionReq(cwdNow(t)))
190
+ if err != nil || res.SessionID != "s1" {
191
+ t.Fatalf("res=%+v err=%v", res, err)
192
+ }
193
+ sch.drain()
194
+ if len(usageUpdates(conn)) != 0 {
195
+ t.Error("usage_update emitted")
196
+ }
197
+ })
198
+
199
+ switching := func(t *testing.T, prompt bool) (*fakeConn, *fakeProc, *Agent) {
200
+ conn, proc := newFakeConn(), newFakeProc()
201
+ state := map[string]any{"thinkingLevel": "medium", "model": map[string]any{"provider": "test", "id": "alpha"}}
202
+ proc.getModelsFn = func() (map[string]any, error) {
203
+ return models([3]string{"test", "alpha", "Alpha"}, [3]string{"test", "beta", "Beta"}), nil
204
+ }
205
+ proc.getStateFn = func() (map[string]any, error) { return state, nil }
206
+ proc.setModelFn = func(p, id string) error {
207
+ state["model"] = map[string]any{"provider": p, "id": id}
208
+ w := 100000
209
+ if id == "beta" {
210
+ w = 200000
211
+ }
212
+ proc.sessionStats = SessionStats{"contextUsage": map[string]any{"tokens": 500, "contextWindow": w}}
213
+ return nil
214
+ }
215
+ proc.setThinkingFn = func(l string) error { state["thinkingLevel"] = l; return nil }
216
+ proc.sessionStats = SessionStats{"contextUsage": map[string]any{"tokens": 500, "contextWindow": 100000}}
217
+ a, _, _ := agentWith(t, proc, conn)
218
+ return conn, proc, a
219
+ }
220
+
221
+ tw(t, "unit/context-usage", "PiAcpAgent: switching the model config option refreshes context usage", func(t *testing.T) {
222
+ conn, _, a := switching(t, false)
223
+ if _, err := a.SetSessionConfigOption(SetSessionConfigOptionRequest{SessionID: "s1", ConfigID: "model", Value: "test/beta"}); err != nil {
224
+ t.Fatal(err)
225
+ }
226
+ if got := conn.kinds(); !reflect.DeepEqual(got, []string{"current_mode_update", "config_option_update", "usage_update"}) {
227
+ t.Fatalf("kinds = %v", got)
228
+ }
229
+ ups := conn.all()
230
+ jsonEqual(t, ups[len(ups)-1], map[string]any{"SessionID": "s1", "Update": map[string]any{"sessionUpdate": "usage_update", "used": 500, "size": 200000}})
231
+ })
232
+
233
+ tw(t, "unit/context-usage", "PiAcpAgent: unstable_setSessionModel refreshes context usage", func(t *testing.T) {
234
+ conn, proc, a := switching(t, false)
235
+ proc.sessionStats = SessionStats{"contextUsage": map[string]any{"tokens": 700, "contextWindow": 100000}}
236
+ proc.setModelFn = func(p, id string) error {
237
+ w := 100000
238
+ if id == "beta" {
239
+ w = 200000
240
+ }
241
+ proc.sessionStats = SessionStats{"contextUsage": map[string]any{"tokens": 700, "contextWindow": w}}
242
+ return nil
243
+ }
244
+ if err := a.UnstableSetSessionModel(SetSessionModelRequest{SessionID: "s1", ModelID: "test/beta"}); err != nil {
245
+ t.Fatal(err)
246
+ }
247
+ if proc.statsCount() != 1 {
248
+ t.Errorf("get_session_stats called %d times", proc.statsCount())
249
+ }
250
+ if got := conn.kinds(); !reflect.DeepEqual(got, []string{"current_mode_update", "config_option_update", "usage_update"}) {
251
+ t.Fatalf("kinds = %v", got)
252
+ }
253
+ ups := conn.all()
254
+ jsonEqual(t, ups[len(ups)-1].Update, map[string]any{"sessionUpdate": "usage_update", "used": 700, "size": 200000})
255
+ })
256
+
257
+ tw(t, "unit/context-usage", "PiAcpAgent: switching the thinking level does not publish context usage", func(t *testing.T) {
258
+ conn, proc, a := switching(t, false)
259
+ if _, err := a.SetSessionConfigOption(SetSessionConfigOptionRequest{SessionID: "s1", ConfigID: "thought_level", Value: "high"}); err != nil {
260
+ t.Fatal(err)
261
+ }
262
+ if proc.statsCount() != 0 || len(usageUpdates(conn)) != 0 {
263
+ t.Errorf("stats=%d updates=%v", proc.statsCount(), usageUpdates(conn))
264
+ }
265
+ })
266
+ }
267
+
268
+ // model-thinking-levels.test.ts
269
+ type levelsFixture struct {
270
+ agent *Agent
271
+ conn *fakeConn
272
+ proc *fakeProc
273
+ state map[string]any
274
+ calls *[]string
275
+ sch *scheduler
276
+ }
277
+
278
+ func newLevelsFixture(t *testing.T) *levelsFixture {
279
+ conn := newFakeConn()
280
+ state := map[string]any{"thinkingLevel": "max", "model": map[string]any{"provider": "test", "id": "reasoning"}}
281
+ calls := &[]string{}
282
+ proc := newFakeProc()
283
+ proc.getStateFn = func() (map[string]any, error) { return state, nil }
284
+ proc.getModelsFn = func() (map[string]any, error) {
285
+ return models([3]string{"test", "reasoning", ""}, [3]string{"test", "plain", ""}), nil
286
+ }
287
+ proc.getLevelsFn = func() ([]string, error) {
288
+ if state["model"].(map[string]any)["id"] == "plain" {
289
+ return []string{"off"}, nil
290
+ }
291
+ return []string{"low", "high", "max"}, nil
292
+ }
293
+ proc.setThinkingFn = func(l string) error { *calls = append(*calls, l); state["thinkingLevel"] = "max"; return nil }
294
+ proc.setModelFn = func(p, id string) error {
295
+ *calls = append(*calls, id)
296
+ state["model"] = map[string]any{"provider": p, "id": id}
297
+ if id == "plain" {
298
+ state["thinkingLevel"] = "off"
299
+ } else {
300
+ state["thinkingLevel"] = "max"
301
+ }
302
+ return nil
303
+ }
304
+ a, sch := testAgent(conn)
305
+ a.sessions = &fakeSessions{session: &fakeSession{id: "s1", cwd: cwdNow(t), proc: proc}, anyID: true}
306
+ return &levelsFixture{a, conn, proc, state, calls, sch}
307
+ }
308
+
309
+ func assertLevelUpdates(t *testing.T, conn *fakeConn, level string, levels []string) {
310
+ t.Helper()
311
+ ups := conn.all()
312
+ if len(ups) != 2 {
313
+ t.Fatalf("%d updates: %+v", len(ups), ups)
314
+ }
315
+ jsonEqual(t, ups[0], map[string]any{"SessionID": "s1", "Update": map[string]any{"sessionUpdate": "current_mode_update", "currentModeId": level}})
316
+ opts, _ := ups[1].Update["configOptions"].([]ConfigOption)
317
+ if opts == nil {
318
+ t.Fatalf("config_option_update = %v", ups[1].Update)
319
+ }
320
+ var thought *ConfigOption
321
+ for i := range opts {
322
+ if opts[i].ID == "thought_level" {
323
+ thought = &opts[i]
324
+ }
325
+ }
326
+ if thought == nil || thought.CurrentValue != level {
327
+ t.Fatalf("thought_level = %+v", thought)
328
+ }
329
+ var vals []string
330
+ for _, o := range thought.Options {
331
+ vals = append(vals, o.Value)
332
+ }
333
+ if !reflect.DeepEqual(vals, levels) {
334
+ t.Errorf("options = %v, want %v", vals, levels)
335
+ }
336
+ }
337
+
338
+ func TestModelThinkingLevels(t *testing.T) {
339
+ for _, legacy := range []bool{false, true} {
340
+ kind := "config"
341
+ if legacy {
342
+ kind = "legacy"
343
+ }
344
+ setLevel := func(f *levelsFixture, level string) (*SetSessionConfigOptionResponse, error) {
345
+ if legacy {
346
+ _, err := f.agent.SetSessionMode(SetSessionModeRequest{SessionID: "s1", ModeID: level})
347
+ return nil, err
348
+ }
349
+ r, err := f.agent.SetSessionConfigOption(SetSessionConfigOptionRequest{SessionID: "s1", ConfigID: "thought_level", Value: level})
350
+ return &r, err
351
+ }
352
+ for _, requested := range []string{"max", "xhigh", "ordinary", " mean ", " "} {
353
+ tw(t, "unit/model-thinking-levels", "${legacy ? 'legacy' : 'config'} reasoning setter reports actual max for ${requested}", func(t *testing.T) {
354
+ t.Run(fmt.Sprintf("%s reasoning setter reports actual max for %s", kind, requested), func(t *testing.T) {
355
+ f := newLevelsFixture(t)
356
+ r, err := setLevel(f, requested)
357
+ if err != nil {
358
+ t.Fatal(err)
359
+ }
360
+ if !legacy && optionValue(r.ConfigOptions, "thought_level") != "max" {
361
+ t.Errorf("thought_level = %q", optionValue(r.ConfigOptions, "thought_level"))
362
+ }
363
+ if !reflect.DeepEqual(*f.calls, []string{requested}) {
364
+ t.Errorf("calls = %q", *f.calls)
365
+ }
366
+ assertLevelUpdates(t, f.conn, "max", []string{"low", "high", "max"})
367
+ })
368
+ })
369
+ }
370
+ tw(t, "unit/model-thinking-levels", "${legacy ? 'legacy' : 'config'} reasoning setter reports an opaque applied level", func(t *testing.T) {
371
+ t.Run(fmt.Sprintf("%s reasoning setter reports an opaque applied level", kind), func(t *testing.T) {
372
+ f := newLevelsFixture(t)
373
+ f.proc.getLevelsFn = func() ([]string, error) { return []string{"ordinary", "mean"}, nil }
374
+ f.proc.setThinkingFn = func(l string) error { *f.calls = append(*f.calls, l); f.state["thinkingLevel"] = "mean"; return nil }
375
+ r, err := setLevel(f, "ordinary")
376
+ if err != nil {
377
+ t.Fatal(err)
378
+ }
379
+ if !legacy && optionValue(r.ConfigOptions, "thought_level") != "mean" {
380
+ t.Errorf("thought_level = %q", optionValue(r.ConfigOptions, "thought_level"))
381
+ }
382
+ if !reflect.DeepEqual(*f.calls, []string{"ordinary"}) {
383
+ t.Errorf("calls = %q", *f.calls)
384
+ }
385
+ assertLevelUpdates(t, f.conn, "mean", []string{"ordinary", "mean"})
386
+ })
387
+ })
388
+ tw(t, "unit/model-thinking-levels", "${legacy ? 'legacy' : 'config'} model setter refreshes model-specific levels and current mode", func(t *testing.T) {
389
+ t.Run(fmt.Sprintf("%s model setter refreshes model-specific levels and current mode", kind), func(t *testing.T) {
390
+ f := newLevelsFixture(t)
391
+ for _, model := range []string{"plain", "reasoning"} {
392
+ f.conn.reset()
393
+ if legacy {
394
+ if err := f.agent.UnstableSetSessionModel(SetSessionModelRequest{SessionID: "s1", ModelID: "test/" + model}); err != nil {
395
+ t.Fatal(err)
396
+ }
397
+ } else if _, err := f.agent.SetSessionConfigOption(SetSessionConfigOptionRequest{SessionID: "s1", ConfigID: "model", Value: "test/" + model}); err != nil {
398
+ t.Fatal(err)
399
+ }
400
+ level, levels := "max", []string{"low", "high", "max"}
401
+ if model == "plain" {
402
+ level, levels = "off", []string{"off"}
403
+ }
404
+ // The model setter also publishes usage; only the two config updates are compared.
405
+ ups := f.conn.all()
406
+ f.conn.reset()
407
+ for _, u := range ups {
408
+ if u.Update["sessionUpdate"] != "usage_update" {
409
+ _ = f.conn.SessionUpdate(u.SessionID, u.Update)
410
+ }
411
+ }
412
+ assertLevelUpdates(t, f.conn, level, levels)
413
+ }
414
+ })
415
+ })
416
+ for _, failure := range []string{"state", "discovery", "empty", "non-string", "inconsistent"} {
417
+ tw(t, "unit/model-thinking-levels", "${legacy ? 'legacy' : 'config'} reasoning setter emits no success on ${failure} read failure", func(t *testing.T) {
418
+ t.Run(fmt.Sprintf("%s reasoning setter emits no success on %s read failure", kind, failure), func(t *testing.T) {
419
+ f := newLevelsFixture(t)
420
+ switch failure {
421
+ case "state":
422
+ f.proc.getStateFn = func() (map[string]any, error) { return nil, errors.New("state failed") }
423
+ case "discovery":
424
+ f.proc.getLevelsFn = func() ([]string, error) { return nil, errors.New("discovery failed") }
425
+ default:
426
+ f.proc.setThinkingFn = func(string) error {
427
+ switch failure {
428
+ case "empty":
429
+ f.state["thinkingLevel"] = ""
430
+ case "non-string":
431
+ f.state["thinkingLevel"] = 1
432
+ default:
433
+ f.state["thinkingLevel"] = "medium"
434
+ }
435
+ return nil
436
+ }
437
+ }
438
+ if _, err := setLevel(f, "max"); err == nil {
439
+ t.Fatal("expected an error")
440
+ }
441
+ if n := len(f.conn.all()); n != 0 {
442
+ t.Errorf("%d updates after a failure", n)
443
+ }
444
+ })
445
+ })
446
+ }
447
+ }
448
+
449
+ tw(t, "unit/model-thinking-levels", "invalid configuration and legacy mode requests do not mutate Pi", func(t *testing.T) {
450
+ f := newLevelsFixture(t)
451
+ for _, req := range []SetSessionConfigOptionRequest{
452
+ {SessionID: "s1", ConfigID: "thought_level", Value: ""},
453
+ {SessionID: "s1", ConfigID: "unknown", Value: "high"},
454
+ {SessionID: "s1", ConfigID: "thought_level", Value: 1},
455
+ } {
456
+ _, err := f.agent.SetSessionConfigOption(req)
457
+ if re, ok := err.(*RequestError); !ok || re.Code != -32602 {
458
+ t.Errorf("%+v: err = %v", req, err)
459
+ }
460
+ }
461
+ for _, mode := range []any{"", 1, nil} {
462
+ _, err := f.agent.SetSessionMode(SetSessionModeRequest{SessionID: "s1", ModeID: mode})
463
+ if re, ok := err.(*RequestError); !ok || re.Code != -32602 {
464
+ t.Errorf("mode %v: err = %v", mode, err)
465
+ }
466
+ }
467
+ if len(*f.calls) != 0 || len(f.conn.all()) != 0 {
468
+ t.Errorf("calls=%v updates=%v", *f.calls, f.conn.all())
469
+ }
470
+ })
471
+
472
+ for _, load := range []bool{false, true} {
473
+ for _, levels := range [][]string{{"off"}, {"low", "high", "max"}, {"ordinary", "mean"}} {
474
+ name := "new"
475
+ if load {
476
+ name = "load"
477
+ }
478
+ tw(t, "unit/model-thinking-levels", "${load ? 'load' : 'new'} session advertises exact ${levels} levels", func(t *testing.T) {
479
+ t.Run(fmt.Sprintf("%s session advertises exact %v levels", name, levels), func(t *testing.T) {
480
+ f := newLevelsFixture(t)
481
+ f.state["thinkingLevel"] = levels[len(levels)-1]
482
+ f.proc.getLevelsFn = func() ([]string, error) { return levels, nil }
483
+ f.agent.spawn = func(SpawnParams) (Proc, error) { return f.proc, nil }
484
+ f.agent.store = newMemStore(StoredSession{SessionID: "s1", Cwd: cwdNow(t), SessionFile: "/tmp/thinking-test.jsonl"})
485
+ f.agent.sessions = NewSessionManager(f.agent.spawn, f.agent.store)
486
+ var modes ModeState
487
+ var options []ConfigOption
488
+ if load {
489
+ r, err := f.agent.LoadSession(LoadSessionRequest{SessionID: "s1", Cwd: cwdNow(t), McpServers: []any{}})
490
+ if err != nil {
491
+ t.Fatal(err)
492
+ }
493
+ modes, options = r.Modes, r.ConfigOptions
494
+ } else {
495
+ r, err := f.agent.NewSession(newSessionReq(cwdNow(t)))
496
+ if err != nil {
497
+ t.Fatal(err)
498
+ }
499
+ modes, options = r.Modes, r.ConfigOptions
500
+ }
501
+ want := levels[len(levels)-1]
502
+ if modes.CurrentModeID != want {
503
+ t.Errorf("currentModeId = %q", modes.CurrentModeID)
504
+ }
505
+ var ids []string
506
+ for _, m := range modes.AvailableModes {
507
+ ids = append(ids, m.ID)
508
+ }
509
+ if !reflect.DeepEqual(ids, levels) {
510
+ t.Errorf("modes = %v", ids)
511
+ }
512
+ var vals []string
513
+ for _, o := range options {
514
+ if o.ID == "thought_level" {
515
+ if o.CurrentValue != want {
516
+ t.Errorf("currentValue = %q", o.CurrentValue)
517
+ }
518
+ for _, x := range o.Options {
519
+ vals = append(vals, x.Value)
520
+ }
521
+ }
522
+ }
523
+ if !reflect.DeepEqual(vals, levels) {
524
+ t.Errorf("options = %v", vals)
525
+ }
526
+ })
527
+ })
528
+ }
529
+ }
530
+ }