@khorsheed/dsh-local-agent-codex 0.1.0-rc.6
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/README.en.md +136 -0
- package/README.i18n.yaml +6 -0
- package/README.md +136 -0
- package/cordis.patch.yml +42 -0
- package/lib/client.js +1066 -0
- package/lib/index.js +3550 -0
- package/lib/invariant.js +31 -0
- package/lib/types/client/SettingsCard.d.ts +76 -0
- package/lib/types/client/SettingsCard.js +200 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +43 -0
- package/lib/types/client/index.js +88 -0
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/locales.js +55 -0
- package/lib/types/codex-cli-provider.d.ts +347 -0
- package/lib/types/codex-cli-provider.js +1225 -0
- package/lib/types/index.d.ts +70 -0
- package/lib/types/index.js +272 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/live-driver.d.ts +229 -0
- package/lib/types/live-driver.js +1279 -0
- package/lib/types/live-switch.d.ts +70 -0
- package/lib/types/live-switch.js +90 -0
- package/lib/types/model-broker.d.ts +102 -0
- package/lib/types/model-broker.js +187 -0
- package/lib/types/model-catalog.d.ts +44 -0
- package/lib/types/model-catalog.js +220 -0
- package/lib/types/provision.d.ts +80 -0
- package/lib/types/provision.js +213 -0
- package/lib/types/records.d.ts +120 -0
- package/lib/types/records.js +505 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +106 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex harness instantiation of the local-agent family. Registers the
|
|
3
|
+
* `codex` harness into the core registry: scoped home under the shared homes
|
|
4
|
+
* root (`CODEX_HOME`), device-code login through `codex login --device-auth`
|
|
5
|
+
* (prompt captured from stdout), and rollout-file session records.
|
|
6
|
+
* Delegation mounts through the `codex-local` provider (one-shot
|
|
7
|
+
* `codex exec` under the scoped home); the bundle patch's
|
|
8
|
+
* `tool-subagent-codex-local` row puts the tool on the profile root, so
|
|
9
|
+
* every agent preset can delegate.
|
|
10
|
+
*
|
|
11
|
+
* @module @khorsheed/dsh-local-agent-codex
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
import z from '@deepseek-ai/schemastery';
|
|
15
|
+
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
16
|
+
export declare const name = "local-agent-codex";
|
|
17
|
+
/**
|
|
18
|
+
* Boot delay before the model catalog's eager warmup probe fires. The probe
|
|
19
|
+
* spawns a `codex app-server` — cheap, but not free — so it waits out the
|
|
20
|
+
* boot storm instead of competing with it, and the FIRST settings-card open
|
|
21
|
+
* usually hits a warm cache. A card opened before the timer fires simply
|
|
22
|
+
* triggers the probe itself (the read path's lazy kick), so the delay never
|
|
23
|
+
* costs correctness. Kept short: a setting visible within seconds of boot.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CATALOG_WARMUP_DELAY_MS = 1500;
|
|
26
|
+
/** Services required before the harness can register. */
|
|
27
|
+
export declare const inject: string[];
|
|
28
|
+
/** Plugin config: the sandbox mode fresh delegations default to, plus the live driver. */
|
|
29
|
+
export interface Config {
|
|
30
|
+
/** Codex sandbox policy for `codex exec` / app-server threads; defaults to workspace-write. */
|
|
31
|
+
sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
32
|
+
/**
|
|
33
|
+
* The model every delegation round starts the CLI with (`codex exec -m
|
|
34
|
+
* <model>`, and `-c model=…` for the resident app-server). Absent — the
|
|
35
|
+
* default — passes NO model flag at all: the scoped `config.toml`'s own
|
|
36
|
+
* `model`, or codex's built-in default with none, decides exactly as it did
|
|
37
|
+
* before this key existed. The settings card writes the same key, so a
|
|
38
|
+
* change applies to the next round without a reload.
|
|
39
|
+
*/
|
|
40
|
+
model?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Live driver: keep one resident `codex app-server` process per member and
|
|
43
|
+
* drive turns over the app-server wire (runtime-level interrupt, push-mode
|
|
44
|
+
* mirroring) instead of one `codex exec` process per round. Default off;
|
|
45
|
+
* the exec one-shot stays the fallback whenever the channel cannot come up.
|
|
46
|
+
*/
|
|
47
|
+
live?: boolean;
|
|
48
|
+
/** Idle lifetime of an unused resident runtime before reclaim. */
|
|
49
|
+
liveIdleMs?: number;
|
|
50
|
+
/** @deprecated Accepted for old profiles; live output is always incremental. */
|
|
51
|
+
liveMirrorGranularity?: 'event' | 'token';
|
|
52
|
+
/** Model identifiers the settings card saved before (settings field). */
|
|
53
|
+
recentModels?: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
export declare const Config: z;
|
|
56
|
+
/** The sandbox policy a fresh delegation defaults to. */
|
|
57
|
+
export declare const DEFAULT_SANDBOX: NonNullable<Config['sandbox']>;
|
|
58
|
+
/**
|
|
59
|
+
* Settings namespace for the settings-page card. The Cordis config feeds the
|
|
60
|
+
* composition `base` layer, so a field absent from the user layer inherits the
|
|
61
|
+
* YAML value — the card only ever stores deliberate overrides.
|
|
62
|
+
*/
|
|
63
|
+
export declare const CODEX_SETTINGS_NAMESPACE = "local-agent-codex";
|
|
64
|
+
/**
|
|
65
|
+
* Register the Codex harness into the local-agent registry.
|
|
66
|
+
* @param ctx - plugin context carrying the registry.
|
|
67
|
+
* @param config - plugin config; `sandbox` selects the `codex exec` policy.
|
|
68
|
+
*/
|
|
69
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
70
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex harness instantiation of the local-agent family. Registers the
|
|
3
|
+
* `codex` harness into the core registry: scoped home under the shared homes
|
|
4
|
+
* root (`CODEX_HOME`), device-code login through `codex login --device-auth`
|
|
5
|
+
* (prompt captured from stdout), and rollout-file session records.
|
|
6
|
+
* Delegation mounts through the `codex-local` provider (one-shot
|
|
7
|
+
* `codex exec` under the scoped home); the bundle patch's
|
|
8
|
+
* `tool-subagent-codex-local` row puts the tool on the profile root, so
|
|
9
|
+
* every agent preset can delegate.
|
|
10
|
+
*
|
|
11
|
+
* @module @khorsheed/dsh-local-agent-codex
|
|
12
|
+
*/
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
|
|
15
|
+
import { endpointHost } from '@khorsheed/dsh-local-agent/types';
|
|
16
|
+
import { CodexCliProvider, codexCliVersion } from './codex-cli-provider.js';
|
|
17
|
+
import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
|
|
18
|
+
import { LiveDriverSwitch } from './live-switch.js';
|
|
19
|
+
import { CodexModelBroker } from './model-broker.js';
|
|
20
|
+
import { CodexModelCatalog } from './model-catalog.js';
|
|
21
|
+
import { codexAuthenticated, listCodexSessions } from './records.js';
|
|
22
|
+
import { codexCredentialStamp, codexLogout, provisionCodexConfig, readCodexBaseUrl, readCodexModel, readCodexReasoningEffort } from './provision.js';
|
|
23
|
+
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
24
|
+
export const name = 'local-agent-codex';
|
|
25
|
+
/**
|
|
26
|
+
* Boot delay before the model catalog's eager warmup probe fires. The probe
|
|
27
|
+
* spawns a `codex app-server` — cheap, but not free — so it waits out the
|
|
28
|
+
* boot storm instead of competing with it, and the FIRST settings-card open
|
|
29
|
+
* usually hits a warm cache. A card opened before the timer fires simply
|
|
30
|
+
* triggers the probe itself (the read path's lazy kick), so the delay never
|
|
31
|
+
* costs correctness. Kept short: a setting visible within seconds of boot.
|
|
32
|
+
*/
|
|
33
|
+
export const CATALOG_WARMUP_DELAY_MS = 1_500;
|
|
34
|
+
/** Services required before the harness can register. */
|
|
35
|
+
export const inject = ['localAgent', 'subagents', 'subprocess', 'settings'];
|
|
36
|
+
/**
|
|
37
|
+
* The settings fields (`live`, `liveMirrorGranularity`, `model`,
|
|
38
|
+
* `recentModels`) ride the plugin Config, marked `.volatile()` where the
|
|
39
|
+
* host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
|
|
40
|
+
* there the SettingsForms writes hot-track the running fiber's Volatile
|
|
41
|
+
* references without a remount, and the official one-shot import moves an old
|
|
42
|
+
* settings.yaml section into this row's config. On 0.1.5 the call is absent,
|
|
43
|
+
* the fields stay plain config keys (a semantic superset — the profile row
|
|
44
|
+
* can now preset them too), and the legacy settings namespace below carries
|
|
45
|
+
* the user layer exactly as before. `model` deliberately carries NO default:
|
|
46
|
+
* an unset key must resolve to undefined, which is what keeps the pre-key
|
|
47
|
+
* behavior byte-identical.
|
|
48
|
+
*/
|
|
49
|
+
const SETTINGS_FIELDS = {
|
|
50
|
+
live: z.boolean().default(false),
|
|
51
|
+
liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
|
|
52
|
+
model: z.string(),
|
|
53
|
+
recentModels: z.array(z.string()).default([]),
|
|
54
|
+
};
|
|
55
|
+
/** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
|
|
56
|
+
function volatilize(field) {
|
|
57
|
+
return typeof field.volatile === 'function' ? field.volatile() : field;
|
|
58
|
+
}
|
|
59
|
+
// Bare `z` annotation: `z<Config>` fails schemastery 3.18.4's variance under
|
|
60
|
+
// exactOptionalPropertyTypes (TS2375) and dropping the annotation trips
|
|
61
|
+
// TS2742; the interface stays the apply signature's contract.
|
|
62
|
+
export const Config = z.object({
|
|
63
|
+
sandbox: z.union([
|
|
64
|
+
z.const('read-only'),
|
|
65
|
+
z.const('workspace-write'),
|
|
66
|
+
z.const('danger-full-access'),
|
|
67
|
+
]),
|
|
68
|
+
model: volatilize(SETTINGS_FIELDS.model),
|
|
69
|
+
live: volatilize(SETTINGS_FIELDS.live),
|
|
70
|
+
liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
|
|
71
|
+
liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
|
|
72
|
+
recentModels: volatilize(SETTINGS_FIELDS.recentModels),
|
|
73
|
+
});
|
|
74
|
+
/** The sandbox policy a fresh delegation defaults to. */
|
|
75
|
+
export const DEFAULT_SANDBOX = 'workspace-write';
|
|
76
|
+
/**
|
|
77
|
+
* Settings namespace for the settings-page card. The Cordis config feeds the
|
|
78
|
+
* composition `base` layer, so a field absent from the user layer inherits the
|
|
79
|
+
* YAML value — the card only ever stores deliberate overrides.
|
|
80
|
+
*/
|
|
81
|
+
export const CODEX_SETTINGS_NAMESPACE = 'local-agent-codex';
|
|
82
|
+
/**
|
|
83
|
+
* The card's 0.1.5 schema; field defaults are the innermost layer below
|
|
84
|
+
* `base`. Shared with the Config schema's settings fields above.
|
|
85
|
+
*/
|
|
86
|
+
const CODEX_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
|
|
87
|
+
/**
|
|
88
|
+
* Register the Codex harness into the local-agent registry.
|
|
89
|
+
* @param ctx - plugin context carrying the registry.
|
|
90
|
+
* @param config - plugin config; `sandbox` selects the `codex exec` policy.
|
|
91
|
+
*/
|
|
92
|
+
export function apply(ctx, config) {
|
|
93
|
+
ctx.effect(() => {
|
|
94
|
+
const sandbox = config.sandbox ?? DEFAULT_SANDBOX;
|
|
95
|
+
const homeDir = ctx.localAgent.homeDir('codex');
|
|
96
|
+
// Codex's default credential store on macOS is the keychain; pin
|
|
97
|
+
// file-based storage so login lands in the scoped home's auth.json.
|
|
98
|
+
void provisionCodexConfig(homeDir).catch((error) => {
|
|
99
|
+
ctx.logger.warn(`local-agent-codex: config.toml provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
});
|
|
101
|
+
// The live driver is settings-driven: the settings card's toggle (user
|
|
102
|
+
// layer over the YAML composition base) swaps driver generations without
|
|
103
|
+
// a reload. Toggling OFF drains the retiring generation — new rounds fall
|
|
104
|
+
// back to exec, in-flight rounds finish on their runtime, idle runtimes
|
|
105
|
+
// are reclaimed at once. Legacy granularity settings are accepted but ignored.
|
|
106
|
+
// The settings face hides the two host lines: 0.1.5 serves the legacy
|
|
107
|
+
// namespace scope (register + base); rc.1 reads the row config's volatile
|
|
108
|
+
// fields and re-reads on `settings/document-updated`. Both hot-apply the
|
|
109
|
+
// card's writes without a reload.
|
|
110
|
+
const readSettings = () => {
|
|
111
|
+
const model = vol(config.model);
|
|
112
|
+
const recentModels = vol(config.recentModels);
|
|
113
|
+
return {
|
|
114
|
+
live: vol(config.live) ?? false,
|
|
115
|
+
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
|
|
116
|
+
...model === undefined ? {} : { model },
|
|
117
|
+
...recentModels === undefined ? {} : { recentModels },
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const legacyBase = {};
|
|
121
|
+
for (const field of ['live', 'liveMirrorGranularity', 'model']) {
|
|
122
|
+
const value = vol(config[field]);
|
|
123
|
+
if (value !== undefined)
|
|
124
|
+
legacyBase[field] = value;
|
|
125
|
+
}
|
|
126
|
+
const face = settingsFace(ctx, {
|
|
127
|
+
namespace: CODEX_SETTINGS_NAMESPACE,
|
|
128
|
+
legacySchema: CODEX_SETTINGS_SCHEMA,
|
|
129
|
+
legacyBase,
|
|
130
|
+
read: readSettings,
|
|
131
|
+
});
|
|
132
|
+
// The model is read PER ROUND, not captured at apply: the settings card
|
|
133
|
+
// writes the same namespace field, so a change has to reach the next
|
|
134
|
+
// delegation without a plugin reload — exactly like the live toggle.
|
|
135
|
+
// Blank is not a model: a whitespace-only value means "unset", which is
|
|
136
|
+
// the pre-key argv.
|
|
137
|
+
const resolveModel = () => {
|
|
138
|
+
const model = face.get().model?.trim();
|
|
139
|
+
return model === undefined || model === '' ? undefined : model;
|
|
140
|
+
};
|
|
141
|
+
// The member-level model surface: the composer picker's session-level
|
|
142
|
+
// overrides (in-memory, deliberately lost on a host restart), shared by
|
|
143
|
+
// reference with the broker (which writes them), the exec provider (which
|
|
144
|
+
// consults them per round), and the live driver (which binds them at
|
|
145
|
+
// runtime spawn). A switch on a member with a live runtime bound to a
|
|
146
|
+
// different model retires that runtime — the next round respawns onto the
|
|
147
|
+
// new model while the codex thread itself resumes.
|
|
148
|
+
const memberModelOverrides = new Map();
|
|
149
|
+
const modelCatalog = new CodexModelCatalog({
|
|
150
|
+
spawn: spec => ctx.subprocess.spawn(spec),
|
|
151
|
+
warn: message => ctx.logger.warn(message),
|
|
152
|
+
});
|
|
153
|
+
// Eager warmup: kick one background probe of the DEFAULT scoped home
|
|
154
|
+
// shortly after apply completes (never blocking it — read() is the
|
|
155
|
+
// synchronous cache read whose only side effect is the background
|
|
156
|
+
// re-probe), so the first settings-card open usually serves the warm
|
|
157
|
+
// cache instead of reading an empty one and waiting a TTL window. Named
|
|
158
|
+
// scopes keep the lazy path: their first modelInfo read probes on demand.
|
|
159
|
+
// The timer is unref'd (it must not hold the host process open) and
|
|
160
|
+
// cleared on dispose (an unloaded plugin spawns nothing).
|
|
161
|
+
const warmup = setTimeout(() => {
|
|
162
|
+
void modelCatalog.read(homeDir);
|
|
163
|
+
}, CATALOG_WARMUP_DELAY_MS);
|
|
164
|
+
warmup.unref();
|
|
165
|
+
const liveSwitch = new LiveDriverSwitch(ctx, face, {
|
|
166
|
+
sandbox,
|
|
167
|
+
model: childSessionId => memberModelOverrides.get(childSessionId) ?? resolveModel(),
|
|
168
|
+
...config.liveIdleMs === undefined ? {} : { liveIdleMs: config.liveIdleMs },
|
|
169
|
+
});
|
|
170
|
+
const modelBroker = new CodexModelBroker({
|
|
171
|
+
ctx,
|
|
172
|
+
settingsModel: resolveModel,
|
|
173
|
+
recentModels: () => face.get().recentModels ?? [],
|
|
174
|
+
homeDir: childSessionId => ctx.localAgent.homeDir('codex', childSessionId === undefined ? undefined : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
|
|
175
|
+
cwd: childSessionId => childSessionId === undefined ? undefined : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.cwd,
|
|
176
|
+
directory: (home, cwd) => modelCatalog.directory(home, cwd),
|
|
177
|
+
refreshDirectory: (home, cwd) => modelCatalog.refresh(home, cwd),
|
|
178
|
+
followDirectory: (home, cwd, signal) => modelCatalog.follow(home, signal, cwd),
|
|
179
|
+
live: () => face.get().live,
|
|
180
|
+
overrides: memberModelOverrides,
|
|
181
|
+
liveBoundModel: childSessionId => liveSwitch.boundModel(childSessionId),
|
|
182
|
+
retireRuntime: childSessionId => liveSwitch.retireRuntime(childSessionId),
|
|
183
|
+
// The account catalog probe is LAZY per read: read() serves the cache
|
|
184
|
+
// and kicks a background probe when stale; the apply above warms the
|
|
185
|
+
// default scope's cache so the first card open usually hits it.
|
|
186
|
+
catalog: (scopedHome, cwd) => modelCatalog.read(scopedHome, cwd),
|
|
187
|
+
// The account's built-in default slug (the probe's isDefault entry) —
|
|
188
|
+
// the layer that names the CLI's compiled default, read off the same
|
|
189
|
+
// cache.
|
|
190
|
+
catalogDefault: (scopedHome, cwd) => modelCatalog.readDefault(scopedHome, cwd),
|
|
191
|
+
});
|
|
192
|
+
const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, sandbox, liveSwitch.resolve, resolveModel, childSessionId => memberModelOverrides.get(childSessionId)));
|
|
193
|
+
const disposeHarness = ctx.localAgent.register({
|
|
194
|
+
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
195
|
+
const driver = liveSwitch.resolve(binding.childSessionId);
|
|
196
|
+
if (driver === undefined || driver.disabled)
|
|
197
|
+
throw new Error('Enable the native live harness before preparing a coordinator');
|
|
198
|
+
await driver.prepare({ cwd: binding.cwd, homeDir: ctx.localAgent.homeDir('codex', binding.scope), childSession,
|
|
199
|
+
parentSessionId: binding.parentSessionId, configuration: configuration.resolved, }, signal);
|
|
200
|
+
},
|
|
201
|
+
name: 'codex',
|
|
202
|
+
displayName: 'Codex',
|
|
203
|
+
homeEnvVar: 'CODEX_HOME',
|
|
204
|
+
delegationProvider: 'codex-local',
|
|
205
|
+
modelBroker,
|
|
206
|
+
// A PTY run of plain `codex login` starts a localhost callback server
|
|
207
|
+
// (no device code, no paste) and opens the browser itself — the
|
|
208
|
+
// first-class flow. `--device-auth` remains the documented headless
|
|
209
|
+
// fallback for remote terminals.
|
|
210
|
+
login: { pty: { command: 'codex', args: ['login'] } },
|
|
211
|
+
records: { listSessions: homeDir => listCodexSessions(homeDir) },
|
|
212
|
+
// A NAMED scope's directory is provisioned through this hook when the
|
|
213
|
+
// registry materializes it — the same `config.toml` storage pin the
|
|
214
|
+
// default scope gets from the apply above, so a scope's first login
|
|
215
|
+
// lands in its own `auth.json` instead of the macOS keychain.
|
|
216
|
+
provision: scopedHome => provisionCodexConfig(scopedHome).then(() => { }),
|
|
217
|
+
isAuthenticated: codexAuthenticated,
|
|
218
|
+
credentialStamp: codexCredentialStamp,
|
|
219
|
+
logout: codexLogout,
|
|
220
|
+
// The eval snapshot: the sandbox policy comes from the plugin config
|
|
221
|
+
// (it rides every spawn argv); effort and endpoint are read live from
|
|
222
|
+
// the scoped config, which codex itself reads — a person-edited value
|
|
223
|
+
// is exactly what the rounds run with. The model follows the family's
|
|
224
|
+
// fixed order: the plugin config key (it overrides the file on every
|
|
225
|
+
// argv) before the scoped config's own `model`, absent when neither
|
|
226
|
+
// names one.
|
|
227
|
+
// The directory is a PARAMETER, not the apply-time capture: a status
|
|
228
|
+
// read (or an evaluation snapshot) of a named scope must report the
|
|
229
|
+
// config that scope's rounds would run with, which is the config in
|
|
230
|
+
// that scope's own directory.
|
|
231
|
+
effectiveSettings: async (scopedHome) => {
|
|
232
|
+
const [reasoningEffort, baseUrl, scopedModel, cliVersion] = await Promise.all([
|
|
233
|
+
readCodexReasoningEffort(scopedHome).catch(() => undefined),
|
|
234
|
+
readCodexBaseUrl(scopedHome).catch(() => undefined),
|
|
235
|
+
readCodexModel(scopedHome).catch(() => undefined),
|
|
236
|
+
codexCliVersion(ctx, scopedHome).catch(() => undefined),
|
|
237
|
+
]);
|
|
238
|
+
const model = resolveModel() ?? scopedModel;
|
|
239
|
+
const baseUrlHost = baseUrl !== undefined ? endpointHost(baseUrl) : undefined;
|
|
240
|
+
return {
|
|
241
|
+
drive: face.get().live ? 'live' : 'exec',
|
|
242
|
+
sandbox,
|
|
243
|
+
...reasoningEffort !== undefined ? { reasoningEffort } : {},
|
|
244
|
+
baseUrlSet: baseUrl !== undefined,
|
|
245
|
+
...baseUrlHost !== undefined ? { baseUrlHost } : {},
|
|
246
|
+
...model !== undefined ? { model } : {},
|
|
247
|
+
...cliVersion !== undefined ? { cliVersion } : {},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
subcommand: (input, invocation) => {
|
|
251
|
+
const [verb] = input.split(/\s+/);
|
|
252
|
+
if (verb === 'sessions') {
|
|
253
|
+
return listCodexSessions(invocation.agent.ctx.localAgent.homeDir('codex'))
|
|
254
|
+
.then(records => ({
|
|
255
|
+
kind: 'success',
|
|
256
|
+
text: records.length === 0
|
|
257
|
+
? 'No codex sessions in the scoped home yet.'
|
|
258
|
+
: records.map(r => `${r.id} ${r.workDir}`).join('\n'),
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
return () => {
|
|
265
|
+
clearTimeout(warmup);
|
|
266
|
+
modelCatalog.dispose();
|
|
267
|
+
disposeProvider();
|
|
268
|
+
disposeHarness();
|
|
269
|
+
liveSwitch.dispose();
|
|
270
|
+
};
|
|
271
|
+
}, 'local-agent-codex: harness');
|
|
272
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-local-agent-codex`.
|
|
3
|
+
* @module @khorsheed/dsh-local-agent-codex/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "local-agent-codex-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - plugin context carrying the invariant registry.
|
|
13
|
+
* @returns the installed registration's disposer.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-local-agent-codex`.
|
|
3
|
+
* @module @khorsheed/dsh-local-agent-codex/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@khorsheed/dsh-local-agent-codex';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'local-agent-codex-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* The codex harness definition is this bundle's contract with the core
|
|
12
|
+
* registry: whenever a `codex` harness registers, its scoped-home variable
|
|
13
|
+
* must be the one this bundle pins.
|
|
14
|
+
*/
|
|
15
|
+
const install = (ctx, fail) => {
|
|
16
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
17
|
+
if (eventName !== 'localAgent/harness-added')
|
|
18
|
+
return;
|
|
19
|
+
const harness = args[0];
|
|
20
|
+
if (harness.name !== 'codex')
|
|
21
|
+
return;
|
|
22
|
+
if (harness.homeEnvVar !== 'CODEX_HOME') {
|
|
23
|
+
fail('codex harness registered with a home env other than the local-agent-codex contract');
|
|
24
|
+
}
|
|
25
|
+
}, { global: true });
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Register this package's invariant companion.
|
|
29
|
+
* @param ctx - plugin context carrying the invariant registry.
|
|
30
|
+
* @returns the installed registration's disposer.
|
|
31
|
+
*/
|
|
32
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
33
|
+
/* jscpd:ignore-end */
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The codex provider's live driver: one resident `codex app-server --stdio`
|
|
3
|
+
* process per member (child session), driven over the vendor's experimental
|
|
4
|
+
* JSON-RPC app-server wire (protocol-probed against codex-cli 0.144.0's
|
|
5
|
+
* `app-server generate-json-schema`; the harness `subagent-codex` package is
|
|
6
|
+
* the reference implementation). A delegation round is a `turn/start` on the
|
|
7
|
+
* member's resident thread instead of a fresh `codex exec` process, which buys
|
|
8
|
+
* what the process boundary could not: runtime-level graceful interrupt
|
|
9
|
+
* (`turn/interrupt`; the process survives and the thread stays continuable)
|
|
10
|
+
* and push-mode mirroring (`item/completed` + delta notifications instead of
|
|
11
|
+
* folding a polled stdout stream).
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle mirrors the dsh live driver's discipline (M1): lazy spawn with an
|
|
14
|
+
* initialize handshake, one in-flight spawn per member, idle-timeout reclaim
|
|
15
|
+
* (stdin EOF → grace → SIGTERM ladder — the app-server wire has no shutdown
|
|
16
|
+
* method), crash re-spawn with `thread/resume` of the recorded thread id, a
|
|
17
|
+
* channel breaker with cooldown, and `disposeAll` on plugin unload. The wire
|
|
18
|
+
* adapter is self-contained (no new dependency): line framing with a
|
|
19
|
+
* StringDecoder for split UTF-8 tails, request correlation, and unattended
|
|
20
|
+
* auto-answers for the server→client approval requests (the proposal's
|
|
21
|
+
* default permission policy: answer exactly as the unattended exec mode does).
|
|
22
|
+
* @module @khorsheed/dsh-local-agent-codex/live-driver
|
|
23
|
+
*/
|
|
24
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
25
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
26
|
+
import { type SubagentRun, type SubagentStartRequest } from '@deepseek-ai/dsh-subagent';
|
|
27
|
+
import type { Config } from './index.ts';
|
|
28
|
+
import type { LocalAgentResolvedConfiguration } from '@khorsheed/dsh-local-agent/types';
|
|
29
|
+
import { type CodexTranscriptLine } from './codex-cli-provider.ts';
|
|
30
|
+
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
31
|
+
export declare const DEFAULT_LIVE_IDLE_MS: number;
|
|
32
|
+
/** Default timeout for a wire request acknowledgement. */
|
|
33
|
+
export declare const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 30000;
|
|
34
|
+
/** Default timeout for the app-server handshake (a cold boot can be slow). */
|
|
35
|
+
export declare const DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS = 60000;
|
|
36
|
+
/** Bounded wait for an interrupted turn to converge while a run disposes. */
|
|
37
|
+
export declare const DEFAULT_LIVE_DISPOSE_CONVERGE_MS = 5000;
|
|
38
|
+
/** How long a tripped channel breaker stays on the exec fallback. */
|
|
39
|
+
export declare const DEFAULT_LIVE_CHANNEL_RETRY_MS: number;
|
|
40
|
+
/** Default minimum interval between one streaming item's snapshot messages. */
|
|
41
|
+
export declare const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 50;
|
|
42
|
+
/** Default minimum text growth between one streaming item's snapshot messages. */
|
|
43
|
+
export declare const DEFAULT_SNAPSHOT_MIN_CHARS = 0;
|
|
44
|
+
/**
|
|
45
|
+
* The app-server channel could not come up (spawn failure or handshake
|
|
46
|
+
* timeout/mismatch). The provider catches exactly this and falls back to the
|
|
47
|
+
* exec one-shot; any other error is a real round failure.
|
|
48
|
+
*/
|
|
49
|
+
export declare class LiveChannelUnavailableError extends Error {
|
|
50
|
+
constructor(message: string);
|
|
51
|
+
}
|
|
52
|
+
/** Internal timeouts; tests inject small values. */
|
|
53
|
+
export interface CodexLiveDriverTimeouts {
|
|
54
|
+
readonly initializeMs: number;
|
|
55
|
+
readonly requestMs: number;
|
|
56
|
+
readonly convergeMs: number;
|
|
57
|
+
readonly channelRetryMs: number;
|
|
58
|
+
}
|
|
59
|
+
/** How much of the live event stream crosses into the child session. */
|
|
60
|
+
export type CodexLiveMirrorGranularity = 'event' | 'token';
|
|
61
|
+
/** Fully resolved inputs for one live round. */
|
|
62
|
+
export interface CodexLiveRoundSpec {
|
|
63
|
+
/** Parent Session workspace; also the runtime process cwd. */
|
|
64
|
+
readonly cwd: string;
|
|
65
|
+
/** The codex harness's scoped home, injected as the runtime's `CODEX_HOME`. */
|
|
66
|
+
readonly homeDir: string;
|
|
67
|
+
/** dsh subagent session recording this delegation (always live on this path). */
|
|
68
|
+
readonly childSession: Session;
|
|
69
|
+
/** The delegating session (member-channel registration). */
|
|
70
|
+
readonly parentSessionId: string;
|
|
71
|
+
/**
|
|
72
|
+
* Resume round: continue the thread recorded as `cliSessionId`, appending
|
|
73
|
+
* under this turn number. Also the reattach path after a runtime crash.
|
|
74
|
+
*/
|
|
75
|
+
readonly resume?: {
|
|
76
|
+
readonly cliSessionId: string;
|
|
77
|
+
readonly turn: number;
|
|
78
|
+
} | undefined;
|
|
79
|
+
/** Fresh round: called with the server-assigned thread id (delegation record). */
|
|
80
|
+
readonly onThreadId?: ((threadId: string) => void) | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* The member's START model — the delegation's own requested model, or the
|
|
83
|
+
* composer's session-level override, as resolved by the provider. When it
|
|
84
|
+
* names one, a resident runtime bound to a DIFFERENT model is retired
|
|
85
|
+
* before the spawn so the round respawns onto it (the same CLI session
|
|
86
|
+
* resumes — the rollout carries over). Absent means the spawn resolver
|
|
87
|
+
* decides (override → recorded → settings).
|
|
88
|
+
*/
|
|
89
|
+
readonly startModel?: string | undefined;
|
|
90
|
+
readonly configuration?: LocalAgentResolvedConfiguration;
|
|
91
|
+
}
|
|
92
|
+
type JsonObject = Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* Auth-shaped failure signatures in codex's error output: the endpoint
|
|
95
|
+
* rejected the credential. Mirrors the exec path's detector
|
|
96
|
+
* (CODEX_AUTH_FAILURE in codex-cli-provider.ts); the live path matches
|
|
97
|
+
* against turn-failure messages, which is where a mid-run 401 surfaces when
|
|
98
|
+
* the process does not exit.
|
|
99
|
+
*/
|
|
100
|
+
export declare const CODEX_LIVE_AUTH_FAILURE: RegExp;
|
|
101
|
+
/**
|
|
102
|
+
* Map one app-server `item/completed` ThreadItem onto the shared transcript
|
|
103
|
+
* line shape — the live transport's entry into the SAME fold the exec NDJSON
|
|
104
|
+
* parser owns (`foldCodexStreamLine`); both produce `CodexTranscriptLine`s
|
|
105
|
+
* and append through `appendCodexTranscriptLine`, so the two drivers' final
|
|
106
|
+
* child transcripts cannot drift.
|
|
107
|
+
*/
|
|
108
|
+
export declare function codexAppServerItemToLine(item: JsonObject): CodexTranscriptLine | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* The codex live driver owns every resident app-server runtime of this
|
|
111
|
+
* provider generation. Created only when `live: true`; disposed with the
|
|
112
|
+
* plugin.
|
|
113
|
+
*/
|
|
114
|
+
export declare class CodexLiveDriver {
|
|
115
|
+
private readonly ctx;
|
|
116
|
+
private readonly config;
|
|
117
|
+
private readonly timeouts;
|
|
118
|
+
private readonly runtimes;
|
|
119
|
+
/** In-flight spawns by member: concurrent rounds share one, disposeAll waits them out. */
|
|
120
|
+
private readonly ensuring;
|
|
121
|
+
private readonly idleTimers;
|
|
122
|
+
/** Per-member round serialization (the resume lock covers resume-vs-resume only). */
|
|
123
|
+
private readonly roundChains;
|
|
124
|
+
private channelBrokenAt;
|
|
125
|
+
private disposed;
|
|
126
|
+
/**
|
|
127
|
+
* Set by drain() (a settings-driven generation handoff): new rounds are
|
|
128
|
+
* refused so the provider falls back to exec, while in-flight rounds finish
|
|
129
|
+
* on their runtime undisturbed. Unlike `disposed`, the driver still serves
|
|
130
|
+
* what it already accepted.
|
|
131
|
+
*/
|
|
132
|
+
private draining;
|
|
133
|
+
private readonly disposeController;
|
|
134
|
+
constructor(ctx: Context, config: Pick<Config, 'sandbox'> & {
|
|
135
|
+
liveIdleMs?: number;
|
|
136
|
+
liveMirrorGranularity?: CodexLiveMirrorGranularity;
|
|
137
|
+
/** Maximum batching wait for incremental streaming messages. */
|
|
138
|
+
snapshotMinIntervalMs?: number;
|
|
139
|
+
/** @deprecated Character growth no longer gates live publication. */
|
|
140
|
+
snapshotMinChars?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Resolver for the member's configured model, read at each RUNTIME SPAWN
|
|
143
|
+
* (the app-server is where a live round's CLI starts). Receives the
|
|
144
|
+
* member's child session id so the session-level override can outrank
|
|
145
|
+
* the settings value; absent, or resolving to nothing, leaves the spawn
|
|
146
|
+
* argv unchanged.
|
|
147
|
+
*/
|
|
148
|
+
model?: (childSessionId: string) => string | undefined;
|
|
149
|
+
}, timeouts?: CodexLiveDriverTimeouts);
|
|
150
|
+
/** Whether the channel is in its post-failure cooldown (rounds fall back to exec). */
|
|
151
|
+
get disabled(): boolean;
|
|
152
|
+
/** Count of resident runtimes currently registered (zombie accounting in tests). */
|
|
153
|
+
get liveCount(): number;
|
|
154
|
+
private clearIdleTimer;
|
|
155
|
+
private armIdleTimer;
|
|
156
|
+
private reclaim;
|
|
157
|
+
/** Reclaim every runtime (plugin unload); no process survives the profile. */
|
|
158
|
+
disposeAll(): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* True while the member's runtime exists or is being spawned. The settings
|
|
161
|
+
* controller gates a new generation on this: a fresh driver must not serve
|
|
162
|
+
* a member whose retiring generation still hosts the (same) codex thread.
|
|
163
|
+
*/
|
|
164
|
+
hasRuntime(key: string): boolean;
|
|
165
|
+
/** @deprecated Compatibility no-op: live output is always incremental. */
|
|
166
|
+
setLiveMirrorGranularity(_granularity: CodexLiveMirrorGranularity): void;
|
|
167
|
+
/**
|
|
168
|
+
* Drain for a settings-driven generation handoff: refuse new rounds (the
|
|
169
|
+
* provider's catch falls back to exec), let every in-flight round finish on
|
|
170
|
+
* its runtime, then reclaim. Resolves when no runtime or spawn remains.
|
|
171
|
+
* Unlike disposeAll, in-flight work is never interrupted.
|
|
172
|
+
*/
|
|
173
|
+
drain(): Promise<void>;
|
|
174
|
+
/**
|
|
175
|
+
* The model the member's live runtime bound at spawn: the identifier, or
|
|
176
|
+
* undefined when the runtime spawned with no model flag. Null when the
|
|
177
|
+
* member has no live runtime in this generation — the broker's switch check
|
|
178
|
+
* distinguishes "no runtime to retire" from "runtime bound to no model".
|
|
179
|
+
*/
|
|
180
|
+
boundModelOf(key: string): string | undefined | null;
|
|
181
|
+
/**
|
|
182
|
+
* Retire the member's resident runtime so the NEXT round respawns (the CLI
|
|
183
|
+
* session itself carries over via thread/resume). The composer's model
|
|
184
|
+
* switch calls this after storing the new override; the caller guarantees
|
|
185
|
+
* no round is in flight for the member.
|
|
186
|
+
*/
|
|
187
|
+
retireRuntime(key: string): Promise<void>;
|
|
188
|
+
/** The member's runtime, spawning it (once per member at a time) when absent or dead. */
|
|
189
|
+
private ensureRuntime;
|
|
190
|
+
/**
|
|
191
|
+
* Spawn the member's resident app-server and prove the wire with a
|
|
192
|
+
* handshake. Failure trips the channel breaker; a cancellation mid-spawn
|
|
193
|
+
* reclaims the half-started process without touching the breaker.
|
|
194
|
+
*/
|
|
195
|
+
private spawnRuntime;
|
|
196
|
+
private markChannelBroken;
|
|
197
|
+
/**
|
|
198
|
+
* Report an auth-shaped round failure to the family registry's auth-failure
|
|
199
|
+
* mark (the exec path's post-exit 401 detection ported to a process that
|
|
200
|
+
* never exits: the match runs against the settled error instead). Degrades
|
|
201
|
+
* silently on a core predating reportAuthFailure.
|
|
202
|
+
*/
|
|
203
|
+
private reportAuthIfShaped;
|
|
204
|
+
/**
|
|
205
|
+
* Auto-answer the app-server's approval/elicitation requests, matching the
|
|
206
|
+
* unattended exec behavior (and the harness reference implementation's
|
|
207
|
+
* tables): the live driver never grants interactive approval. This is the
|
|
208
|
+
* proposal's default permission policy; the relay hook for a human
|
|
209
|
+
* approval UI is a later increment.
|
|
210
|
+
*/
|
|
211
|
+
private answerServerRequest;
|
|
212
|
+
/**
|
|
213
|
+
* Drive one delegation round on the member's resident app-server. Mirrors
|
|
214
|
+
* the exec run's settlement contract exactly so the facade, the tool, and
|
|
215
|
+
* the projections cannot tell the difference — except `cancel` is a runtime
|
|
216
|
+
* interrupt and the process survives. Cancellation is honored in every
|
|
217
|
+
* window (the M1 acceptance lesson): the abort listener registers before
|
|
218
|
+
* any await, the spawn/handshake races it, and a pre-accept cancel reclaims
|
|
219
|
+
* the fresh runtime instead of letting the turn run unwatched.
|
|
220
|
+
*/
|
|
221
|
+
/** Create/resume the native thread without starting a model turn. */
|
|
222
|
+
prepare(spec: CodexLiveRoundSpec, signal: AbortSignal): Promise<void>;
|
|
223
|
+
private ensureThread;
|
|
224
|
+
startRound(request: SubagentStartRequest, spec: CodexLiveRoundSpec): Promise<SubagentRun>;
|
|
225
|
+
/** The serialized round body. */
|
|
226
|
+
private startRoundLocked;
|
|
227
|
+
}
|
|
228
|
+
export {};
|
|
229
|
+
//# sourceMappingURL=live-driver.d.ts.map
|