@shuind/dsh-codex-harness 0.1.10 → 0.1.12

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.
@@ -1,6 +1,15 @@
1
1
  /** Codex-compatible prompt overlay and core tools for a dsh agent preset. */
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import z from '@deepseek-ai/schemastery';
4
+ import type { LlmCallConfig } from '@deepseek-ai/dsh-llm';
5
+ declare module '@deepseek-ai/dsh-llm' {
6
+ interface LlmCallConfig {
7
+ /** Codex request context capacity override, in tokens. */
8
+ contextWindow?: number;
9
+ /** Provider-facing service tier, for example Responses `priority`. */
10
+ serviceTier?: string;
11
+ }
12
+ }
4
13
  export declare const name = "codex";
5
14
  export declare const inject: string[];
6
15
  /** Configuration for the Codex shell result bridge. */
@@ -13,9 +22,32 @@ export interface Config {
13
22
  writeYieldTimeMs?: number;
14
23
  /** Maximum output retained in one canonical result, in UTF-8 bytes. */
15
24
  maxOutputBytes?: number;
25
+ /** Send GPT Responses requests with the native hosted web_search tool first. */
26
+ hostedWebSearch?: boolean;
27
+ /** Use the provider's /responses/compact endpoint before local compaction. */
28
+ remoteCompact?: boolean;
16
29
  }
17
30
  /** Runtime configuration schema for the Codex tool bridge. */
18
31
  export declare const Config: z<Config>;
32
+ /** Live Codex-only request controls shared by the Web controls and agent layer. */
33
+ export declare const CODEX_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
34
+ export interface CodexSettings {
35
+ /** Use the Responses priority service tier for GPT requests. */
36
+ fast: boolean;
37
+ /** Optional context capacity override, in tokens. */
38
+ contextWindow?: number;
39
+ }
40
+ export declare const CODEX_SETTINGS_SCHEMA: z<CodexSettings>;
41
+ export interface CodexModelProfile {
42
+ id: string;
43
+ input?: string[];
44
+ reasoningEfforts?: false | Record<string, string | null>;
45
+ [key: string]: unknown;
46
+ }
47
+ /** Add Codex defaults without overwriting explicit user capabilities. */
48
+ export declare function enrichCodexModel(model: CodexModelProfile): CodexModelProfile;
49
+ /** Apply the live Codex controls to one agent request without leaking them to other routes. */
50
+ export declare function applyCodexRequestSettings(request: LlmCallConfig, settings: CodexSettings): LlmCallConfig;
19
51
  /** Mount the Codex prompt/tool layer inside one fixed agent preset. */
20
52
  export declare function apply(ctx: Context, config?: Config): void;
21
53
  declare const _default: {
@@ -1,17 +1,141 @@
1
1
  /** Codex-compatible prompt overlay and core tools for a dsh agent preset. */
2
2
  import z from '@deepseek-ai/schemastery';
3
+ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
3
4
  import { defineTool } from '@deepseek-ai/dsh-tools';
5
+ import { CODEX_CONTEXT_MAX } from "./context.js";
4
6
  import { applyPatchHunks, parsePatch } from "./patch.js";
5
7
  import { renderExecResult, runExecCommand, runWriteStdin } from "./exec.js";
8
+ import { hostedWebSearchStream, installHostedWebSearch, remoteCompactStream } from "./remote.js";
6
9
  export const name = 'codex';
7
- export const inject = ['tools', 'systemPrompt', 'shell', 'fs'];
10
+ export const inject = ['tools', 'systemPrompt', 'shell', 'fs', 'llm', 'credentials', 'settings'];
8
11
  /** Runtime configuration schema for the Codex tool bridge. */
9
12
  export const Config = z.object({
10
13
  defaultYieldTimeMs: z.number().step(1).min(0).default(10_000),
11
14
  pollYieldTimeMs: z.number().step(1).min(0).default(5_000),
12
15
  writeYieldTimeMs: z.number().step(1).min(0).default(250),
13
16
  maxOutputBytes: z.number().step(1).min(1).default(64_000),
17
+ hostedWebSearch: z.boolean().default(true),
18
+ remoteCompact: z.boolean().default(true),
14
19
  });
20
+ const LLM_PI_AI_SETTINGS = settingsNamespace('llm-pi-ai');
21
+ /** Live Codex-only request controls shared by the Web controls and agent layer. */
22
+ export const CODEX_SETTINGS_NAMESPACE = settingsNamespace('codex');
23
+ export const CODEX_SETTINGS_SCHEMA = z.object({
24
+ fast: z.boolean().default(false),
25
+ contextWindow: z.number().step(1).min(1).max(CODEX_CONTEXT_MAX),
26
+ });
27
+ const CODEX_SETTINGS_ENTRY = { fast: false };
28
+ const GPT_REASONING_EFFORTS = {
29
+ low: 'low',
30
+ medium: 'medium',
31
+ high: 'high',
32
+ xhigh: 'xhigh',
33
+ max: 'max',
34
+ };
35
+ function codexReasoningEfforts(configured) {
36
+ if (configured === false)
37
+ return false;
38
+ const filtered = configured === undefined
39
+ ? {}
40
+ : Object.fromEntries(Object.entries(configured).filter(([level]) => level !== 'off' && level !== 'minimal'));
41
+ return Object.keys(filtered).length === 0 ? GPT_REASONING_EFFORTS : filtered;
42
+ }
43
+ /** GPT model ids are the only models whose relay capabilities Codex fills in. */
44
+ function isGptModel(id) {
45
+ return /(?:^|\/)(?:gpt|chatgpt)(?:[-_.]|\d|$)/i.test(id);
46
+ }
47
+ /** Add Codex defaults without overwriting explicit user capabilities. */
48
+ export function enrichCodexModel(model) {
49
+ if (!isGptModel(model.id))
50
+ return model;
51
+ const input = model.input === undefined || model.input.length === 0
52
+ ? ['text', 'image']
53
+ : model.input;
54
+ const efforts = codexReasoningEfforts(model.reasoningEfforts);
55
+ if (input === model.input && efforts === model.reasoningEfforts)
56
+ return model;
57
+ return { ...model, input, reasoningEfforts: efforts };
58
+ }
59
+ /** The settings schema keys modelOverrides by id, so its values must not carry an id field. */
60
+ function enrichCodexOverride(id, model) {
61
+ if (!isGptModel(id))
62
+ return model;
63
+ const enriched = enrichCodexModel({ ...model, id });
64
+ const { id: _id, ...withoutId } = enriched;
65
+ const inputMissing = model.input === undefined || model.input.length === 0;
66
+ const nextEfforts = codexReasoningEfforts(model.reasoningEfforts);
67
+ const reasoningChanged = JSON.stringify(nextEfforts) !== JSON.stringify(model.reasoningEfforts);
68
+ if (!inputMissing && !reasoningChanged)
69
+ return model;
70
+ return { ...withoutId, reasoningEfforts: nextEfforts };
71
+ }
72
+ /** Persist only missing GPT capabilities into the user's existing pi-ai model config. */
73
+ async function enrichConfiguredGptModels(ctx) {
74
+ const settings = ctx.get('settings');
75
+ if (settings === undefined)
76
+ return;
77
+ const current = settings.get(LLM_PI_AI_SETTINGS);
78
+ if (current?.providers === undefined)
79
+ return;
80
+ const providers = {};
81
+ let changed = false;
82
+ for (const [provider, profile] of Object.entries(current.providers)) {
83
+ const models = profile.models;
84
+ const overrides = profile.modelOverrides;
85
+ const nextModels = Array.isArray(models) ? models.map(enrichCodexModel) : undefined;
86
+ const nextOverrides = overrides === undefined
87
+ ? undefined
88
+ : Object.fromEntries(Object.entries(overrides).map(([id, model]) => [id, enrichCodexOverride(id, model)]));
89
+ const modelsChanged = nextModels !== undefined && nextModels.some((model, index) => model !== models?.[index]);
90
+ const overridesChanged = nextOverrides !== undefined
91
+ && overrides !== undefined
92
+ && Object.entries(nextOverrides).some(([id, model]) => model !== overrides[id]);
93
+ if (modelsChanged || overridesChanged) {
94
+ changed = true;
95
+ providers[provider] = {
96
+ ...modelsChanged ? { models: nextModels } : {},
97
+ ...overridesChanged ? { modelOverrides: nextOverrides } : {},
98
+ };
99
+ }
100
+ }
101
+ if (changed)
102
+ await settings.update(LLM_PI_AI_SETTINGS, { providers });
103
+ }
104
+ /** Keep newly edited GPT model entries enriched while Codex mode is mounted. */
105
+ function watchConfiguredGptModels(ctx) {
106
+ let tail = Promise.resolve();
107
+ const schedule = () => {
108
+ tail = tail.then(() => enrichConfiguredGptModels(ctx)).catch(error => {
109
+ ctx.logger.warn('codex: could not enrich configured GPT model capabilities');
110
+ ctx.logger.warn(error);
111
+ });
112
+ };
113
+ ctx.on('settings/document-updated', (ns) => {
114
+ if (ns === LLM_PI_AI_SETTINGS)
115
+ schedule();
116
+ });
117
+ schedule();
118
+ }
119
+ /** Install the optional settings source used by the request waterfall. */
120
+ function installCodexSettings(ctx) {
121
+ let source = () => CODEX_SETTINGS_ENTRY;
122
+ installSettingsSection(ctx, CODEX_SETTINGS_NAMESPACE, CODEX_SETTINGS_SCHEMA, CODEX_SETTINGS_ENTRY, {
123
+ setSource: (current) => { source = current; },
124
+ onChange: () => { },
125
+ });
126
+ return { current: () => source() };
127
+ }
128
+ /** Apply the live Codex controls to one agent request without leaking them to other routes. */
129
+ export function applyCodexRequestSettings(request, settings) {
130
+ const { contextWindow: _inheritedContextWindow, serviceTier: _inheritedServiceTier, ...withoutCodexControls } = request;
131
+ if (!isGptModel(request.model))
132
+ return withoutCodexControls;
133
+ return {
134
+ ...withoutCodexControls,
135
+ ...settings.contextWindow === undefined ? {} : { contextWindow: settings.contextWindow },
136
+ ...settings.fast ? { serviceTier: 'priority' } : {},
137
+ };
138
+ }
15
139
  const CODEX_BASE_PROMPT = String.raw `You are Codex, based on {{model}}. You are running as a coding agent in dsh Web on a user's computer.
16
140
 
17
141
  ## General
@@ -308,10 +432,41 @@ export function apply(ctx, config = {}) {
308
432
  pollYieldTimeMs: config.pollYieldTimeMs ?? 5_000,
309
433
  writeYieldTimeMs: config.writeYieldTimeMs ?? 250,
310
434
  maxOutputBytes: config.maxOutputBytes ?? 64_000,
435
+ hostedWebSearch: config.hostedWebSearch ?? true,
436
+ remoteCompact: config.remoteCompact ?? true,
311
437
  };
312
438
  if (ctx.fs.sandboxMode !== undefined && ctx.get('sandboxPolicy') === undefined) {
313
439
  throw new Error('codex: a sandboxing filesystem requires ctx.sandboxPolicy');
314
440
  }
441
+ // The generic pi-ai plugin remains the owner of the user's configured
442
+ // provider routes. Codex adds only scoped transport behavior and capability
443
+ // metadata; failed remote operations continue through the generic path.
444
+ watchConfiguredGptModels(ctx);
445
+ const codexSettings = installCodexSettings(ctx);
446
+ // Keep these controls in the request config rather than mutating provider
447
+ // settings. That makes a change apply to the next step without rebuilding
448
+ // the user's model catalog, while the LLM service still freezes it per call.
449
+ ctx.on('agent/request', async (_payload, next) => {
450
+ const request = await next();
451
+ return applyCodexRequestSettings(request, codexSettings.current());
452
+ });
453
+ if ((resolved.hostedWebSearch || resolved.remoteCompact) && typeof ctx.effect === 'function') {
454
+ const disposeHostedWebSearch = installHostedWebSearch();
455
+ ctx.effect(() => disposeHostedWebSearch, 'codex: Responses transport wrapper');
456
+ }
457
+ if (resolved.hostedWebSearch || resolved.remoteCompact) {
458
+ ctx.on('llm/stream', ((options, next) => {
459
+ if (resolved.remoteCompact && options.purpose === 'compaction' && isGptModel(options.model)) {
460
+ return remoteCompactStream(ctx, options, next);
461
+ }
462
+ if ((resolved.hostedWebSearch || resolved.remoteCompact)
463
+ && options.purpose === undefined
464
+ && isGptModel(options.model)) {
465
+ return hostedWebSearchStream(next);
466
+ }
467
+ return next();
468
+ }));
469
+ }
315
470
  ctx.systemPrompt.section({ name: 'codex:base', order: 10, text: CODEX_BASE_PROMPT });
316
471
  registerExecTools(ctx, resolved);
317
472
  registerPatchTool(ctx);
@@ -1,20 +1,20 @@
1
1
  /** Parser and line-oriented applicator for Codex's `apply_patch` language. */
2
2
  /** The grammar sent to providers that support OpenAI custom grammar tools. */
3
- export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch hunk+ end_patch
4
- begin_patch: "*** Begin Patch" LF
5
- end_patch: "*** End Patch" LF?
6
- hunk: add_hunk | delete_hunk | update_hunk
7
- add_hunk: "*** Add File: " filename LF add_line+
8
- delete_hunk: "*** Delete File: " filename LF
9
- update_hunk: "*** Update File: " filename LF change_move? change?
10
- filename: /(.+)/
11
- add_line: "+" /(.*)/ LF -> line
12
- change_move: "*** Move to: " filename LF
13
- change: (change_context | change_line)+ eof_line?
14
- change_context: ("@@" | "@@ " /(.+)/) LF
15
- change_line: ("+" | "-" | " ") /(.*)/ LF
16
- eof_line: "*** End of File" LF
17
- %import common.LF
3
+ export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch hunk+ end_patch
4
+ begin_patch: "*** Begin Patch" LF
5
+ end_patch: "*** End Patch" LF?
6
+ hunk: add_hunk | delete_hunk | update_hunk
7
+ add_hunk: "*** Add File: " filename LF add_line+
8
+ delete_hunk: "*** Delete File: " filename LF
9
+ update_hunk: "*** Update File: " filename LF change_move? change?
10
+ filename: /(.+)/
11
+ add_line: "+" /(.*)/ LF -> line
12
+ change_move: "*** Move to: " filename LF
13
+ change: (change_context | change_line)+ eof_line?
14
+ change_context: ("@@" | "@@ " /(.+)/) LF
15
+ change_line: ("+" | "-" | " ") /(.*)/ LF
16
+ eof_line: "*** End of File" LF
17
+ %import common.LF
18
18
  `;
19
19
  function invalid(message) {
20
20
  throw new Error(`apply_patch: ${message}`);
@@ -0,0 +1,21 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
3
+ export interface RemoteCodexConfig {
4
+ hostedWebSearch: boolean;
5
+ remoteCompact: boolean;
6
+ }
7
+ interface JsonObject {
8
+ [key: string]: unknown;
9
+ }
10
+ /** Convert a generic DSH Responses tool list to the hosted Codex variant. */
11
+ export declare function addHostedWebSearch(body: JsonObject): JsonObject;
12
+ /** Replace the text placeholder written by dsh-compaction-basic with the native item. */
13
+ export declare function replaceRemoteCompactions(body: JsonObject): JsonObject;
14
+ /** Enable the transport patch for this Codex plugin scope only. */
15
+ export declare function installHostedWebSearch(): () => void;
16
+ /** Iterate an existing DSH stream with the hosted-request context installed. */
17
+ export declare function hostedWebSearchStream(next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>;
18
+ /** Remote-first compaction waterfall with the existing DSH path as fallback. */
19
+ export declare function remoteCompactStream(ctx: Context, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>;
20
+ export {};
21
+ //# sourceMappingURL=remote.d.ts.map
@@ -0,0 +1,324 @@
1
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ const SETTINGS_NAMESPACE = 'llm-pi-ai';
4
+ const REMOTE_COMPACTION_OPEN = '<codex-remote-compaction>';
5
+ const REMOTE_COMPACTION_CLOSE = '</codex-remote-compaction>';
6
+ const HOSTED_REQUESTS = new AsyncLocalStorage();
7
+ let hostedPatchUsers = 0;
8
+ let hostedPatchRestore;
9
+ function isGptModel(model) {
10
+ return typeof model === 'string' && /(?:^|\/)(?:gpt|chatgpt)(?:[-_.]|\d|$)/i.test(model);
11
+ }
12
+ function settingsOf(ctx) {
13
+ const provider = ctx.get('settings');
14
+ return provider?.get?.(SETTINGS_NAMESPACE);
15
+ }
16
+ function profileOf(ctx, provider) {
17
+ return settingsOf(ctx)?.providers?.[provider];
18
+ }
19
+ function supportsResponses(profile, provider) {
20
+ return profile?.api === 'openai-responses'
21
+ || profile?.api === 'openai-codex-responses'
22
+ || (profile?.api === undefined && provider === 'openai');
23
+ }
24
+ function responsesEndpoint(baseURL, suffix) {
25
+ return `${baseURL.replace(/\/+$/, '')}/${suffix}`;
26
+ }
27
+ function hasHeader(headers, name) {
28
+ return Object.keys(headers ?? {}).some(key => key.toLowerCase() === name.toLowerCase());
29
+ }
30
+ async function apiHeaders(ctx, profile) {
31
+ const headers = { ...profile.headers, 'content-type': 'application/json' };
32
+ if (profile.apiKeyEnv !== undefined && !hasHeader(headers, 'authorization')) {
33
+ const credentials = ctx.get('credentials');
34
+ const resolved = await credentials?.resolve(credentialRef(profile.apiKeyEnv));
35
+ if (resolved?.value === undefined)
36
+ return undefined;
37
+ headers.authorization = `Bearer ${resolved.value}`;
38
+ }
39
+ return headers;
40
+ }
41
+ function isResponsesRequest(url) {
42
+ try {
43
+ const path = new URL(url).pathname.replace(/\/+$/, '');
44
+ return path.endsWith('/responses');
45
+ }
46
+ catch {
47
+ return url.replace(/\/+$/, '').endsWith('/responses');
48
+ }
49
+ }
50
+ function isWebSearchTool(tool) {
51
+ if (typeof tool !== 'object' || tool === null)
52
+ return false;
53
+ const value = tool;
54
+ if (value.name === 'web_search')
55
+ return true;
56
+ const fn = value.function;
57
+ return typeof fn === 'object' && fn !== null && fn.name === 'web_search';
58
+ }
59
+ function hasWebSearchTool(body) {
60
+ return Array.isArray(body.tools) && body.tools.some(isWebSearchTool);
61
+ }
62
+ /** Convert a generic DSH Responses tool list to the hosted Codex variant. */
63
+ export function addHostedWebSearch(body) {
64
+ const tools = Array.isArray(body.tools) ? body.tools.filter(tool => !isWebSearchTool(tool)) : [];
65
+ tools.push({ type: 'web_search', external_web_access: true });
66
+ return { ...replaceRemoteCompactions(body), tools };
67
+ }
68
+ /** Replace the text placeholder written by dsh-compaction-basic with the native item. */
69
+ export function replaceRemoteCompactions(body) {
70
+ if (!Array.isArray(body.input))
71
+ return body;
72
+ const input = body.input.map(item => {
73
+ if (typeof item !== 'object' || item === null)
74
+ return item;
75
+ const value = item;
76
+ const content = Array.isArray(value.content) ? value.content : [];
77
+ const marker = content.find(part => (typeof part === 'object'
78
+ && part !== null
79
+ && typeof part.text === 'string'
80
+ && String(part.text).includes(REMOTE_COMPACTION_OPEN)));
81
+ if (marker === undefined)
82
+ return item;
83
+ const text = String(marker.text);
84
+ const start = text.indexOf(REMOTE_COMPACTION_OPEN) + REMOTE_COMPACTION_OPEN.length;
85
+ const end = text.indexOf(REMOTE_COMPACTION_CLOSE, start);
86
+ if (end < start)
87
+ return item;
88
+ return {
89
+ type: 'compaction',
90
+ encrypted_content: text.slice(start, end),
91
+ };
92
+ });
93
+ return { ...body, input };
94
+ }
95
+ function hasRemoteCompaction(body) {
96
+ if (!Array.isArray(body.input))
97
+ return false;
98
+ return body.input.some(item => {
99
+ if (typeof item !== 'object' || item === null)
100
+ return false;
101
+ const content = item.content;
102
+ return Array.isArray(content) && content.some(part => (typeof part === 'object'
103
+ && part !== null
104
+ && typeof part.text === 'string'
105
+ && String(part.text).includes(REMOTE_COMPACTION_OPEN)));
106
+ });
107
+ }
108
+ function canPatchBody(body) {
109
+ return isGptModel(body.model)
110
+ && (hasWebSearchTool(body) || hasRemoteCompaction(body));
111
+ }
112
+ /**
113
+ * Install a scoped fetch shim for pi-ai's already-built Responses request.
114
+ * The generic DSH adapter remains the owner of auth, streaming, replay, and
115
+ * attachments; this shim changes only the hosted-tool portion of the wire body.
116
+ */
117
+ function installGlobalHostedWebSearchPatch() {
118
+ const original = globalThis.fetch;
119
+ const patched = async (input, init) => {
120
+ if (!HOSTED_REQUESTS.getStore())
121
+ return original(input, init);
122
+ const request = new Request(input, init);
123
+ if (!isResponsesRequest(request.url))
124
+ return original(input, init);
125
+ let body;
126
+ try {
127
+ body = JSON.parse(await request.clone().text());
128
+ }
129
+ catch {
130
+ return original(input, init);
131
+ }
132
+ if (!canPatchBody(body))
133
+ return original(input, init);
134
+ const fallbackRequest = request.clone();
135
+ const headers = new Headers(request.headers);
136
+ headers.delete('content-length');
137
+ const hostedRequest = new Request(request, {
138
+ body: JSON.stringify(hasWebSearchTool(body) ? addHostedWebSearch(body) : replaceRemoteCompactions(body)),
139
+ headers,
140
+ });
141
+ try {
142
+ const hostedResponse = await original(hostedRequest);
143
+ // A rejected hosted-tool request is retried with the untouched request so
144
+ // dsh-tool-web can still produce the normal local function-tool path.
145
+ if (!hostedResponse.ok)
146
+ return original(fallbackRequest);
147
+ return hostedResponse;
148
+ }
149
+ catch {
150
+ // Network and transport failures must have the same fallback behavior as
151
+ // an HTTP rejection; the local function-tool path remains available.
152
+ return original(fallbackRequest);
153
+ }
154
+ };
155
+ globalThis.fetch = patched;
156
+ return () => {
157
+ if (globalThis.fetch === patched)
158
+ globalThis.fetch = original;
159
+ };
160
+ }
161
+ /** Enable the transport patch for this Codex plugin scope only. */
162
+ export function installHostedWebSearch() {
163
+ hostedPatchUsers += 1;
164
+ hostedPatchRestore ??= installGlobalHostedWebSearchPatch();
165
+ return () => {
166
+ hostedPatchUsers = Math.max(0, hostedPatchUsers - 1);
167
+ if (hostedPatchUsers === 0) {
168
+ hostedPatchRestore?.();
169
+ hostedPatchRestore = undefined;
170
+ }
171
+ };
172
+ }
173
+ /** Iterate an existing DSH stream with the hosted-request context installed. */
174
+ export function hostedWebSearchStream(next) {
175
+ return (async function* () {
176
+ const iterator = next()[Symbol.asyncIterator]();
177
+ let completed = false;
178
+ try {
179
+ while (true) {
180
+ const item = await HOSTED_REQUESTS.run(true, () => iterator.next());
181
+ if (item.done) {
182
+ completed = true;
183
+ return;
184
+ }
185
+ yield item.value;
186
+ }
187
+ }
188
+ finally {
189
+ if (!completed)
190
+ await iterator.return?.();
191
+ }
192
+ })();
193
+ }
194
+ function textOf(message) {
195
+ return message.content
196
+ .filter(block => block.type === 'text')
197
+ .map(block => block.text)
198
+ .join('');
199
+ }
200
+ function responsesInput(messages) {
201
+ const result = [];
202
+ for (const message of messages) {
203
+ const text = textOf(message);
204
+ if (message.role === 'assistant') {
205
+ if (text.length > 0)
206
+ result.push({
207
+ type: 'message',
208
+ role: 'assistant',
209
+ content: [{ type: 'output_text', text }],
210
+ });
211
+ for (const block of message.content) {
212
+ if (block.type === 'tool-call') {
213
+ result.push({
214
+ type: 'function_call',
215
+ call_id: block.id,
216
+ name: block.name,
217
+ arguments: block.arguments,
218
+ });
219
+ }
220
+ }
221
+ continue;
222
+ }
223
+ const role = message.role === 'system' ? 'developer' : 'user';
224
+ if (text.length > 0)
225
+ result.push({
226
+ type: 'message',
227
+ role,
228
+ content: [{ type: 'input_text', text }],
229
+ });
230
+ for (const block of message.content) {
231
+ if (block.type !== 'tool-result')
232
+ continue;
233
+ result.push({
234
+ type: 'function_call_output',
235
+ call_id: block.toolCallId,
236
+ output: block.content.filter(item => item.type === 'text').map(item => item.text).join(''),
237
+ });
238
+ }
239
+ }
240
+ return result;
241
+ }
242
+ function responsesTools(tools) {
243
+ if (tools === undefined)
244
+ return undefined;
245
+ return tools.filter(tool => tool.name !== 'web_search').map(tool => ({
246
+ type: 'function',
247
+ name: tool.name,
248
+ description: tool.description,
249
+ parameters: tool.parameters,
250
+ }));
251
+ }
252
+ function compactBody(options) {
253
+ const tools = responsesTools(options.tools);
254
+ const body = {
255
+ model: options.model,
256
+ // dsh-compaction-basic appends an instruction for its local summarizer.
257
+ // The hosted compact endpoint owns summarization and must not receive it.
258
+ input: responsesInput(options.messages.slice(0, -1)),
259
+ ...options.system === undefined ? {} : { instructions: options.system },
260
+ ...tools === undefined ? {} : { tools },
261
+ ...options.maxTokens === undefined ? {} : { max_output_tokens: options.maxTokens },
262
+ ...options.reasoningEffort === undefined ? {} : { reasoning: { effort: options.reasoningEffort } },
263
+ };
264
+ const compacted = replaceRemoteCompactions(body);
265
+ return options.tools?.some(tool => tool.name === 'web_search') ? addHostedWebSearch(compacted) : compacted;
266
+ }
267
+ function compactText(body) {
268
+ const output = Array.isArray(body.output) ? body.output : [];
269
+ const text = [];
270
+ for (const item of output) {
271
+ if (typeof item !== 'object' || item === null)
272
+ continue;
273
+ const value = item;
274
+ if (typeof value.encrypted_content === 'string')
275
+ text.push(value.encrypted_content);
276
+ }
277
+ const encrypted = text.join('\n\n').trim();
278
+ return encrypted.length === 0 ? '' : `${REMOTE_COMPACTION_OPEN}${encrypted}${REMOTE_COMPACTION_CLOSE}`;
279
+ }
280
+ async function remoteCompact(ctx, options) {
281
+ const profile = profileOf(ctx, options.provider);
282
+ if (!supportsResponses(profile, options.provider) || profile?.baseURL === undefined) {
283
+ throw new Error('Codex remote compaction requires an OpenAI Responses provider with baseURL');
284
+ }
285
+ const headers = await apiHeaders(ctx, profile);
286
+ if (headers === undefined)
287
+ throw new Error('Codex remote compaction has no configured API key');
288
+ const response = await fetch(responsesEndpoint(profile.baseURL, 'responses/compact'), {
289
+ method: 'POST',
290
+ headers,
291
+ body: JSON.stringify(compactBody(options)),
292
+ ...options.signal === undefined ? {} : { signal: options.signal },
293
+ });
294
+ if (!response.ok)
295
+ throw new Error(`remote compaction returned HTTP ${response.status}`);
296
+ const body = await response.json();
297
+ const text = compactText(body);
298
+ if (text.length === 0)
299
+ throw new Error('remote compaction returned no compaction text');
300
+ return text;
301
+ }
302
+ /** Remote-first compaction waterfall with the existing DSH path as fallback. */
303
+ export function remoteCompactStream(ctx, options, next) {
304
+ return (async function* () {
305
+ try {
306
+ const text = await remoteCompact(ctx, options);
307
+ yield { type: 'block-start', index: 0, blockType: 'text' };
308
+ yield { type: 'text-delta', index: 0, text };
309
+ yield { type: 'block-end', index: 0, block: { type: 'text', text } };
310
+ yield { type: 'usage', usage: { inputTokens: 0, outputTokens: 0 } };
311
+ yield { type: 'finish', reason: { kind: 'stop' } };
312
+ }
313
+ catch (error) {
314
+ if (options.signal?.aborted)
315
+ throw error;
316
+ ctx.logger.warn('codex: remote compaction failed; using dsh-compaction-basic fallback');
317
+ ctx.logger.warn(error);
318
+ // The local fallback still replays the marker through a later GPT
319
+ // Responses request, so it needs the same scoped wire context.
320
+ yield* hostedWebSearchStream(next);
321
+ }
322
+ })();
323
+ }
324
+ //# sourceMappingURL=remote.js.map