Skip to content

Commit 0c056ee

Browse files
authored
Merge pull request #197 from asynkron/codex/locate-self-heal.md-62gob4
Fix OpenAI response types and consolidate web panel toggles
2 parents 8e844b4 + 134e911 commit 0c056ee

File tree

8 files changed

+108
-94
lines changed

8 files changed

+108
-94
lines changed

packages/core/src/agent/context.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
## Key Modules
88

99
- `loop.ts` (emits `loop.js`) — orchestrates the event-driven runtime: manages plan snapshots, queues inputs/outputs, handles OpenAI calls, applies filters, coordinates cancellation, and enforces a payload-growth failsafe that dumps runaway histories before retrying. The runtime exposes factory hooks (`createOutputsQueueFn`, `createInputsQueueFn`, `createPlanManagerFn`, `createEscStateFn`, `createPromptCoordinatorFn`, `createApprovalManagerFn`) so hosts can inject alternatives without patching the core loop. Additional DI knobs let callers override logging, request cancellation, prompt phrasing, auto-response limits, and downstream pass executor dependencies.
10-
- `passExecutor.ts` (emits `passExecutor.js`) — coordinates multi-pass reasoning. It requests model completions, parses and validates assistant responses, merges incoming plan data, dispatches shell commands, records observations, nudges the model when plans stall, and syncs plan snapshots back to disk. The helper accepts overridable collaborators for OpenAI calls, command execution, history compaction, observation building, context usage summaries, and schema validation. Supporting utilities live alongside it under `passExecutor/` (plan execution helpers, refusal heuristics) so orchestration logic stays focused on control flow.
10+
- `passExecutor.ts` (emits `passExecutor.js`) — coordinates multi-pass reasoning. It requests model completions, parses and validates assistant responses, merges incoming plan data, dispatches shell commands, records observations, nudges the model when plans stall, and syncs plan snapshots back to disk. The helper accepts overridable collaborators for OpenAI calls, command execution, history compaction, observation building, context usage summaries, and schema validation. Supporting utilities live alongside it under `passExecutor/` (plan execution helpers, refusal heuristics) so orchestration logic stays focused on control flow, and a small plan-reminder controller now keeps auto-response counters consistent even when hosts omit a tracker implementation. Recent tightening removed `@ts-nocheck`, aligning the module with typed OpenAI clients and DI hooks so build-time checking now covers its glue code.
11+
- `passExecutor.ts` (emits `passExecutor.js`) — coordinates multi-pass reasoning. It requests model completions, parses and validates assistant responses, merges incoming plan data, dispatches shell commands, records observations, nudges the model when plans stall, and syncs plan snapshots back to disk. The helper accepts overridable collaborators for OpenAI calls, command execution, history compaction, observation building, context usage summaries, and schema validation. Supporting utilities live alongside it under `passExecutor/` (plan execution helpers, refusal heuristics) so orchestration logic stays focused on control flow, and a small plan-reminder controller now keeps auto-response counters consistent even when hosts omit a tracker implementation. Recent tightening removed `@ts-nocheck`, aligning the module with typed OpenAI clients and DI hooks so build-time checking now covers its glue code, and the plan manager invoker now carries explicit plan argument types instead of falling back to `any`.
1112
- `approvalManager.ts` (emits `approvalManager.js`) — normalizes the approval policy: checks allowlists/session approvals, optionally prompts the human, and records session grants.
1213
- `amnesiaManager.ts` (emits `amnesiaManager.js`) — prunes stale history entries and exposes a dementia policy helper that drops messages older than the configured pass threshold.
1314
- `commandExecution.ts` (emits `commandExecution.js`) and `commands/ExecuteCommand.ts` — normalize assistant command payloads and delegate to the injected shell runner.

packages/core/src/agent/passExecutor.ts

Lines changed: 85 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
// @ts-nocheck
21
import { planHasOpenSteps } from '../utils/plan.js';
32
import { incrementCommandCount as defaultIncrementCommandCount } from '../services/commandStatsService.js';
43
import {
54
combineStdStreams as defaultCombineStdStreams,
65
buildPreview as defaultBuildPreview,
76
} from '../utils/output.js';
8-
import ObservationBuilder from './observationBuilder.js';
7+
import ObservationBuilder, { type ObservationBuilderDeps } from './observationBuilder.js';
98
import { parseAssistantResponse as defaultParseAssistantResponse } from './responseParser.js';
109
import { requestModelCompletion as defaultRequestModelCompletion } from './openaiRequest.js';
1110
import { executeAgentCommand as defaultExecuteAgentCommand } from './commandExecution.js';
@@ -19,6 +18,7 @@ import {
1918
createChatMessageEntry as defaultCreateChatMessageEntry,
2019
type ChatMessageEntry,
2120
} from './historyEntry.js';
21+
import type { ResponsesClient } from '../openai/responses.js';
2222
import {
2323
createObservationHistoryEntry,
2424
createPlanReminderEntry,
@@ -55,16 +55,18 @@ interface PlanManagerLike {
5555
sync?: (plan: PlanStep[] | null | undefined) => Promise<unknown>;
5656
}
5757

58-
type PlanManagerMethod = ((...args: unknown[]) => unknown) | null | undefined;
58+
type PlanManagerMethod = ((plan?: PlanStep[] | null | undefined) => unknown) | null | undefined;
5959

6060
const createPlanManagerInvoker =
6161
(manager: PlanManagerLike) =>
62-
(method: PlanManagerMethod, ...args: unknown[]): unknown => {
62+
(method: PlanManagerMethod, plan?: PlanStep[] | null | undefined): unknown => {
6363
if (typeof method !== 'function') {
6464
return undefined;
6565
}
6666

67-
return method.call(manager, ...args);
67+
// Passing an explicit plan argument keeps TypeScript satisfied while still
68+
// allowing zero-argument plan manager methods to ignore it at runtime.
69+
return method.call(manager, plan);
6870
};
6971

7072
interface ExecutableCandidate extends ExecutablePlanStep {
@@ -74,14 +76,75 @@ interface ExecutableCandidate extends ExecutablePlanStep {
7476

7577
const PLAN_REMINDER_AUTO_RESPONSE_LIMIT = 3;
7678

79+
interface PlanReminderController {
80+
recordAttempt: () => number;
81+
reset: () => void;
82+
getCount: () => number;
83+
}
84+
85+
const createPlanReminderController = (
86+
tracker: PlanAutoResponseTracker | null | undefined,
87+
): PlanReminderController => {
88+
if (tracker && typeof tracker.increment === 'function' && typeof tracker.reset === 'function') {
89+
return {
90+
recordAttempt: () => tracker.increment(),
91+
reset: () => tracker.reset(),
92+
getCount: () => (typeof tracker.getCount === 'function' ? (tracker.getCount() ?? 0) : 0),
93+
};
94+
}
95+
96+
// Fallback tracker keeps local state so the reminder limit still applies even when
97+
// the caller does not provide a dedicated tracker implementation.
98+
let fallbackCount = 0;
99+
return {
100+
recordAttempt: () => {
101+
fallbackCount += 1;
102+
return fallbackCount;
103+
},
104+
reset: () => {
105+
fallbackCount = 0;
106+
},
107+
getCount: () => fallbackCount,
108+
};
109+
};
110+
111+
const pickNextExecutableCandidate = (entries: ExecutablePlanStep[]): ExecutableCandidate | null => {
112+
let best: ExecutableCandidate | null = null;
113+
114+
for (let index = 0; index < entries.length; index += 1) {
115+
const entry = entries[index];
116+
const candidate: ExecutableCandidate = {
117+
...entry,
118+
index,
119+
priority: getPriorityScore(entry.step),
120+
};
121+
122+
if (!best) {
123+
best = candidate;
124+
continue;
125+
}
126+
127+
if (candidate.priority < best.priority) {
128+
best = candidate;
129+
continue;
130+
}
131+
132+
if (candidate.priority === best.priority && candidate.index < best.index) {
133+
best = candidate;
134+
}
135+
}
136+
137+
return best;
138+
};
139+
77140
const normalizeAssistantMessage = (value: unknown): string =>
78141
typeof value === 'string' ? value.replace(/[\u2018\u2019]/g, "'") : '';
79142
// Quick heuristic to detect short apology-style refusals so we can auto-nudge the model.
80143
const isLikelyRefusalMessage = (message: unknown): boolean =>
81144
refusalHeuristics.isLikelyRefusalMessage(message);
82145

83146
export interface ExecuteAgentPassOptions {
84-
openai: unknown;
147+
openai: ResponsesClient;
85148
model: string;
86149
history: ChatMessageEntry[];
87150
emitEvent?: (event: UnknownRecord) => void;
@@ -103,14 +166,9 @@ export interface ExecuteAgentPassOptions {
103166
passIndex: number;
104167
requestModelCompletionFn?: typeof defaultRequestModelCompletion;
105168
executeAgentCommandFn?: typeof defaultExecuteAgentCommand;
106-
createObservationBuilderFn?: (deps: {
107-
combineStdStreams: typeof defaultCombineStdStreams;
108-
applyFilter: (text: string, regex: string) => string;
109-
tailLines: (text: string, lines: number) => string;
110-
buildPreview: typeof defaultBuildPreview;
111-
}) => ObservationBuilder;
112-
combineStdStreamsFn?: typeof defaultCombineStdStreams;
113-
buildPreviewFn?: typeof defaultBuildPreview;
169+
createObservationBuilderFn?: (deps: ObservationBuilderDeps) => ObservationBuilder;
170+
combineStdStreamsFn?: ObservationBuilderDeps['combineStdStreams'];
171+
buildPreviewFn?: ObservationBuilderDeps['buildPreview'];
114172
parseAssistantResponseFn?: typeof defaultParseAssistantResponse;
115173
validateAssistantResponseSchemaFn?: typeof defaultValidateAssistantResponseSchema;
116174
validateAssistantResponseFn?: typeof defaultValidateAssistantResponse;
@@ -152,8 +210,10 @@ export async function executeAgentPass({
152210
requestModelCompletionFn = defaultRequestModelCompletion,
153211
executeAgentCommandFn = defaultExecuteAgentCommand,
154212
createObservationBuilderFn = (deps) => new ObservationBuilder(deps),
155-
combineStdStreamsFn = defaultCombineStdStreams,
156-
buildPreviewFn = defaultBuildPreview,
213+
combineStdStreamsFn = (stdout, stderr, exitCode) =>
214+
// Normalize optional exit codes before delegating so ObservationBuilder sees a consistent signature.
215+
defaultCombineStdStreams(stdout, stderr, exitCode ?? 0),
216+
buildPreviewFn = (text) => defaultBuildPreview(text),
157217
parseAssistantResponseFn = defaultParseAssistantResponse,
158218
validateAssistantResponseSchemaFn = defaultValidateAssistantResponseSchema,
159219
validateAssistantResponseFn = defaultValidateAssistantResponse,
@@ -326,28 +386,7 @@ export async function executeAgentPass({
326386

327387
const parsed = parseResult.value;
328388

329-
const planAutoResponder =
330-
planAutoResponseTracker &&
331-
typeof planAutoResponseTracker.increment === 'function' &&
332-
typeof planAutoResponseTracker.reset === 'function'
333-
? planAutoResponseTracker
334-
: null;
335-
336-
const incrementPlanReminder = () => {
337-
if (!planAutoResponder || typeof planAutoResponder.increment !== 'function') {
338-
return 1;
339-
}
340-
341-
return planAutoResponder.increment();
342-
};
343-
344-
const resetPlanReminder = () => {
345-
if (!planAutoResponder || typeof planAutoResponder.reset !== 'function') {
346-
return;
347-
}
348-
349-
planAutoResponder.reset();
350-
};
389+
const planReminder = createPlanReminderController(planAutoResponseTracker);
351390

352391
emitDebug(() => ({
353392
stage: 'assistant-response',
@@ -493,27 +532,8 @@ export async function executeAgentPass({
493532

494533
let planMutatedDuringExecution = false;
495534

496-
const selectNextExecutableEntry = (): ExecutableCandidate | null => {
497-
const candidates = collectExecutablePlanSteps(activePlan).map((entry, index) => ({
498-
...entry,
499-
index,
500-
priority: getPriorityScore(entry.step),
501-
}));
502-
503-
if (candidates.length === 0) {
504-
return null;
505-
}
506-
507-
candidates.sort((a, b) => {
508-
if (a.priority === b.priority) {
509-
return a.index - b.index;
510-
}
511-
512-
return a.priority - b.priority;
513-
});
514-
515-
return candidates[0] ?? null;
516-
};
535+
const selectNextExecutableEntry = (): ExecutableCandidate | null =>
536+
pickNextExecutableCandidate(collectExecutablePlanSteps(activePlan));
517537

518538
let nextExecutable = selectNextExecutableEntry();
519539

@@ -545,15 +565,15 @@ export async function executeAgentPass({
545565
pass: activePass,
546566
}),
547567
);
548-
resetPlanReminder();
568+
planReminder.reset();
549569
return true;
550570
}
551571

552572
const hasOpenSteps =
553573
Array.isArray(activePlan) && activePlan.length > 0 && planHasOpenSteps(activePlan);
554574

555575
if (hasOpenSteps) {
556-
const attempt = incrementPlanReminder();
576+
const attempt = planReminder.recordAttempt();
557577

558578
if (attempt <= PLAN_REMINDER_AUTO_RESPONSE_LIMIT) {
559579
emitEvent({
@@ -570,7 +590,7 @@ export async function executeAgentPass({
570590

571591
if (!activePlanEmpty && !hasOpenSteps) {
572592
// The plan is finished; wipe the snapshot so follow-up prompts start cleanly.
573-
if (invokePlanManager) {
593+
if (planManager && invokePlanManager) {
574594
try {
575595
const cleared = await invokePlanManager(planManager.reset);
576596
if (Array.isArray(cleared)) {
@@ -594,11 +614,11 @@ export async function executeAgentPass({
594614
emitEvent({ type: 'plan', plan: clonePlanForExecution(activePlan) });
595615
}
596616

597-
resetPlanReminder();
617+
planReminder.reset();
598618
return false;
599619
}
600620

601-
resetPlanReminder();
621+
planReminder.reset();
602622

603623
const manageCommandThinking =
604624
Boolean(nextExecutable) &&
@@ -817,7 +837,7 @@ export async function executeAgentPass({
817837
if (planMutatedDuringExecution && Array.isArray(activePlan)) {
818838
emitEvent({ type: 'plan', plan: clonePlanForExecution(activePlan) });
819839

820-
if (invokePlanManager && typeof planManager?.sync === 'function') {
840+
if (planManager && invokePlanManager && typeof planManager.sync === 'function') {
821841
try {
822842
await invokePlanManager(planManager.sync, activePlan);
823843
} catch (error) {

packages/core/src/openai/context.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
## Key Files
88

99
- `client.js` — lazily instantiates the OpenAI SDK client based on environment variables, validates models, surfaces setup guidance when configuration is missing, and exposes `MODEL`, `getOpenAIClient`, `resetOpenAIClient`.
10-
- `responses.js` — constructs structured responses API calls, attaches tool schemas, handles retries, and normalizes errors (now resolving both object- and function-shaped providers returned by the AI SDK) while exposing typed call options so downstream callers no longer rely on defensive runtime checks.
10+
- `responses.js` — constructs structured responses API calls, attaches tool schemas, handles retries, and normalizes errors (now resolving both object- and function-shaped providers returned by the AI SDK) while exposing typed call options so downstream callers no longer rely on defensive runtime checks. Call settings now use a partial type so default retry/abort behavior compiles cleanly when no overrides are provided.
1111
- `responseUtils.js` — normalizes OpenAI Responses payloads, exposing helpers to pull the sanitized `open-agent` tool call (for protocol validation) while still providing a text fallback for legacy/plain-text replies.
1212

1313
## Positive Signals

packages/core/src/openai/responses.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export interface ResponseCallOptions {
4242
maxRetries?: number;
4343
}
4444

45-
type ResponseCallSettings = Pick<CallSettings, 'abortSignal' | 'maxRetries'>;
45+
type ResponseCallSettings = Partial<Pick<CallSettings, 'abortSignal' | 'maxRetries'>>;
4646

4747
function buildCallSettings(options: ResponseCallOptions | undefined): ResponseCallSettings {
4848
const { maxRetries } = getOpenAIRequestSettings();

packages/core/src/utils/context.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
- `contextUsage.ts` — tracks token usage metrics for display in the CLI.
1212
- `fetch.ts` — thin wrapper around `undici`/`node-fetch` semantics with timeout & error normalization; now prefers the global Fetch API with typed fallbacks to Node's `http`/`https` modules.
1313
- `jsonAssetValidator.ts` — validates JSON files against provided schemas; leveraged by scripts/tests.
14-
- `output.ts` — formatting helpers for CLI output and logs, now typed to guarantee string outputs.
14+
- `output.ts` — formatting helpers for CLI output and logs, now typed to guarantee string outputs. The `combineStdStreams` helper tolerates missing exit codes so observation builders can share the same implementation across typed and untyped callers.
1515
- `plan.ts` — plan tree clone/merge/progress utilities used by agent runtime & UI.
1616
- Incoming items with `status: 'abandoned'` now remove the matching plan branch during merge.
1717
- Steps waiting on dependencies now remain blocked if any dependency failed instead of treating failure as completion.

packages/core/src/utils/output.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ export type CombinedStreams = {
1010
export function combineStdStreams(
1111
filteredStdout: unknown,
1212
filteredStderr: unknown,
13-
exitCode: number,
13+
exitCode: number | null | undefined,
1414
): CombinedStreams {
1515
const stdoutText = filteredStdout != null ? String(filteredStdout) : '';
1616
const stderrText = filteredStderr != null ? String(filteredStderr) : '';
17+
const normalizedExitCode = typeof exitCode === 'number' ? exitCode : 0;
1718

18-
if (exitCode === 0 && stderrText.trim().length > 0) {
19+
if (normalizedExitCode === 0 && stderrText.trim().length > 0) {
1920
const combined = stdoutText ? `${stdoutText}\n${stderrText}` : stderrText;
2021
return { stdout: combined, stderr: '' };
2122
}

packages/web/frontend/context.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@
3131
- Pruned unused helper typings in shared context/tests so ESLint stays quiet under the expanded repo-wide lint run.
3232
- Extracted helper utilities inside `services/chat.ts` to normalise event payloads before rendering, reducing duplicated DOM-building logic when displaying agent banners and status messages.
3333
- `services/chat.ts` now centralises message container creation/append helpers so message, event, and command renders share the same DOM plumbing instead of repeating wrapper scaffolding.
34+
- Panel activation toggles now share a dedicated helper so conversation start and reset flows reuse the same DOM updates without duplicating focus/visibility logic.

packages/web/frontend/src/js/services/chat.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -169,26 +169,30 @@ export function createChatService({
169169
}
170170
}
171171

172-
function ensureConversationStarted(): void {
173-
if (hasConversation) {
174-
return;
175-
}
176-
hasConversation = true;
177-
panelRef.classList.toggle('agent-panel--empty', false);
172+
function setPanelActive(active: boolean): void {
173+
panelRef.classList.toggle('agent-panel--empty', !active);
178174
if (startContainer) {
179-
startContainer.classList.toggle('hidden', true);
175+
startContainer.classList.toggle('hidden', active);
180176
}
181177
if (chatContainer) {
182-
chatContainer.classList.toggle('hidden', false);
178+
chatContainer.classList.toggle('hidden', !active);
183179
}
184-
if (chatInput) {
180+
if (active && chatInput) {
185181
windowRef.requestAnimationFrame(() => {
186182
chatInput.focus();
187183
autoResize(chatInput);
188184
});
189185
}
190186
}
191187

188+
function ensureConversationStarted(): void {
189+
if (hasConversation) {
190+
return;
191+
}
192+
hasConversation = true;
193+
setPanelActive(true);
194+
}
195+
192196
function ensureThinkingMessage(): void {
193197
if (!messageList || thinkingMessage) {
194198
return;
@@ -520,20 +524,7 @@ export function createChatService({
520524
}
521525

522526
function updatePanelState(): void {
523-
const active = hasConversation;
524-
panelRef.classList.toggle('agent-panel--empty', !active);
525-
if (startContainer) {
526-
startContainer.classList.toggle('hidden', active);
527-
}
528-
if (chatContainer) {
529-
chatContainer.classList.toggle('hidden', !active);
530-
}
531-
if (active && chatInput) {
532-
windowRef.requestAnimationFrame(() => {
533-
chatInput.focus();
534-
autoResize(chatInput);
535-
});
536-
}
527+
setPanelActive(hasConversation);
537528
}
538529

539530
/**

0 commit comments

Comments
 (0)