kanna-code 0.41.6 → 0.42.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.
- package/dist/client/assets/{index-BujIAIt0.css → index-COpRUlCZ.css} +1 -1
- package/dist/client/assets/{index-DqIqYlzq.js → index-DDpLGxoy.js} +205 -205
- package/dist/client/index.html +2 -2
- package/dist/export-viewer/assets/{index-Bw6GI0aH.css → index-DH7NuXKA.css} +1 -1
- package/dist/export-viewer/assets/{index--p-u0wfg.js → index-DoK2sc-h.js} +51 -51
- package/dist/export-viewer/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent.test.ts +1 -8
- package/src/server/agent.ts +41 -22
- package/src/server/app-settings.test.ts +48 -2
- package/src/server/app-settings.ts +43 -8
- package/src/server/async-queue.ts +45 -0
- package/src/server/codex-app-server.test.ts +70 -1
- package/src/server/codex-app-server.ts +4 -62
- package/src/server/cursor-cli.test.ts +219 -0
- package/src/server/cursor-cli.ts +376 -0
- package/src/server/diff-store.test.ts +47 -0
- package/src/server/diff-store.ts +2 -5
- package/src/server/paths.ts +64 -0
- package/src/server/provider-catalog.test.ts +31 -4
- package/src/server/provider-catalog.ts +28 -22
- package/src/server/read-models.test.ts +17 -0
- package/src/server/read-models.ts +2 -0
- package/src/server/transcript.ts +14 -0
- package/src/server/ws-router.test.ts +7 -0
- package/src/server/ws-router.ts +26 -3
- package/src/shared/assert.ts +7 -0
- package/src/shared/git-url.ts +54 -0
- package/src/shared/json.ts +14 -0
- package/src/shared/protocol.ts +1 -0
- package/src/shared/types.test.ts +50 -1
- package/src/shared/types.ts +150 -15
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
6
|
<meta name="theme-color" content="#ffffff" />
|
|
7
7
|
<title>Kanna Transcript</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-DoK2sc-h.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DH7NuXKA.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/server/agent.test.ts
CHANGED
|
@@ -9,14 +9,7 @@ import {
|
|
|
9
9
|
} from "./agent"
|
|
10
10
|
import type { HarnessTurn } from "./harness-types"
|
|
11
11
|
import type { ChatAttachment, TranscriptEntry } from "../shared/types"
|
|
12
|
-
|
|
13
|
-
function timestamped<T extends Omit<TranscriptEntry, "_id" | "createdAt">>(entry: T): TranscriptEntry {
|
|
14
|
-
return {
|
|
15
|
-
_id: crypto.randomUUID(),
|
|
16
|
-
createdAt: Date.now(),
|
|
17
|
-
...entry,
|
|
18
|
-
} as TranscriptEntry
|
|
19
|
-
}
|
|
12
|
+
import { timestamped } from "./transcript"
|
|
20
13
|
|
|
21
14
|
async function waitFor(condition: () => boolean, timeoutMs = 2000) {
|
|
22
15
|
const start = Date.now()
|
package/src/server/agent.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os"
|
|
|
3
3
|
import type {
|
|
4
4
|
AgentProvider,
|
|
5
5
|
ChatAttachment,
|
|
6
|
+
CodexReasoningEffort,
|
|
6
7
|
ContextWindowUsageSnapshot,
|
|
7
8
|
ModelOptions,
|
|
8
9
|
NormalizedToolCall,
|
|
@@ -17,19 +18,24 @@ import { EventStore } from "./event-store"
|
|
|
17
18
|
import type { AnalyticsReporter } from "./analytics"
|
|
18
19
|
import { NoopAnalyticsReporter } from "./analytics"
|
|
19
20
|
import { CodexAppServerManager } from "./codex-app-server"
|
|
21
|
+
import { CursorCliManager } from "./cursor-cli"
|
|
20
22
|
import { type GenerateChatTitleResult, generateTitleForChatDetailed } from "./generate-title"
|
|
21
23
|
import type { HarnessEvent, HarnessToolRequest, HarnessTurn } from "./harness-types"
|
|
22
24
|
import {
|
|
23
25
|
applyClaudeSdkModels,
|
|
24
26
|
type ClaudeSdkModelInfo,
|
|
25
27
|
codexServiceTierFromModelOptions,
|
|
28
|
+
cursorModelIdForOptions,
|
|
26
29
|
getServerProviderCatalog,
|
|
27
30
|
normalizeClaudeModelOptions,
|
|
28
31
|
normalizeCodexModelOptions,
|
|
32
|
+
normalizeCursorModelOptions,
|
|
29
33
|
normalizeServerModel,
|
|
30
34
|
} from "./provider-catalog"
|
|
31
35
|
import { resolveClaudeApiModelId } from "../shared/types"
|
|
32
36
|
import { fallbackTitleFromMessage } from "./generate-title"
|
|
37
|
+
import { asNumber, asRecord } from "../shared/json"
|
|
38
|
+
import { timestamped } from "./transcript"
|
|
33
39
|
|
|
34
40
|
const CLAUDE_TOOLSET = [
|
|
35
41
|
"Skill",
|
|
@@ -106,6 +112,7 @@ interface AgentCoordinatorArgs {
|
|
|
106
112
|
onStateChange: (chatId?: string, options?: { immediate?: boolean }) => void
|
|
107
113
|
analytics?: AnalyticsReporter
|
|
108
114
|
codexManager?: CodexAppServerManager
|
|
115
|
+
cursorManager?: CursorCliManager
|
|
109
116
|
generateTitle?: (messageContent: string, cwd: string) => Promise<GenerateChatTitleResult>
|
|
110
117
|
startClaudeSession?: (args: {
|
|
111
118
|
localPath: string
|
|
@@ -147,17 +154,6 @@ interface SendMessageOptions {
|
|
|
147
154
|
planMode?: boolean
|
|
148
155
|
}
|
|
149
156
|
|
|
150
|
-
function timestamped<T extends Omit<TranscriptEntry, "_id" | "createdAt">>(
|
|
151
|
-
entry: T,
|
|
152
|
-
createdAt = Date.now()
|
|
153
|
-
): TranscriptEntry {
|
|
154
|
-
return {
|
|
155
|
-
_id: crypto.randomUUID(),
|
|
156
|
-
createdAt,
|
|
157
|
-
...entry,
|
|
158
|
-
} as TranscriptEntry
|
|
159
|
-
}
|
|
160
|
-
|
|
161
157
|
function stringFromUnknown(value: unknown) {
|
|
162
158
|
if (typeof value === "string") return value
|
|
163
159
|
try {
|
|
@@ -173,14 +169,6 @@ function buildSteeredMessageContent(content: string) {
|
|
|
173
169
|
: STEERED_MESSAGE_PREFIX
|
|
174
170
|
}
|
|
175
171
|
|
|
176
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
177
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function asNumber(value: unknown): number | undefined {
|
|
181
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
|
182
|
-
}
|
|
183
|
-
|
|
184
172
|
function escapeXmlAttribute(value: string) {
|
|
185
173
|
return value
|
|
186
174
|
.replaceAll("&", "&")
|
|
@@ -680,6 +668,7 @@ export class AgentCoordinator {
|
|
|
680
668
|
private readonly onStateChange: (chatId?: string, options?: { immediate?: boolean }) => void
|
|
681
669
|
private readonly analytics: AnalyticsReporter
|
|
682
670
|
private readonly codexManager: CodexAppServerManager
|
|
671
|
+
private readonly cursorManager: CursorCliManager
|
|
683
672
|
private readonly generateTitle: (messageContent: string, cwd: string) => Promise<GenerateChatTitleResult>
|
|
684
673
|
private readonly startClaudeSessionFn: NonNullable<AgentCoordinatorArgs["startClaudeSession"]>
|
|
685
674
|
private reportBackgroundError: ((message: string) => void) | null = null
|
|
@@ -692,6 +681,7 @@ export class AgentCoordinator {
|
|
|
692
681
|
this.onStateChange = args.onStateChange
|
|
693
682
|
this.analytics = args.analytics ?? NoopAnalyticsReporter
|
|
694
683
|
this.codexManager = args.codexManager ?? new CodexAppServerManager()
|
|
684
|
+
this.cursorManager = args.cursorManager ?? new CursorCliManager()
|
|
695
685
|
this.generateTitle = args.generateTitle ?? generateTitleForChatDetailed
|
|
696
686
|
this.startClaudeSessionFn = args.startClaudeSession ?? startClaudeSession
|
|
697
687
|
}
|
|
@@ -784,9 +774,20 @@ export class AgentCoordinator {
|
|
|
784
774
|
}
|
|
785
775
|
}
|
|
786
776
|
|
|
787
|
-
|
|
777
|
+
if (provider === "cursor") {
|
|
778
|
+
const modelOptions = normalizeCursorModelOptions(options.modelOptions)
|
|
779
|
+
return {
|
|
780
|
+
model: cursorModelIdForOptions(normalizeServerModel(provider, options.model), modelOptions),
|
|
781
|
+
effort: undefined,
|
|
782
|
+
serviceTier: undefined,
|
|
783
|
+
planMode: false,
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const model = normalizeServerModel(provider, options.model)
|
|
788
|
+
const modelOptions = normalizeCodexModelOptions(model, options.modelOptions, options.effort)
|
|
788
789
|
return {
|
|
789
|
-
model
|
|
790
|
+
model,
|
|
790
791
|
effort: modelOptions.reasoningEffort,
|
|
791
792
|
serviceTier: codexServiceTierFromModelOptions(modelOptions),
|
|
792
793
|
planMode: catalog.supportsPlanMode ? Boolean(options.planMode) : false,
|
|
@@ -957,6 +958,24 @@ export class AgentCoordinator {
|
|
|
957
958
|
provider: args.provider,
|
|
958
959
|
model: args.model,
|
|
959
960
|
})
|
|
961
|
+
} else if (args.provider === "cursor") {
|
|
962
|
+
logSendToStartingProfile(args.profile, "start_turn.provider_boot.begin", {
|
|
963
|
+
chatId: args.chatId,
|
|
964
|
+
provider: args.provider,
|
|
965
|
+
model: args.model,
|
|
966
|
+
})
|
|
967
|
+
// Cursor cannot fork (see canForkChat), so a turn always resumes its own session.
|
|
968
|
+
turn = await this.cursorManager.startTurn({
|
|
969
|
+
cwd: project.localPath,
|
|
970
|
+
content: buildPromptText(args.content, args.attachments),
|
|
971
|
+
model: args.model,
|
|
972
|
+
sessionToken: chat.sessionToken,
|
|
973
|
+
})
|
|
974
|
+
logSendToStartingProfile(args.profile, "start_turn.provider_boot.ready", {
|
|
975
|
+
chatId: args.chatId,
|
|
976
|
+
provider: args.provider,
|
|
977
|
+
model: args.model,
|
|
978
|
+
})
|
|
960
979
|
} else {
|
|
961
980
|
logSendToStartingProfile(args.profile, "start_turn.provider_boot.begin", {
|
|
962
981
|
chatId: args.chatId,
|
|
@@ -983,7 +1002,7 @@ export class AgentCoordinator {
|
|
|
983
1002
|
chatId: args.chatId,
|
|
984
1003
|
content: buildPromptText(args.content, args.attachments),
|
|
985
1004
|
model: args.model,
|
|
986
|
-
effort: args.effort as
|
|
1005
|
+
effort: args.effort as CodexReasoningEffort | undefined,
|
|
987
1006
|
serviceTier: args.serviceTier,
|
|
988
1007
|
planMode: args.planMode,
|
|
989
1008
|
onToolRequest,
|
|
@@ -44,9 +44,16 @@ function expectedSettingsSnapshot(filePath: string, overrides: Partial<AppSettin
|
|
|
44
44
|
planMode: false,
|
|
45
45
|
},
|
|
46
46
|
codex: {
|
|
47
|
-
model: "gpt-5.
|
|
47
|
+
model: "gpt-5.6-sol",
|
|
48
|
+
modelOptions: {
|
|
49
|
+
reasoningEffort: "medium",
|
|
50
|
+
fastMode: false,
|
|
51
|
+
},
|
|
52
|
+
planMode: false,
|
|
53
|
+
},
|
|
54
|
+
cursor: {
|
|
55
|
+
model: "composer-2.5",
|
|
48
56
|
modelOptions: {
|
|
49
|
-
reasoningEffort: "high",
|
|
50
57
|
fastMode: false,
|
|
51
58
|
},
|
|
52
59
|
planMode: false,
|
|
@@ -159,4 +166,43 @@ describe("AppSettingsManager", () => {
|
|
|
159
166
|
|
|
160
167
|
manager.dispose()
|
|
161
168
|
})
|
|
169
|
+
|
|
170
|
+
test("normalizes GPT-5.6 reasoning levels when settings are written", async () => {
|
|
171
|
+
const filePath = await createTempFilePath()
|
|
172
|
+
const manager = new AppSettingsManager(filePath)
|
|
173
|
+
await manager.initialize()
|
|
174
|
+
|
|
175
|
+
const sol = await manager.writePatch({
|
|
176
|
+
providerDefaults: {
|
|
177
|
+
codex: { model: "gpt-5.6-sol", modelOptions: { reasoningEffort: "ultra" } },
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
expect(sol.providerDefaults.codex.modelOptions.reasoningEffort).toBe("ultra")
|
|
181
|
+
|
|
182
|
+
const terra = await manager.writePatch({
|
|
183
|
+
providerDefaults: {
|
|
184
|
+
codex: { model: "gpt-5.6-terra", modelOptions: { reasoningEffort: "max" } },
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
expect(terra.providerDefaults.codex.modelOptions.reasoningEffort).toBe("max")
|
|
188
|
+
|
|
189
|
+
const luna = await manager.writePatch({
|
|
190
|
+
providerDefaults: {
|
|
191
|
+
codex: { model: "gpt-5.6-luna", modelOptions: { reasoningEffort: "ultra" } },
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
expect(luna.providerDefaults.codex.modelOptions.reasoningEffort).toBe("max")
|
|
195
|
+
|
|
196
|
+
const legacy = await manager.writePatch({
|
|
197
|
+
providerDefaults: {
|
|
198
|
+
codex: { model: "gpt-5.5", modelOptions: { reasoningEffort: "xhigh" } },
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
expect(legacy.providerDefaults.codex).toMatchObject({
|
|
202
|
+
model: "gpt-5.5",
|
|
203
|
+
modelOptions: { reasoningEffort: "xhigh", fastMode: false },
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
manager.dispose()
|
|
207
|
+
})
|
|
162
208
|
})
|
|
@@ -7,11 +7,14 @@ import { getSettingsFilePath, LOG_PREFIX } from "../shared/branding"
|
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_CLAUDE_MODEL_OPTIONS,
|
|
9
9
|
DEFAULT_CODEX_MODEL_OPTIONS,
|
|
10
|
+
DEFAULT_CURSOR_MODEL_OPTIONS,
|
|
10
11
|
isClaudeReasoningEffort,
|
|
11
12
|
isCodexReasoningEffort,
|
|
12
13
|
normalizeClaudeContextWindow,
|
|
13
14
|
normalizeClaudeModelId,
|
|
14
15
|
normalizeCodexModelId,
|
|
16
|
+
normalizeCodexReasoningEffort,
|
|
17
|
+
normalizeCursorModelId,
|
|
15
18
|
supportsClaudeMaxReasoningEffort,
|
|
16
19
|
type AppSettingsPatch,
|
|
17
20
|
type AppSettingsSnapshot,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
type ChatSoundPreference,
|
|
22
25
|
type ClaudeModelOptions,
|
|
23
26
|
type CodexModelOptions,
|
|
27
|
+
type CursorModelOptions,
|
|
24
28
|
type DefaultProviderPreference,
|
|
25
29
|
type EditorPreset,
|
|
26
30
|
type ProviderPreference,
|
|
@@ -45,6 +49,7 @@ interface AppSettingsFile {
|
|
|
45
49
|
providerDefaults?: {
|
|
46
50
|
claude?: Partial<ProviderPreference<Partial<ClaudeModelOptions>>> & { effort?: unknown }
|
|
47
51
|
codex?: Partial<ProviderPreference<Partial<CodexModelOptions>>> & { effort?: unknown }
|
|
52
|
+
cursor?: Partial<ProviderPreference<Partial<CursorModelOptions>>>
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -104,10 +109,15 @@ function createDefaultProviderDefaults(): ChatProviderPreferences {
|
|
|
104
109
|
planMode: false,
|
|
105
110
|
},
|
|
106
111
|
codex: {
|
|
107
|
-
model: "gpt-5.
|
|
112
|
+
model: "gpt-5.6-sol",
|
|
108
113
|
modelOptions: { ...DEFAULT_CODEX_MODEL_OPTIONS },
|
|
109
114
|
planMode: false,
|
|
110
115
|
},
|
|
116
|
+
cursor: {
|
|
117
|
+
model: "composer-2.5",
|
|
118
|
+
modelOptions: { ...DEFAULT_CURSOR_MODEL_OPTIONS },
|
|
119
|
+
planMode: false,
|
|
120
|
+
},
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
123
|
|
|
@@ -143,7 +153,7 @@ function normalizeChatSoundId(value: unknown): ChatSoundId {
|
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
function normalizeDefaultProvider(value: unknown): DefaultProviderPreference {
|
|
146
|
-
return value === "claude" || value === "codex" || value === "last_used" ? value : "last_used"
|
|
156
|
+
return value === "claude" || value === "codex" || value === "cursor" || value === "last_used" ? value : "last_used"
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
function normalizeEditorPreset(value: unknown): EditorPreset {
|
|
@@ -187,15 +197,15 @@ function normalizeCodexPreference(value?: {
|
|
|
187
197
|
modelOptions?: Partial<Record<keyof CodexModelOptions, unknown>>
|
|
188
198
|
planMode?: unknown
|
|
189
199
|
}): ProviderPreference<CodexModelOptions> {
|
|
200
|
+
const model = normalizeCodexModelId(typeof value?.model === "string" ? value.model : undefined)
|
|
190
201
|
const reasoningEffort = value?.modelOptions?.reasoningEffort
|
|
191
202
|
return {
|
|
192
|
-
model
|
|
203
|
+
model,
|
|
193
204
|
modelOptions: {
|
|
194
|
-
reasoningEffort:
|
|
195
|
-
|
|
196
|
-
:
|
|
197
|
-
|
|
198
|
-
: DEFAULT_CODEX_MODEL_OPTIONS.reasoningEffort,
|
|
205
|
+
reasoningEffort: normalizeCodexReasoningEffort(
|
|
206
|
+
model,
|
|
207
|
+
isCodexReasoningEffort(reasoningEffort) ? reasoningEffort : value?.effort,
|
|
208
|
+
),
|
|
199
209
|
fastMode: typeof value?.modelOptions?.fastMode === "boolean"
|
|
200
210
|
? value.modelOptions.fastMode
|
|
201
211
|
: DEFAULT_CODEX_MODEL_OPTIONS.fastMode,
|
|
@@ -204,11 +214,28 @@ function normalizeCodexPreference(value?: {
|
|
|
204
214
|
}
|
|
205
215
|
}
|
|
206
216
|
|
|
217
|
+
function normalizeCursorPreference(value?: {
|
|
218
|
+
model?: unknown
|
|
219
|
+
modelOptions?: Partial<Record<keyof CursorModelOptions, unknown>>
|
|
220
|
+
planMode?: unknown
|
|
221
|
+
}): ProviderPreference<CursorModelOptions> {
|
|
222
|
+
return {
|
|
223
|
+
model: normalizeCursorModelId(typeof value?.model === "string" ? value.model : undefined),
|
|
224
|
+
modelOptions: {
|
|
225
|
+
fastMode: typeof value?.modelOptions?.fastMode === "boolean"
|
|
226
|
+
? value.modelOptions.fastMode
|
|
227
|
+
: DEFAULT_CURSOR_MODEL_OPTIONS.fastMode,
|
|
228
|
+
},
|
|
229
|
+
planMode: false,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
207
233
|
function normalizeProviderDefaults(value: AppSettingsFile["providerDefaults"] | undefined): ChatProviderPreferences {
|
|
208
234
|
const defaults = createDefaultProviderDefaults()
|
|
209
235
|
return {
|
|
210
236
|
claude: normalizeClaudePreference(value?.claude ?? defaults.claude),
|
|
211
237
|
codex: normalizeCodexPreference(value?.codex ?? defaults.codex),
|
|
238
|
+
cursor: normalizeCursorPreference(value?.cursor ?? defaults.cursor),
|
|
212
239
|
}
|
|
213
240
|
}
|
|
214
241
|
|
|
@@ -348,6 +375,14 @@ function applyPatch(state: AppSettingsState, patch: AppSettingsPatch): AppSettin
|
|
|
348
375
|
...patch.providerDefaults?.codex?.modelOptions,
|
|
349
376
|
},
|
|
350
377
|
},
|
|
378
|
+
cursor: {
|
|
379
|
+
...state.providerDefaults.cursor,
|
|
380
|
+
...patch.providerDefaults?.cursor,
|
|
381
|
+
modelOptions: {
|
|
382
|
+
...state.providerDefaults.cursor.modelOptions,
|
|
383
|
+
...patch.providerDefaults?.cursor?.modelOptions,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
351
386
|
},
|
|
352
387
|
}, state.filePathDisplay).payload
|
|
353
388
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal push/pull async queue used to turn callback/stream-driven agent
|
|
3
|
+
* output into an `AsyncIterable`. Shared by the provider harness adapters
|
|
4
|
+
* (Codex app-server, Cursor CLI).
|
|
5
|
+
*/
|
|
6
|
+
export class AsyncQueue<T> implements AsyncIterable<T> {
|
|
7
|
+
private values: T[] = []
|
|
8
|
+
private resolvers: Array<(value: IteratorResult<T>) => void> = []
|
|
9
|
+
private done = false
|
|
10
|
+
|
|
11
|
+
push(value: T) {
|
|
12
|
+
if (this.done) return
|
|
13
|
+
const resolver = this.resolvers.shift()
|
|
14
|
+
if (resolver) {
|
|
15
|
+
resolver({ value, done: false })
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
this.values.push(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
finish() {
|
|
22
|
+
if (this.done) return
|
|
23
|
+
this.done = true
|
|
24
|
+
while (this.resolvers.length > 0) {
|
|
25
|
+
const resolver = this.resolvers.shift()
|
|
26
|
+
resolver?.({ value: undefined as T, done: true })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
31
|
+
return {
|
|
32
|
+
next: () => {
|
|
33
|
+
if (this.values.length > 0) {
|
|
34
|
+
return Promise.resolve({ value: this.values.shift() as T, done: false })
|
|
35
|
+
}
|
|
36
|
+
if (this.done) {
|
|
37
|
+
return Promise.resolve({ value: undefined as T, done: true })
|
|
38
|
+
}
|
|
39
|
+
return new Promise<IteratorResult<T>>((resolve) => {
|
|
40
|
+
this.resolvers.push(resolve)
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -210,7 +210,76 @@ describe("CodexAppServerManager", () => {
|
|
|
210
210
|
expect(threadStart?.params.serviceTier).toBe("fast")
|
|
211
211
|
expect(turnStart?.params.effort).toBe("xhigh")
|
|
212
212
|
expect(turnStart?.params.serviceTier).toBe("fast")
|
|
213
|
-
expect(turnStart?.params.collaborationMode?.settings?.reasoning_effort).
|
|
213
|
+
expect(turnStart?.params.collaborationMode?.settings?.reasoning_effort).toBe("xhigh")
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test("forwards every supported GPT-5.6 model and reasoning combination", async () => {
|
|
217
|
+
const combinations = [
|
|
218
|
+
...(["low", "medium", "high", "xhigh", "max", "ultra"] as const)
|
|
219
|
+
.map((effort) => ({ model: "gpt-5.6-sol", effort })),
|
|
220
|
+
...(["low", "medium", "high", "xhigh", "max", "ultra"] as const)
|
|
221
|
+
.map((effort) => ({ model: "gpt-5.6-terra", effort })),
|
|
222
|
+
...(["low", "medium", "high", "xhigh", "max"] as const)
|
|
223
|
+
.map((effort) => ({ model: "gpt-5.6-luna", effort })),
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
expect(combinations).toHaveLength(17)
|
|
227
|
+
|
|
228
|
+
for (const [index, combination] of combinations.entries()) {
|
|
229
|
+
const threadId = `thread-matrix-${index}`
|
|
230
|
+
const process = new FakeCodexProcess((message, child) => {
|
|
231
|
+
if (message.method === "initialize") {
|
|
232
|
+
child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
|
|
233
|
+
} else if (message.method === "thread/start") {
|
|
234
|
+
child.writeServerMessage({
|
|
235
|
+
id: message.id,
|
|
236
|
+
result: { thread: { id: threadId }, model: combination.model, reasoningEffort: combination.effort },
|
|
237
|
+
})
|
|
238
|
+
} else if (message.method === "turn/start") {
|
|
239
|
+
child.writeServerMessage({
|
|
240
|
+
id: message.id,
|
|
241
|
+
result: { turn: { id: `turn-matrix-${index}`, status: "completed", error: null } },
|
|
242
|
+
})
|
|
243
|
+
child.writeServerMessage({
|
|
244
|
+
method: "turn/completed",
|
|
245
|
+
params: {
|
|
246
|
+
threadId,
|
|
247
|
+
turn: { id: `turn-matrix-${index}`, status: "completed", error: null },
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
const manager = new CodexAppServerManager({ spawnProcess: () => process as never })
|
|
253
|
+
const chatId = `chat-matrix-${index}`
|
|
254
|
+
|
|
255
|
+
await manager.startSession({
|
|
256
|
+
chatId,
|
|
257
|
+
cwd: "/tmp/project",
|
|
258
|
+
model: combination.model,
|
|
259
|
+
sessionToken: null,
|
|
260
|
+
})
|
|
261
|
+
const turn = await manager.startTurn({
|
|
262
|
+
chatId,
|
|
263
|
+
model: combination.model,
|
|
264
|
+
effort: combination.effort,
|
|
265
|
+
content: "matrix test",
|
|
266
|
+
planMode: false,
|
|
267
|
+
onToolRequest: async () => ({}),
|
|
268
|
+
})
|
|
269
|
+
await collectStream(turn.stream)
|
|
270
|
+
|
|
271
|
+
const threadStart = process.messages.find((message: any) => message.method === "thread/start") as any
|
|
272
|
+
const turnStart = process.messages.find((message: any) => message.method === "turn/start") as any
|
|
273
|
+
expect(threadStart.params.model).toBe(combination.model)
|
|
274
|
+
expect(turnStart.params.model).toBe(combination.model)
|
|
275
|
+
expect(turnStart.params.effort).toBe(combination.effort)
|
|
276
|
+
expect(turnStart.params.collaborationMode.settings).toMatchObject({
|
|
277
|
+
model: combination.model,
|
|
278
|
+
reasoning_effort: combination.effort,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
manager.stopSession(chatId)
|
|
282
|
+
}
|
|
214
283
|
})
|
|
215
284
|
|
|
216
285
|
test("maps thread token usage updates into context window transcript entries", async () => {
|
|
@@ -11,6 +11,9 @@ import type {
|
|
|
11
11
|
TranscriptEntry,
|
|
12
12
|
} from "../shared/types"
|
|
13
13
|
import type { HarnessEvent, HarnessToolRequest, HarnessTurn } from "./harness-types"
|
|
14
|
+
import { AsyncQueue } from "./async-queue"
|
|
15
|
+
import { asNumber, asRecord } from "../shared/json"
|
|
16
|
+
import { timestamped } from "./transcript"
|
|
14
17
|
import {
|
|
15
18
|
type CollabAgentToolCallItem,
|
|
16
19
|
type ContextCompactedNotification,
|
|
@@ -142,17 +145,6 @@ export interface GenerateStructuredArgs {
|
|
|
142
145
|
serviceTier?: ServiceTier
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
function timestamped<T extends Omit<TranscriptEntry, "_id" | "createdAt">>(
|
|
146
|
-
entry: T,
|
|
147
|
-
createdAt = Date.now()
|
|
148
|
-
): TranscriptEntry {
|
|
149
|
-
return {
|
|
150
|
-
_id: randomUUID(),
|
|
151
|
-
createdAt,
|
|
152
|
-
...entry,
|
|
153
|
-
} as TranscriptEntry
|
|
154
|
-
}
|
|
155
|
-
|
|
156
148
|
function codexSystemInitEntry(model: string): TranscriptEntry {
|
|
157
149
|
return timestamped({
|
|
158
150
|
kind: "system_init",
|
|
@@ -237,15 +229,6 @@ function contentFromMcpResult(item: McpToolCallItem): unknown {
|
|
|
237
229
|
return item.result?.structuredContent ?? item.result?.content ?? null
|
|
238
230
|
}
|
|
239
231
|
|
|
240
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
241
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null
|
|
242
|
-
return value as Record<string, unknown>
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function asNumber(value: unknown): number | undefined {
|
|
246
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
|
247
|
-
}
|
|
248
|
-
|
|
249
232
|
function normalizeCodexTokenUsage(
|
|
250
233
|
notification: ThreadTokenUsageUpdatedNotification,
|
|
251
234
|
): ContextWindowUsageSnapshot | null {
|
|
@@ -692,47 +675,6 @@ function itemToToolResults(item: ThreadItem): TranscriptEntry[] {
|
|
|
692
675
|
}
|
|
693
676
|
}
|
|
694
677
|
|
|
695
|
-
class AsyncQueue<T> implements AsyncIterable<T> {
|
|
696
|
-
private values: T[] = []
|
|
697
|
-
private resolvers: Array<(value: IteratorResult<T>) => void> = []
|
|
698
|
-
private done = false
|
|
699
|
-
|
|
700
|
-
push(value: T) {
|
|
701
|
-
if (this.done) return
|
|
702
|
-
const resolver = this.resolvers.shift()
|
|
703
|
-
if (resolver) {
|
|
704
|
-
resolver({ value, done: false })
|
|
705
|
-
return
|
|
706
|
-
}
|
|
707
|
-
this.values.push(value)
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
finish() {
|
|
711
|
-
if (this.done) return
|
|
712
|
-
this.done = true
|
|
713
|
-
while (this.resolvers.length > 0) {
|
|
714
|
-
const resolver = this.resolvers.shift()
|
|
715
|
-
resolver?.({ value: undefined as T, done: true })
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
720
|
-
return {
|
|
721
|
-
next: () => {
|
|
722
|
-
if (this.values.length > 0) {
|
|
723
|
-
return Promise.resolve({ value: this.values.shift() as T, done: false })
|
|
724
|
-
}
|
|
725
|
-
if (this.done) {
|
|
726
|
-
return Promise.resolve({ value: undefined as T, done: true })
|
|
727
|
-
}
|
|
728
|
-
return new Promise<IteratorResult<T>>((resolve) => {
|
|
729
|
-
this.resolvers.push(resolve)
|
|
730
|
-
})
|
|
731
|
-
},
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
678
|
export class CodexAppServerManager {
|
|
737
679
|
private readonly sessions = new Map<string, SessionContext>()
|
|
738
680
|
private readonly spawnProcess: SpawnCodexAppServer
|
|
@@ -880,7 +822,7 @@ export class CodexAppServerManager {
|
|
|
880
822
|
mode: args.planMode ? "plan" : "default",
|
|
881
823
|
settings: {
|
|
882
824
|
model: args.model,
|
|
883
|
-
reasoning_effort: null,
|
|
825
|
+
reasoning_effort: args.effort ?? null,
|
|
884
826
|
developer_instructions: null,
|
|
885
827
|
},
|
|
886
828
|
},
|