@canonmsg/codex-plugin 0.22.2 → 0.22.3
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/adapter.d.ts +4 -0
- package/dist/app-server-adapter.d.ts +20 -0
- package/dist/app-server-adapter.js +126 -5
- package/dist/host.d.ts +6 -21
- package/dist/host.js +87 -45
- package/dist/model-catalog.d.ts +19 -0
- package/dist/model-catalog.js +137 -0
- package/package.json +1 -1
package/dist/adapter.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ export interface CodexSkillMetadata {
|
|
|
8
8
|
shortDescription?: string;
|
|
9
9
|
displayName?: string;
|
|
10
10
|
}
|
|
11
|
+
export interface CodexReasoningEffortMetadata {
|
|
12
|
+
value: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CodexModelMetadata {
|
|
16
|
+
id: string;
|
|
17
|
+
model: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
supportedReasoningEfforts: CodexReasoningEffortMetadata[];
|
|
21
|
+
defaultReasoningEffort?: string;
|
|
22
|
+
isDefault: boolean;
|
|
23
|
+
}
|
|
11
24
|
export declare class CodexAppServerAdapter {
|
|
12
25
|
private readonly cwd;
|
|
13
26
|
private readonly codexBin;
|
|
@@ -24,6 +37,7 @@ export declare class CodexAppServerAdapter {
|
|
|
24
37
|
private threadId;
|
|
25
38
|
private loadedThreadId;
|
|
26
39
|
private resolvedModel;
|
|
40
|
+
private resolvedReasoningEffort;
|
|
27
41
|
private currentTurnId;
|
|
28
42
|
private requestSeq;
|
|
29
43
|
private pending;
|
|
@@ -54,6 +68,8 @@ export declare class CodexAppServerAdapter {
|
|
|
54
68
|
dynamicTools?: readonly JsonRecord[];
|
|
55
69
|
});
|
|
56
70
|
getThreadId(): string | null;
|
|
71
|
+
getResolvedModel(): string | null;
|
|
72
|
+
getResolvedReasoningEffort(): string | null;
|
|
57
73
|
clearThreadId(): void;
|
|
58
74
|
setModel(model: string | null): void;
|
|
59
75
|
/**
|
|
@@ -78,7 +94,11 @@ export declare class CodexAppServerAdapter {
|
|
|
78
94
|
private buildWritableRoots;
|
|
79
95
|
listSkills(options?: {
|
|
80
96
|
forceReload?: boolean;
|
|
97
|
+
requestTimeoutMs?: number;
|
|
81
98
|
}): Promise<CodexSkillMetadata[]>;
|
|
99
|
+
listModels(options?: {
|
|
100
|
+
requestTimeoutMs?: number;
|
|
101
|
+
}): Promise<CodexModelMetadata[]>;
|
|
82
102
|
private buildTurnInput;
|
|
83
103
|
private ensureStarted;
|
|
84
104
|
private handleLine;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
+
const DEFAULT_DISCOVERY_REQUEST_TIMEOUT_MS = 5_000;
|
|
3
4
|
export class CodexAppServerAdapter {
|
|
4
5
|
cwd;
|
|
5
6
|
codexBin;
|
|
@@ -16,6 +17,7 @@ export class CodexAppServerAdapter {
|
|
|
16
17
|
threadId;
|
|
17
18
|
loadedThreadId = null;
|
|
18
19
|
resolvedModel = null;
|
|
20
|
+
resolvedReasoningEffort;
|
|
19
21
|
currentTurnId = null;
|
|
20
22
|
requestSeq = 1;
|
|
21
23
|
pending = new Map();
|
|
@@ -37,6 +39,7 @@ export class CodexAppServerAdapter {
|
|
|
37
39
|
this.codexBin = opts.codexBin ?? 'codex';
|
|
38
40
|
this.model = opts.model ?? null;
|
|
39
41
|
this.reasoningEffort = opts.reasoningEffort ?? null;
|
|
42
|
+
this.resolvedReasoningEffort = this.reasoningEffort;
|
|
40
43
|
this.sandbox = opts.sandbox ?? null;
|
|
41
44
|
this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
|
|
42
45
|
this.addDirs = opts.addDirs ?? [];
|
|
@@ -48,13 +51,21 @@ export class CodexAppServerAdapter {
|
|
|
48
51
|
getThreadId() {
|
|
49
52
|
return this.threadId;
|
|
50
53
|
}
|
|
54
|
+
getResolvedModel() {
|
|
55
|
+
return this.resolvedModel ?? this.model;
|
|
56
|
+
}
|
|
57
|
+
getResolvedReasoningEffort() {
|
|
58
|
+
return this.resolvedReasoningEffort ?? this.reasoningEffort;
|
|
59
|
+
}
|
|
51
60
|
clearThreadId() {
|
|
52
61
|
this.threadId = null;
|
|
53
62
|
this.loadedThreadId = null;
|
|
54
63
|
this.resolvedModel = null;
|
|
64
|
+
this.resolvedReasoningEffort = this.reasoningEffort;
|
|
55
65
|
}
|
|
56
66
|
setModel(model) {
|
|
57
67
|
this.model = model;
|
|
68
|
+
this.resolvedModel = model;
|
|
58
69
|
}
|
|
59
70
|
/**
|
|
60
71
|
* Sets GPT reasoning effort, applied via model_reasoning_effort. The config
|
|
@@ -66,9 +77,10 @@ export class CodexAppServerAdapter {
|
|
|
66
77
|
*/
|
|
67
78
|
setReasoningEffort(effort) {
|
|
68
79
|
const next = effort && effort.trim() ? effort.trim() : null;
|
|
69
|
-
if (next === this.reasoningEffort)
|
|
80
|
+
if (next === this.reasoningEffort && next === this.resolvedReasoningEffort)
|
|
70
81
|
return;
|
|
71
82
|
this.reasoningEffort = next;
|
|
83
|
+
this.resolvedReasoningEffort = next;
|
|
72
84
|
if (this.threadId && this.loadedThreadId === this.threadId) {
|
|
73
85
|
this.loadedThreadId = null;
|
|
74
86
|
}
|
|
@@ -200,7 +212,7 @@ export class CodexAppServerAdapter {
|
|
|
200
212
|
continue;
|
|
201
213
|
config[key] = parseConfigValue(raw.slice(separator + 1));
|
|
202
214
|
}
|
|
203
|
-
if (this.reasoningEffort
|
|
215
|
+
if (this.reasoningEffort) {
|
|
204
216
|
config.model_reasoning_effort = this.reasoningEffort;
|
|
205
217
|
}
|
|
206
218
|
return Object.keys(config).length > 0 ? { config } : {};
|
|
@@ -249,11 +261,38 @@ export class CodexAppServerAdapter {
|
|
|
249
261
|
const result = await this.sendRequest('skills/list', {
|
|
250
262
|
cwds: [this.cwd],
|
|
251
263
|
...(options.forceReload ? { forceReload: true } : {}),
|
|
252
|
-
});
|
|
264
|
+
}, options.requestTimeoutMs ?? DEFAULT_DISCOVERY_REQUEST_TIMEOUT_MS);
|
|
253
265
|
const parsed = parseSkillsListResponse(result);
|
|
254
266
|
this.skillsCache = parsed;
|
|
255
267
|
return parsed;
|
|
256
268
|
}
|
|
269
|
+
async listModels(options = {}) {
|
|
270
|
+
await this.ensureStarted();
|
|
271
|
+
const models = [];
|
|
272
|
+
const seen = new Set();
|
|
273
|
+
const seenCursors = new Set();
|
|
274
|
+
let cursor = null;
|
|
275
|
+
do {
|
|
276
|
+
const result = await this.sendRequest('model/list', {
|
|
277
|
+
limit: 100,
|
|
278
|
+
includeHidden: false,
|
|
279
|
+
...(cursor ? { cursor } : {}),
|
|
280
|
+
}, options.requestTimeoutMs ?? DEFAULT_DISCOVERY_REQUEST_TIMEOUT_MS);
|
|
281
|
+
for (const model of parseModelListResponse(result)) {
|
|
282
|
+
if (seen.has(model.model))
|
|
283
|
+
continue;
|
|
284
|
+
seen.add(model.model);
|
|
285
|
+
models.push(model);
|
|
286
|
+
}
|
|
287
|
+
const nextCursor = readString(result, 'nextCursor') ?? null;
|
|
288
|
+
if (nextCursor && seenCursors.has(nextCursor))
|
|
289
|
+
break;
|
|
290
|
+
if (nextCursor)
|
|
291
|
+
seenCursors.add(nextCursor);
|
|
292
|
+
cursor = nextCursor;
|
|
293
|
+
} while (cursor);
|
|
294
|
+
return models;
|
|
295
|
+
}
|
|
257
296
|
async buildTurnInput(prompt, imagePaths) {
|
|
258
297
|
const skillPrompt = parseSkillSlashPrompt(prompt);
|
|
259
298
|
const input = [];
|
|
@@ -373,6 +412,25 @@ export class CodexAppServerAdapter {
|
|
|
373
412
|
}
|
|
374
413
|
if (!this.isCurrentThreadNotification(params))
|
|
375
414
|
return;
|
|
415
|
+
if (method === 'thread/settings/updated') {
|
|
416
|
+
const settings = isRecord(params.threadSettings) ? params.threadSettings : {};
|
|
417
|
+
const model = readString(settings, 'model') ?? null;
|
|
418
|
+
const effort = readString(settings, 'effort') ?? null;
|
|
419
|
+
this.resolvedModel = model ?? this.resolvedModel;
|
|
420
|
+
this.resolvedReasoningEffort = effort;
|
|
421
|
+
this.currentOnEvent?.({ type: 'settings.updated', model, effort });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (method === 'model/rerouted') {
|
|
425
|
+
const model = readString(params, 'toModel') ?? null;
|
|
426
|
+
this.resolvedModel = model ?? this.resolvedModel;
|
|
427
|
+
this.currentOnEvent?.({
|
|
428
|
+
type: 'settings.updated',
|
|
429
|
+
model,
|
|
430
|
+
effort: this.getResolvedReasoningEffort(),
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
376
434
|
if (method === 'turn/started') {
|
|
377
435
|
this.currentTurnId = readString(params.turn, 'id') ?? this.currentTurnId;
|
|
378
436
|
this.currentOnEvent?.({ type: 'turn.started' });
|
|
@@ -518,15 +576,39 @@ export class CodexAppServerAdapter {
|
|
|
518
576
|
this.messageTextByItem.clear();
|
|
519
577
|
this.planText = '';
|
|
520
578
|
}
|
|
521
|
-
sendRequest(method, params) {
|
|
579
|
+
sendRequest(method, params, timeoutMs) {
|
|
522
580
|
const id = this.requestSeq++;
|
|
523
581
|
return new Promise((resolve, reject) => {
|
|
524
|
-
|
|
582
|
+
let timer = null;
|
|
583
|
+
const clearTimer = () => {
|
|
584
|
+
if (!timer)
|
|
585
|
+
return;
|
|
586
|
+
clearTimeout(timer);
|
|
587
|
+
timer = null;
|
|
588
|
+
};
|
|
589
|
+
this.pending.set(id, {
|
|
590
|
+
resolve: (value) => {
|
|
591
|
+
clearTimer();
|
|
592
|
+
resolve(value);
|
|
593
|
+
},
|
|
594
|
+
reject: (error) => {
|
|
595
|
+
clearTimer();
|
|
596
|
+
reject(error);
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
600
|
+
timer = setTimeout(() => {
|
|
601
|
+
if (!this.pending.delete(id))
|
|
602
|
+
return;
|
|
603
|
+
reject(new Error(`Codex app-server ${method} timed out after ${timeoutMs}ms`));
|
|
604
|
+
}, timeoutMs);
|
|
605
|
+
}
|
|
525
606
|
try {
|
|
526
607
|
this.write({ id, method, params });
|
|
527
608
|
}
|
|
528
609
|
catch (error) {
|
|
529
610
|
this.pending.delete(id);
|
|
611
|
+
clearTimer();
|
|
530
612
|
reject(error);
|
|
531
613
|
}
|
|
532
614
|
});
|
|
@@ -619,6 +701,45 @@ function parseSkillsListResponse(result) {
|
|
|
619
701
|
}
|
|
620
702
|
return skills;
|
|
621
703
|
}
|
|
704
|
+
function parseModelListResponse(result) {
|
|
705
|
+
const entries = Array.isArray(result.data) ? result.data : [];
|
|
706
|
+
const models = [];
|
|
707
|
+
for (const entry of entries) {
|
|
708
|
+
if (!isRecord(entry) || entry.hidden === true)
|
|
709
|
+
continue;
|
|
710
|
+
const id = readString(entry, 'id');
|
|
711
|
+
const model = readString(entry, 'model') ?? id;
|
|
712
|
+
if (!id || !model)
|
|
713
|
+
continue;
|
|
714
|
+
const displayName = readString(entry, 'displayName') ?? model;
|
|
715
|
+
const description = readString(entry, 'description');
|
|
716
|
+
const efforts = Array.isArray(entry.supportedReasoningEfforts)
|
|
717
|
+
? entry.supportedReasoningEfforts.flatMap((rawEffort) => {
|
|
718
|
+
if (!isRecord(rawEffort))
|
|
719
|
+
return [];
|
|
720
|
+
const value = readString(rawEffort, 'reasoningEffort');
|
|
721
|
+
if (!value)
|
|
722
|
+
return [];
|
|
723
|
+
const effortDescription = readString(rawEffort, 'description');
|
|
724
|
+
return [{
|
|
725
|
+
value,
|
|
726
|
+
...(effortDescription ? { description: effortDescription } : {}),
|
|
727
|
+
}];
|
|
728
|
+
})
|
|
729
|
+
: [];
|
|
730
|
+
const defaultReasoningEffort = readString(entry, 'defaultReasoningEffort');
|
|
731
|
+
models.push({
|
|
732
|
+
id,
|
|
733
|
+
model,
|
|
734
|
+
displayName,
|
|
735
|
+
...(description ? { description } : {}),
|
|
736
|
+
supportedReasoningEfforts: efforts,
|
|
737
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
738
|
+
isDefault: entry.isDefault === true,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return models;
|
|
742
|
+
}
|
|
622
743
|
function stringifyPreview(value) {
|
|
623
744
|
if (typeof value === 'string')
|
|
624
745
|
return value;
|
package/dist/host.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type WorkspaceOption, type CanonWorkspaceRootMetadata } from '@canonmsg/core';
|
|
3
3
|
import { type CodexSkillMetadata } from './app-server-adapter.js';
|
|
4
|
+
import { type CodexControlOption } from './model-catalog.js';
|
|
4
5
|
interface HostSessionState {
|
|
5
6
|
lastError?: string;
|
|
6
7
|
model?: string;
|
|
@@ -32,30 +33,14 @@ export declare function buildCodexLiveSessionConfig(input: {
|
|
|
32
33
|
permissionMode?: string | undefined;
|
|
33
34
|
model?: string | undefined;
|
|
34
35
|
};
|
|
35
|
-
/**
|
|
36
|
-
export declare const CODEX_EFFORT_OPTIONS: readonly [
|
|
37
|
-
readonly value: "minimal";
|
|
38
|
-
readonly label: "Minimal";
|
|
39
|
-
}, {
|
|
40
|
-
readonly value: "low";
|
|
41
|
-
readonly label: "Low";
|
|
42
|
-
}, {
|
|
43
|
-
readonly value: "medium";
|
|
44
|
-
readonly label: "Medium";
|
|
45
|
-
}, {
|
|
46
|
-
readonly value: "high";
|
|
47
|
-
readonly label: "High";
|
|
48
|
-
}, {
|
|
49
|
-
readonly value: "xhigh";
|
|
50
|
-
readonly label: "Extra high";
|
|
51
|
-
}];
|
|
36
|
+
/** Conservative fallback used only when native app-server discovery is unavailable. */
|
|
37
|
+
export declare const CODEX_EFFORT_OPTIONS: readonly CodexControlOption[];
|
|
52
38
|
export declare const CODEX_SESSION_CONFIG_FIELDS: readonly ["permissionMode", "effort"];
|
|
53
39
|
export declare function buildCodexSkillCommands(skills: ReadonlyArray<CodexSkillMetadata>): CanonRuntimeCommandDescriptor[];
|
|
54
40
|
export declare function buildCodexRuntimeDescriptor(input: {
|
|
55
|
-
models:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}>;
|
|
41
|
+
models: CodexControlOption[];
|
|
42
|
+
effortOptions?: CodexControlOption[];
|
|
43
|
+
defaultEffort?: string | null;
|
|
59
44
|
workspaces: WorkspaceOption[];
|
|
60
45
|
workspaceRoots?: CanonWorkspaceRootMetadata[];
|
|
61
46
|
executionModes: ExecutionEnvironmentMode[];
|
package/dist/host.js
CHANGED
|
@@ -9,7 +9,7 @@ import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/co
|
|
|
9
9
|
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
10
10
|
import { decideAutoReply, } from './inbound-policy.js';
|
|
11
11
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
12
|
-
import { CodexAppServerAdapter } from './app-server-adapter.js';
|
|
12
|
+
import { CodexAppServerAdapter, } from './app-server-adapter.js';
|
|
13
13
|
import { CODEX_APP_DYNAMIC_TOOLS, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, } from './codex-app-tools.js';
|
|
14
14
|
import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
|
|
15
15
|
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
@@ -21,6 +21,7 @@ import { createCodexControlPoller } from './control-channel.js';
|
|
|
21
21
|
import { runCli } from './cli-entry.js';
|
|
22
22
|
import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
|
|
23
23
|
import { applyTextSegmentBlock, beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
|
|
24
|
+
import { FALLBACK_CODEX_EFFORT_OPTIONS, buildCodexEffortOptions, buildCodexModelOptions, readCodexConfiguredEffort, resolveCodexDefaultModel, resolveCodexEffortForModel, } from './model-catalog.js';
|
|
24
25
|
const HELP = `canon-codex — run a local Codex agent host for Canon
|
|
25
26
|
|
|
26
27
|
USAGE
|
|
@@ -88,15 +89,8 @@ let workingDir = process.cwd();
|
|
|
88
89
|
let workspaceOptions = [];
|
|
89
90
|
let workspaceRoots = [];
|
|
90
91
|
let workspaceRootMetadata = [];
|
|
91
|
-
/**
|
|
92
|
-
export const CODEX_EFFORT_OPTIONS =
|
|
93
|
-
{ value: 'minimal', label: 'Minimal' },
|
|
94
|
-
{ value: 'low', label: 'Low' },
|
|
95
|
-
{ value: 'medium', label: 'Medium' },
|
|
96
|
-
{ value: 'high', label: 'High' },
|
|
97
|
-
{ value: 'xhigh', label: 'Extra high' },
|
|
98
|
-
];
|
|
99
|
-
const CODEX_EFFORT_VALUES = new Set(CODEX_EFFORT_OPTIONS.map((option) => option.value));
|
|
92
|
+
/** Conservative fallback used only when native app-server discovery is unavailable. */
|
|
93
|
+
export const CODEX_EFFORT_OPTIONS = FALLBACK_CODEX_EFFORT_OPTIONS;
|
|
100
94
|
export const CODEX_SESSION_CONFIG_FIELDS = ['permissionMode', 'effort'];
|
|
101
95
|
const MAX_CODEX_SKILL_COMMAND_CHOICES = 50;
|
|
102
96
|
export function buildCodexSkillCommands(skills) {
|
|
@@ -176,8 +170,8 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
176
170
|
defaultPermissionMode: input.defaultPermissionMode,
|
|
177
171
|
permissionModeLabel: 'Execution policy',
|
|
178
172
|
modelLiveBehavior: 'next_turn',
|
|
179
|
-
effortOptions: [...CODEX_EFFORT_OPTIONS],
|
|
180
|
-
defaultEffort: 'medium',
|
|
173
|
+
effortOptions: input.effortOptions ?? [...CODEX_EFFORT_OPTIONS],
|
|
174
|
+
defaultEffort: input.defaultEffort ?? 'medium',
|
|
181
175
|
effortLiveBehavior: 'next_turn',
|
|
182
176
|
presentation: input.presentation,
|
|
183
177
|
streamingTextMode: 'snapshot',
|
|
@@ -242,20 +236,6 @@ export function buildCodexTurnResponseRouting(input) {
|
|
|
242
236
|
export function getCodexRequestingUserId(message) {
|
|
243
237
|
return message.senderType === 'human' ? message.senderId : null;
|
|
244
238
|
}
|
|
245
|
-
function modelOptionLabel(model) {
|
|
246
|
-
if (/^gpt-5\.5(?:$|[-_:])/i.test(model))
|
|
247
|
-
return 'GPT-5.5';
|
|
248
|
-
if (/^gpt-5\.4-mini(?:$|[-_:])/i.test(model))
|
|
249
|
-
return 'GPT-5.4 Mini';
|
|
250
|
-
if (/^gpt-5\.4(?:$|[-_:])/i.test(model))
|
|
251
|
-
return 'GPT-5.4';
|
|
252
|
-
return model;
|
|
253
|
-
}
|
|
254
|
-
function buildCodexModelOptions(model) {
|
|
255
|
-
return typeof model === 'string' && model.trim()
|
|
256
|
-
? [{ value: model.trim(), label: modelOptionLabel(model.trim()) }]
|
|
257
|
-
: [];
|
|
258
|
-
}
|
|
259
239
|
async function publishAgentRuntime(agentId, runtime) {
|
|
260
240
|
await publishHostAgentRuntime(agentId, 'codex', runtime);
|
|
261
241
|
}
|
|
@@ -1019,9 +999,13 @@ export async function main() {
|
|
|
1019
999
|
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
1020
1000
|
}
|
|
1021
1001
|
const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint);
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
:
|
|
1002
|
+
const effectiveModel = policy.model ?? codexDefaultModel;
|
|
1003
|
+
const initialEffortResolution = resolveCodexEffortForModel({
|
|
1004
|
+
models: codexModels,
|
|
1005
|
+
model: effectiveModel,
|
|
1006
|
+
requestedEffort: config?.effort ?? configuredCodexEffort,
|
|
1007
|
+
});
|
|
1008
|
+
const initialEffort = initialEffortResolution.value;
|
|
1025
1009
|
const adapter = useAppServer
|
|
1026
1010
|
? new CodexAppServerAdapter({
|
|
1027
1011
|
cwd: sessionCwd,
|
|
@@ -1059,7 +1043,7 @@ export async function main() {
|
|
|
1059
1043
|
queue: [],
|
|
1060
1044
|
running: false,
|
|
1061
1045
|
state: buildCodexInitialSessionState({
|
|
1062
|
-
model:
|
|
1046
|
+
model: effectiveModel ?? undefined,
|
|
1063
1047
|
permissionMode: policy.permissionMode,
|
|
1064
1048
|
effort: initialEffort,
|
|
1065
1049
|
}),
|
|
@@ -1577,6 +1561,13 @@ export async function main() {
|
|
|
1577
1561
|
void refreshCodexSkillInventory(true).then(() => publishRuntimeHeartbeat());
|
|
1578
1562
|
return;
|
|
1579
1563
|
}
|
|
1564
|
+
if (event.type === 'settings.updated') {
|
|
1565
|
+
if (event.model)
|
|
1566
|
+
session.state.model = event.model;
|
|
1567
|
+
session.state.effort = event.effort ?? undefined;
|
|
1568
|
+
writeState(session);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1580
1571
|
if (event.type === 'message') {
|
|
1581
1572
|
session.turnState = 'streaming';
|
|
1582
1573
|
markTurnProgress(session);
|
|
@@ -1679,6 +1670,17 @@ export async function main() {
|
|
|
1679
1670
|
clearStoredThread();
|
|
1680
1671
|
result = await runTurnOnce();
|
|
1681
1672
|
}
|
|
1673
|
+
if (session.adapter instanceof CodexAppServerAdapter) {
|
|
1674
|
+
const resolvedModel = session.adapter.getResolvedModel();
|
|
1675
|
+
const resolvedEffort = session.adapter.getResolvedReasoningEffort();
|
|
1676
|
+
if ((resolvedModel && resolvedModel !== session.state.model)
|
|
1677
|
+
|| resolvedEffort !== (session.state.effort ?? null)) {
|
|
1678
|
+
if (resolvedModel)
|
|
1679
|
+
session.state.model = resolvedModel;
|
|
1680
|
+
session.state.effort = resolvedEffort ?? undefined;
|
|
1681
|
+
writeState(session);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1682
1684
|
if (result.threadId && !session.resetRequested) {
|
|
1683
1685
|
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
|
|
1684
1686
|
}
|
|
@@ -1867,7 +1869,16 @@ export async function main() {
|
|
|
1867
1869
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
1868
1870
|
];
|
|
1869
1871
|
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
|
|
1870
|
-
const
|
|
1872
|
+
const configuredCodexEffort = readCodexConfiguredEffort(args.config ?? []);
|
|
1873
|
+
let codexModels = [];
|
|
1874
|
+
let codexModelOptions = buildCodexModelOptions(codexModels, args.model);
|
|
1875
|
+
let codexEffortOptions = buildCodexEffortOptions(codexModels);
|
|
1876
|
+
let codexDefaultModel = resolveCodexDefaultModel(codexModels, args.model);
|
|
1877
|
+
let codexDefaultEffort = resolveCodexEffortForModel({
|
|
1878
|
+
models: codexModels,
|
|
1879
|
+
model: codexDefaultModel,
|
|
1880
|
+
requestedEffort: configuredCodexEffort,
|
|
1881
|
+
}).value;
|
|
1871
1882
|
const runtimePresentation = buildRuntimePresentationPolicy({
|
|
1872
1883
|
base: DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION,
|
|
1873
1884
|
preset: parseRuntimeVisibilityPreset(args['runtime-visibility']),
|
|
@@ -1877,7 +1888,7 @@ export async function main() {
|
|
|
1877
1888
|
let codexSkills = [];
|
|
1878
1889
|
const buildCurrentRuntimeDescriptor = () => ({
|
|
1879
1890
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
1880
|
-
...(
|
|
1891
|
+
...(codexDefaultModel ? { defaultModel: codexDefaultModel } : {}),
|
|
1881
1892
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1882
1893
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1883
1894
|
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
@@ -1886,6 +1897,8 @@ export async function main() {
|
|
|
1886
1897
|
: {}),
|
|
1887
1898
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
1888
1899
|
models: codexModelOptions,
|
|
1900
|
+
effortOptions: codexEffortOptions,
|
|
1901
|
+
defaultEffort: codexDefaultEffort,
|
|
1889
1902
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1890
1903
|
workspaceRoots: workspaceRootMetadata,
|
|
1891
1904
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -1908,16 +1921,32 @@ export async function main() {
|
|
|
1908
1921
|
model: typeof args.model === 'string' ? args.model : null,
|
|
1909
1922
|
configOverrides: args.config ?? [],
|
|
1910
1923
|
});
|
|
1924
|
+
try {
|
|
1925
|
+
const discoveredModels = await probe.listModels();
|
|
1926
|
+
if (discoveredModels.length > 0) {
|
|
1927
|
+
codexModels = discoveredModels;
|
|
1928
|
+
codexModelOptions = buildCodexModelOptions(codexModels, args.model);
|
|
1929
|
+
codexEffortOptions = buildCodexEffortOptions(codexModels);
|
|
1930
|
+
codexDefaultModel = resolveCodexDefaultModel(codexModels, args.model);
|
|
1931
|
+
codexDefaultEffort = resolveCodexEffortForModel({
|
|
1932
|
+
models: codexModels,
|
|
1933
|
+
model: codexDefaultModel,
|
|
1934
|
+
requestedEffort: configuredCodexEffort,
|
|
1935
|
+
}).value;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
catch (error) {
|
|
1939
|
+
console.error('[canon-codex] Failed to load Codex models:', error instanceof Error ? error.message : error);
|
|
1940
|
+
}
|
|
1911
1941
|
try {
|
|
1912
1942
|
codexSkills = await probe.listSkills({ forceReload });
|
|
1913
|
-
runtimeDescriptor = buildCurrentRuntimeDescriptor();
|
|
1914
1943
|
}
|
|
1915
1944
|
catch (error) {
|
|
1916
1945
|
codexSkills = [];
|
|
1917
|
-
runtimeDescriptor = buildCurrentRuntimeDescriptor();
|
|
1918
1946
|
console.error('[canon-codex] Failed to load Codex skills:', error instanceof Error ? error.message : error);
|
|
1919
1947
|
}
|
|
1920
1948
|
finally {
|
|
1949
|
+
runtimeDescriptor = buildCurrentRuntimeDescriptor();
|
|
1921
1950
|
probe.close();
|
|
1922
1951
|
}
|
|
1923
1952
|
}
|
|
@@ -1925,7 +1954,13 @@ export async function main() {
|
|
|
1925
1954
|
const session = sessions.get(conversationId);
|
|
1926
1955
|
if (!session || session.closed)
|
|
1927
1956
|
return;
|
|
1957
|
+
let modelChanged = false;
|
|
1928
1958
|
if (control.model && control.model !== session.state.model) {
|
|
1959
|
+
if (codexModelOptions.length > 0 && !codexModelOptions.some((option) => option.value === control.model)) {
|
|
1960
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring model outside the discovered Codex catalog (${control.model})`);
|
|
1961
|
+
writeState(session);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1929
1964
|
const modelGuard = buildCodexModelGuardMessage(control.model, codexCliStatus);
|
|
1930
1965
|
if (modelGuard) {
|
|
1931
1966
|
session.state.lastError = modelGuard;
|
|
@@ -1937,29 +1972,36 @@ export async function main() {
|
|
|
1937
1972
|
}
|
|
1938
1973
|
session.adapter.setModel(control.model);
|
|
1939
1974
|
session.state.model = control.model;
|
|
1975
|
+
modelChanged = true;
|
|
1940
1976
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Model set for next turn -> ${control.model}`);
|
|
1941
|
-
writeState(session);
|
|
1942
1977
|
}
|
|
1943
1978
|
if (control.permissionMode) {
|
|
1944
1979
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] approval mode is session-creation-only; ignoring mid-session change request (${control.permissionMode})`);
|
|
1945
1980
|
// Convergence contract: a consumed session control must always be
|
|
1946
1981
|
// answered. Re-publish the currently applied state so clients settle on
|
|
1947
1982
|
// the authoritative value instead of holding the composer until timeout.
|
|
1948
|
-
writeState(session);
|
|
1949
1983
|
}
|
|
1950
|
-
if (control.effort) {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
session.state.
|
|
1984
|
+
if (control.effort || modelChanged) {
|
|
1985
|
+
const effortResolution = resolveCodexEffortForModel({
|
|
1986
|
+
models: codexModels,
|
|
1987
|
+
model: session.state.model,
|
|
1988
|
+
requestedEffort: control.effort ?? session.state.effort,
|
|
1989
|
+
});
|
|
1990
|
+
if (effortResolution.value !== session.state.effort) {
|
|
1991
|
+
session.adapter.setReasoningEffort(effortResolution.value);
|
|
1992
|
+
session.state.effort = effortResolution.value ?? undefined;
|
|
1993
|
+
}
|
|
1994
|
+
if (control.effort && !effortResolution.accepted) {
|
|
1995
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${control.effort} is unsupported by ${session.state.model ?? 'the active model'}; reset to ${effortResolution.value ?? 'the model default'}`);
|
|
1996
|
+
}
|
|
1997
|
+
else if (control.effort) {
|
|
1954
1998
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Reasoning effort set for next turn -> ${control.effort}`);
|
|
1955
|
-
writeState(session);
|
|
1956
1999
|
}
|
|
1957
|
-
else {
|
|
1958
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}]
|
|
1959
|
-
// Same contract: ignored values still get an authoritative re-publish.
|
|
1960
|
-
writeState(session);
|
|
2000
|
+
else if (modelChanged && effortResolution.value) {
|
|
2001
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Reasoning effort for ${session.state.model} -> ${effortResolution.value}`);
|
|
1961
2002
|
}
|
|
1962
2003
|
}
|
|
2004
|
+
writeState(session);
|
|
1963
2005
|
}
|
|
1964
2006
|
async function handleControlSignal(event) {
|
|
1965
2007
|
const { conversationId, type } = event;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CodexModelMetadata } from './app-server-adapter.js';
|
|
2
|
+
export interface CodexControlOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const FALLBACK_CODEX_EFFORT_OPTIONS: ReadonlyArray<CodexControlOption>;
|
|
8
|
+
export declare function buildCodexModelOptions(models: ReadonlyArray<CodexModelMetadata>, configuredModel?: unknown): CodexControlOption[];
|
|
9
|
+
export declare function buildCodexEffortOptions(models: ReadonlyArray<CodexModelMetadata>): CodexControlOption[];
|
|
10
|
+
export declare function resolveCodexDefaultModel(models: ReadonlyArray<CodexModelMetadata>, configuredModel?: unknown): string | null;
|
|
11
|
+
export declare function resolveCodexEffortForModel(input: {
|
|
12
|
+
models: ReadonlyArray<CodexModelMetadata>;
|
|
13
|
+
model: string | null | undefined;
|
|
14
|
+
requestedEffort: string | null | undefined;
|
|
15
|
+
}): {
|
|
16
|
+
value: string | null;
|
|
17
|
+
accepted: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function readCodexConfiguredEffort(configOverrides: ReadonlyArray<string>): string | null;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const EFFORT_LABELS = {
|
|
2
|
+
minimal: 'Minimal',
|
|
3
|
+
low: 'Low',
|
|
4
|
+
medium: 'Medium',
|
|
5
|
+
high: 'High',
|
|
6
|
+
xhigh: 'Extra high',
|
|
7
|
+
max: 'Max',
|
|
8
|
+
ultra: 'Ultra',
|
|
9
|
+
};
|
|
10
|
+
const EFFORT_ORDER = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
11
|
+
export const FALLBACK_CODEX_EFFORT_OPTIONS = [
|
|
12
|
+
{ value: 'minimal', label: 'Minimal' },
|
|
13
|
+
{ value: 'low', label: 'Low' },
|
|
14
|
+
{ value: 'medium', label: 'Medium' },
|
|
15
|
+
{ value: 'high', label: 'High' },
|
|
16
|
+
{ value: 'xhigh', label: 'Extra high' },
|
|
17
|
+
];
|
|
18
|
+
export function buildCodexModelOptions(models, configuredModel) {
|
|
19
|
+
const configured = typeof configuredModel === 'string' ? configuredModel.trim() : '';
|
|
20
|
+
const ordered = [...models].sort((left, right) => {
|
|
21
|
+
if (configured) {
|
|
22
|
+
if (left.model === configured)
|
|
23
|
+
return -1;
|
|
24
|
+
if (right.model === configured)
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
return Number(right.isDefault) - Number(left.isDefault);
|
|
28
|
+
});
|
|
29
|
+
const options = ordered.map((model) => ({
|
|
30
|
+
value: model.model,
|
|
31
|
+
label: model.displayName,
|
|
32
|
+
...(model.description ? { description: model.description } : {}),
|
|
33
|
+
}));
|
|
34
|
+
if (configured && !options.some((option) => option.value === configured)) {
|
|
35
|
+
options.push({ value: configured, label: configured });
|
|
36
|
+
}
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
export function buildCodexEffortOptions(models) {
|
|
40
|
+
if (models.length === 0)
|
|
41
|
+
return [...FALLBACK_CODEX_EFFORT_OPTIONS];
|
|
42
|
+
const values = new Set();
|
|
43
|
+
const descriptions = new Map();
|
|
44
|
+
for (const model of models) {
|
|
45
|
+
for (const effort of model.supportedReasoningEfforts) {
|
|
46
|
+
values.add(effort.value);
|
|
47
|
+
if (effort.description && !descriptions.has(effort.value)) {
|
|
48
|
+
descriptions.set(effort.value, effort.description);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...values]
|
|
53
|
+
.sort((left, right) => {
|
|
54
|
+
const leftIndex = EFFORT_ORDER.indexOf(left);
|
|
55
|
+
const rightIndex = EFFORT_ORDER.indexOf(right);
|
|
56
|
+
if (leftIndex === -1 && rightIndex === -1)
|
|
57
|
+
return left.localeCompare(right);
|
|
58
|
+
if (leftIndex === -1)
|
|
59
|
+
return 1;
|
|
60
|
+
if (rightIndex === -1)
|
|
61
|
+
return -1;
|
|
62
|
+
return leftIndex - rightIndex;
|
|
63
|
+
})
|
|
64
|
+
.map((value) => {
|
|
65
|
+
const description = buildEffortDescription(value, descriptions.get(value), models);
|
|
66
|
+
return {
|
|
67
|
+
value,
|
|
68
|
+
label: EFFORT_LABELS[value] ?? humanizeEffort(value),
|
|
69
|
+
...(description ? { description } : {}),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function resolveCodexDefaultModel(models, configuredModel) {
|
|
74
|
+
const configured = typeof configuredModel === 'string' ? configuredModel.trim() : '';
|
|
75
|
+
if (configured)
|
|
76
|
+
return configured;
|
|
77
|
+
return models.find((model) => model.isDefault)?.model ?? models[0]?.model ?? null;
|
|
78
|
+
}
|
|
79
|
+
export function resolveCodexEffortForModel(input) {
|
|
80
|
+
const requested = input.requestedEffort?.trim() || null;
|
|
81
|
+
const model = input.models.find((candidate) => candidate.model === input.model);
|
|
82
|
+
if (!model) {
|
|
83
|
+
const fallbackValues = new Set([
|
|
84
|
+
...FALLBACK_CODEX_EFFORT_OPTIONS.map((option) => option.value),
|
|
85
|
+
...input.models.flatMap((candidate) => (candidate.supportedReasoningEfforts.map((effort) => effort.value))),
|
|
86
|
+
]);
|
|
87
|
+
if (!requested)
|
|
88
|
+
return { value: 'medium', accepted: true };
|
|
89
|
+
return fallbackValues.has(requested)
|
|
90
|
+
? { value: requested, accepted: true }
|
|
91
|
+
: { value: 'medium', accepted: false };
|
|
92
|
+
}
|
|
93
|
+
const supported = new Set(model.supportedReasoningEfforts.map((effort) => effort.value));
|
|
94
|
+
const fallback = supported.has(model.defaultReasoningEffort ?? '')
|
|
95
|
+
? model.defaultReasoningEffort ?? null
|
|
96
|
+
: model.supportedReasoningEfforts[0]?.value ?? null;
|
|
97
|
+
if (!requested)
|
|
98
|
+
return { value: fallback, accepted: true };
|
|
99
|
+
return supported.has(requested)
|
|
100
|
+
? { value: requested, accepted: true }
|
|
101
|
+
: { value: fallback, accepted: false };
|
|
102
|
+
}
|
|
103
|
+
export function readCodexConfiguredEffort(configOverrides) {
|
|
104
|
+
let configured = null;
|
|
105
|
+
for (const raw of configOverrides) {
|
|
106
|
+
const separator = raw.indexOf('=');
|
|
107
|
+
if (separator <= 0 || raw.slice(0, separator).trim() !== 'model_reasoning_effort')
|
|
108
|
+
continue;
|
|
109
|
+
const value = raw.slice(separator + 1).trim();
|
|
110
|
+
if (!value)
|
|
111
|
+
continue;
|
|
112
|
+
configured = unquote(value);
|
|
113
|
+
}
|
|
114
|
+
return configured;
|
|
115
|
+
}
|
|
116
|
+
function humanizeEffort(value) {
|
|
117
|
+
return value
|
|
118
|
+
.replace(/[_-]+/g, ' ')
|
|
119
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
120
|
+
}
|
|
121
|
+
function buildEffortDescription(effort, nativeDescription, models) {
|
|
122
|
+
const supportedBy = models.filter((model) => (model.supportedReasoningEfforts.some((candidate) => candidate.value === effort)));
|
|
123
|
+
const compatibility = supportedBy.length > 0 && supportedBy.length < models.length
|
|
124
|
+
? `Supported by ${supportedBy.map((model) => model.displayName).join(', ')}.`
|
|
125
|
+
: '';
|
|
126
|
+
return [nativeDescription, compatibility].filter(Boolean).join(' ') || undefined;
|
|
127
|
+
}
|
|
128
|
+
function unquote(value) {
|
|
129
|
+
if (value.length >= 2) {
|
|
130
|
+
const first = value[0];
|
|
131
|
+
const last = value[value.length - 1];
|
|
132
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
133
|
+
return value.slice(1, -1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|