maka-agent 0.2.0-dev.45.20260921 → 0.2.0-dev.47.20260922

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.
Files changed (53) hide show
  1. package/DISCLAIMER-WIP +6 -1
  2. package/README.md +6 -1
  3. package/dist/acp/maka-acp-agent.js +1 -1
  4. package/dist/acp/session-mcp.js +212 -0
  5. package/dist/acp/session-registry.js +52 -8
  6. package/dist/acp/stdio-server.js +3 -0
  7. package/dist/cli-core.js +1 -1
  8. package/dist/mcp-capability-provider.js +6 -1
  9. package/dist/mcp-capability-publication.js +139 -0
  10. package/dist/pi-tui-layout.js +46 -17
  11. package/dist/pi-tui-pickers.js +25 -14
  12. package/dist/pi-tui-runner.js +24 -15
  13. package/dist/runtime-host-service-manager.js +26 -2
  14. package/dist/tui-context-refresh.js +15 -26
  15. package/dist/tui-mcp-control.js +29 -88
  16. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  17. package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
  18. package/node_modules/@maka/core/dist/model-thinking.js +36 -1
  19. package/node_modules/@maka/core/dist/provider-registry.js +0 -1
  20. package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
  21. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
  22. package/node_modules/@maka/core/dist/session.js +17 -1
  23. package/node_modules/@maka/core/dist/settings.js +2 -0
  24. package/node_modules/@maka/core/package.json +1 -0
  25. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
  26. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
  27. package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
  28. package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
  29. package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
  30. package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
  31. package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
  32. package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
  33. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +6 -0
  34. package/node_modules/@maka/runtime/dist/session-manager.js +15 -2
  35. package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
  36. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
  37. package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
  38. package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
  39. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
  40. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
  41. package/node_modules/@maka/runtime-host/dist/protocol/index.js +11 -1
  42. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +35 -3
  43. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
  44. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
  45. package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
  46. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +29 -2
  47. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
  48. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
  49. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +1 -1
  50. package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
  51. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +32 -4
  52. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
  53. package/package.json +1 -1
@@ -614,7 +614,6 @@ const providerRegistry = {
614
614
  runtimeAdapter: {
615
615
  kind: 'openai-compatible',
616
616
  name: 'provider',
617
- applyPatchProtocol: 'codex-v4a-freeform',
618
617
  responses: { adapter: 'open-responses', reasoningReplay: 'plaintext-content' },
619
618
  },
620
619
  modelDiscovery: { kind: 'protocol' },
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /**
20
+ * Coordinates a debounced read of an authority-owned projection.
21
+ *
22
+ * The freshness invariant is intentionally stronger than "only the latest
23
+ * query may apply": an authority event also invalidates a read that is
24
+ * already in flight. That read may have captured the projection before the
25
+ * event and can otherwise resolve after the newer state has reached the
26
+ * caller. Callers decide which events are relevant; this module owns the
27
+ * ordering and cancellation rule behind that small interface.
28
+ */
29
+ export function createRefreshReadCoordinator(input) {
30
+ let cancelScheduled;
31
+ let revision = 0;
32
+ const dropScheduled = () => {
33
+ cancelScheduled?.();
34
+ cancelScheduled = undefined;
35
+ };
36
+ const run = () => {
37
+ cancelScheduled = undefined;
38
+ const readRevision = ++revision;
39
+ void input.read().then((result) => {
40
+ if (readRevision !== revision)
41
+ return;
42
+ input.apply(result);
43
+ }, () => { });
44
+ };
45
+ return {
46
+ observe() {
47
+ // Invalidate before scheduling. The previous read can resolve during the
48
+ // debounce window, before the replacement read is issued.
49
+ revision += 1;
50
+ dropScheduled();
51
+ cancelScheduled = input.schedule(run, input.delayMs);
52
+ },
53
+ refresh() {
54
+ dropScheduled();
55
+ run();
56
+ },
57
+ cancel() {
58
+ dropScheduled();
59
+ revision += 1;
60
+ },
61
+ };
62
+ }
@@ -170,6 +170,7 @@ export function decodeModelOverridesTable(value) {
170
170
  'thinkingLevels',
171
171
  'defaultThinkingLevel',
172
172
  'vision',
173
+ 'applyPatch',
173
174
  'contextWindow',
174
175
  'serviceTier',
175
176
  'compactionThreshold',
@@ -210,6 +211,9 @@ export function decodeModelOverridesTable(value) {
210
211
  if (entry.vision !== undefined) {
211
212
  declared.vision = booleanValue(entry.vision, `declared vision for ${modelId}`);
212
213
  }
214
+ if (entry.applyPatch !== undefined) {
215
+ declared.applyPatch = booleanValue(entry.applyPatch, `declared ApplyPatch for ${modelId}`);
216
+ }
213
217
  for (const field of [
214
218
  'contextWindow',
215
219
  'compactionThreshold',
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { isWorkHubActionReceipt } from './workhub-action-result.js';
20
20
  import { isExecutorId } from './executor-id.js';
21
+ import { isThinkingLevel } from './model-thinking.js';
21
22
  import { MODEL_FAILURE_MESSAGE_MAX_BYTES, isModelRetryDecision, } from './model-failure.js';
22
23
  import { decodeMessageContent, TOOL_ACTIVITY_KINDS, } from './events.js';
23
24
  import { isPermissionMode, isToolCategory, } from './permission.js';
@@ -304,16 +305,31 @@ export function isConversationTextMessage(message) {
304
305
  export const WORKHUB_COORDINATION_RECORD_SCHEMA_VERSION = 1;
305
306
  export const WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION = 2;
306
307
  export const WORKHUB_COORDINATION_STOP_SCHEMA_VERSION = 3;
308
+ /** UTF-8 wire limit shared by persisted and protocol Session model identifiers. */
309
+ export const SESSION_MODEL_ID_MAX_BYTES = 512;
307
310
  export function isWorkHubCreateDefaults(value) {
308
311
  if (!isRecord(value) ||
309
- Object.keys(value).some((key) => key !== 'executorId' && key !== 'model' && key !== 'permissionMode'))
312
+ Object.keys(value).some((key) => key !== 'executorId' &&
313
+ key !== 'executorModel' &&
314
+ key !== 'model' &&
315
+ key !== 'thinkingLevel' &&
316
+ key !== 'permissionMode'))
310
317
  return false;
311
318
  if (value.permissionMode !== undefined && !isPermissionMode(value.permissionMode))
312
319
  return false;
313
320
  if (value.executorId !== undefined && !isExecutorId(value.executorId))
314
321
  return false;
322
+ if (value.executorModel !== undefined &&
323
+ (typeof value.executorModel !== 'string' ||
324
+ value.executorModel.trim().length === 0 ||
325
+ new TextEncoder().encode(value.executorModel).byteLength > SESSION_MODEL_ID_MAX_BYTES))
326
+ return false;
327
+ if (value.executorModel !== undefined && value.executorId === undefined)
328
+ return false;
315
329
  if (value.executorId !== undefined && value.model !== undefined)
316
330
  return false;
331
+ if (value.thinkingLevel !== undefined && !isThinkingLevel(value.thinkingLevel))
332
+ return false;
317
333
  if (value.model === undefined)
318
334
  return true;
319
335
  const model = value.model;
@@ -340,6 +340,7 @@ export function createDefaultSettings() {
340
340
  activeTab: 'requests',
341
341
  },
342
342
  appearance: {
343
+ workbarTogglePosition: 'edge',
343
344
  theme: 'auto',
344
345
  palette: 'default',
345
346
  appIcon: DEFAULT_APP_ICON,
@@ -515,6 +516,7 @@ export function normalizeSettings(input) {
515
516
  // position; UI density is no longer a product setting.
516
517
  appearance: {
517
518
  ...appearanceWithoutLegacyFields,
519
+ workbarTogglePosition: base.appearance.workbarTogglePosition === 'titlebar' ? 'titlebar' : 'edge',
518
520
  palette: isThemePalette(base.appearance.palette) ? base.appearance.palette : 'default',
519
521
  // Same fail-closed rule as `palette` above, for the same reason: an
520
522
  // unknown id would otherwise reach the main process and resolve to a
@@ -133,6 +133,7 @@
133
133
  "./computer-use": "./dist/computer-use.js",
134
134
  "./connection-error-copy": "./dist/connection-error-copy.js",
135
135
  "./connection-usage": "./dist/connection-usage.js",
136
+ "./refresh-read-coordinator": "./dist/refresh-read-coordinator.js",
136
137
  "./git-review": "./dist/git-review.js",
137
138
  "./graph-command": "./dist/graph-command.js",
138
139
  "./project": "./dist/project.js",
@@ -59,7 +59,7 @@ function isImageToolResult(value) {
59
59
  typeof image.ref === 'object');
60
60
  }
61
61
  function toolResultText(text) {
62
- return { type: 'content', value: [{ type: 'text', text }] };
62
+ return { type: 'text', text };
63
63
  }
64
64
  function nativeApplyPatchFailureOutput(output) {
65
65
  const value = output.type === 'json' || output.type === 'error-json' ? output.value : undefined;
@@ -538,18 +538,24 @@ export class AiSdkMessageProjection {
538
538
  async materializeToolResultOutput(budget, output, isError, decisionKey) {
539
539
  if (isError || !isImageToolResult(output))
540
540
  return toolResultOutput(output, isError);
541
+ return {
542
+ type: 'content',
543
+ value: [await this.materializeImage(budget, output.ref, output.mimeType, decisionKey)],
544
+ };
545
+ }
546
+ async materializeImage(budget, ref, mediaType, decisionKey) {
541
547
  if (this.input.supportsVision !== true) {
542
548
  return toolResultText('Image was read, but the selected model does not support image input.');
543
549
  }
544
550
  if (!this.input.readAttachmentBytes) {
545
551
  return toolResultText('Image was read, but its stored bytes are unavailable.');
546
552
  }
547
- if (budget && budget.decisions.get(decisionKey) === false) {
553
+ if (budget.decisions.get(decisionKey) === false) {
548
554
  return toolResultText(PROVIDER_IMAGE_BUDGET_EXCEEDED_MESSAGE);
549
555
  }
550
556
  let read;
551
557
  try {
552
- read = await this.input.readAttachmentBytes(output.ref);
558
+ read = await this.input.readAttachmentBytes(ref);
553
559
  }
554
560
  catch {
555
561
  return toolResultText('Image could not be loaded from artifact storage: read_failed.');
@@ -561,18 +567,9 @@ export class AiSdkMessageProjection {
561
567
  return toolResultText(PROVIDER_IMAGE_BUDGET_EXCEEDED_MESSAGE);
562
568
  }
563
569
  return {
564
- type: 'content',
565
- value: [
566
- { type: 'text', text: 'Image read successfully.' },
567
- {
568
- type: 'file',
569
- data: {
570
- type: 'data',
571
- data: Buffer.from(read.bytes).toString('base64'),
572
- },
573
- mediaType: output.mimeType,
574
- },
575
- ],
570
+ type: 'file',
571
+ data: { type: 'data', data: Buffer.from(read.bytes).toString('base64') },
572
+ mediaType,
576
573
  };
577
574
  }
578
575
  async materializeDurableToolResultProjection(budget, projection, decisionKey) {
@@ -580,51 +577,9 @@ export class AiSdkMessageProjection {
580
577
  return durableProjectionToToolResultOutput(projection);
581
578
  const value = [];
582
579
  for (const [index, part] of projection.parts.entries()) {
583
- if (part.kind === 'text') {
584
- value.push({ type: 'text', text: part.text });
585
- continue;
586
- }
587
- if (this.input.supportsVision !== true) {
588
- value.push({
589
- type: 'text',
590
- text: 'Image was read, but the selected model does not support image input.',
591
- });
592
- continue;
593
- }
594
- if (!this.input.readAttachmentBytes) {
595
- value.push({
596
- type: 'text',
597
- text: 'Image was read, but its stored bytes are unavailable.',
598
- });
599
- continue;
600
- }
601
- let read;
602
- try {
603
- read = await this.input.readAttachmentBytes(part.ref);
604
- }
605
- catch {
606
- value.push({
607
- type: 'text',
608
- text: 'Image could not be loaded from artifact storage: read_failed.',
609
- });
610
- continue;
611
- }
612
- if (!read.ok) {
613
- value.push({
614
- type: 'text',
615
- text: `Image could not be loaded from artifact storage: ${read.reason}.`,
616
- });
617
- continue;
618
- }
619
- if (!this.chargeImageBudget(budget, read.bytes.length, `${decisionKey}:artifact:${index}`)) {
620
- value.push({ type: 'text', text: PROVIDER_IMAGE_BUDGET_EXCEEDED_MESSAGE });
621
- continue;
622
- }
623
- value.push({
624
- type: 'file',
625
- data: { type: 'data', data: Buffer.from(read.bytes).toString('base64') },
626
- mediaType: part.mediaType,
627
- });
580
+ value.push(part.kind === 'text'
581
+ ? toolResultText(part.text)
582
+ : await this.materializeImage(budget, part.ref, part.mediaType, `${decisionKey}:artifact:${index}`));
628
583
  }
629
584
  return { type: 'content', value };
630
585
  }
@@ -35,6 +35,7 @@ import { decodePlaintextResponsesReasoningState, responsesReasoningItemId, } fro
35
35
  import { finitePositive } from './context-budget-helpers.js';
36
36
  import { contextDiagnosticsCompactionOf, } from './context-diagnostics.js';
37
37
  import { hasBlockingReplayDiagnostics } from './ai-sdk-compaction.js';
38
+ import { portableApplyPatchTool } from './apply-patch-profile.js';
38
39
  import { RunTrace } from './run-trace.js';
39
40
  import { REQUEST_SANDBOX_BOUNDARY_TOOL_NAME, SANDBOX_BOUNDARY_DENIED_FOR_TURN, SANDBOX_BOUNDARY_FINALIZATION_PROMPT, } from './sandbox-boundary-tool.js';
40
41
  import { buildRuntimeEventModelReplayPlan, buildSteeringEnvelope, collectToolActivityTurnIds, compatibleProviderReasoningReplayEventIds, formatTextWithInlineRefs, steeringMessagesMissingFromBase, steeringModelMessage, } from './model-history.js';
@@ -251,6 +252,10 @@ function renderCodeModeCatalogPrompt(nested) {
251
252
  function nestableToolSnapshot(providerTools, activeToolNames) {
252
253
  const active = new Set(activeToolNames);
253
254
  return new Map(providerTools
255
+ .map((tool) => tool.providerTool?.kind === 'openai-apply-patch' ||
256
+ tool.providerTool?.kind === 'codex-apply-patch'
257
+ ? portableApplyPatchTool(tool)
258
+ : tool)
254
259
  .filter((tool) => active.has(tool.name) &&
255
260
  tool.name !== INVALID_TOOL_NAME &&
256
261
  tool.name !== 'exec' &&
@@ -348,6 +353,7 @@ function joinPromptFragments(fragments) {
348
353
  }
349
354
  const MAX_WAITING_CODE_MODE_CELLS = 1;
350
355
  const MAX_PROVIDER_ATTEMPTS_PER_STEP = 10;
356
+ const CONTEXT_RECOVERY_MAX_OUTPUT_TOKENS = 8_000;
351
357
  const PROVIDER_RETRY_BASE_DELAY_MS = 1_000;
352
358
  const PROVIDER_RETRY_MAX_DELAY_MS = 32_000;
353
359
  const PROVIDER_RETRY_JITTER_FACTOR = 0.25;
@@ -1168,6 +1174,7 @@ export class AiSdkTurn {
1168
1174
  : undefined;
1169
1175
  providerRequestTracker?.setStep(runtimeSteps, requestCompositionId);
1170
1176
  let attemptMessages = projectedMessages;
1177
+ let overflowRecoveryMaxOutputTokens;
1171
1178
  let providerAttempt = 0;
1172
1179
  const returnedToolCalls = [];
1173
1180
  const providerToolInputs = new Map();
@@ -1205,6 +1212,8 @@ export class AiSdkTurn {
1205
1212
  ? nestedTools
1206
1213
  : undefined;
1207
1214
  const requestWatchdog = watchdogState.current;
1215
+ const requestMaxOutputTokens = overflowRecoveryMaxOutputTokens ??
1216
+ this.deps.modelAdapter.maxOutputTokensForInput(midTurnState?.baselineTokens ?? midTurnState?.lastAcceptedTotalTokens);
1208
1217
  // Read here, beside the messages it describes: `attemptMessages` is
1209
1218
  // rebuilt in place by overflow recovery, and the boundary it folded
1210
1219
  // under must travel with that rebuild, not with the step.
@@ -1232,6 +1241,9 @@ export class AiSdkTurn {
1232
1241
  ...(providerRequestTracker ? { providerRequestTracker } : {}),
1233
1242
  ...(historyCompactBoundary ? { historyCompactBoundary } : {}),
1234
1243
  continuationKey: this.turnId,
1244
+ ...(requestMaxOutputTokens !== undefined
1245
+ ? { maxOutputTokens: requestMaxOutputTokens }
1246
+ : {}),
1235
1247
  });
1236
1248
  for await (const event of result.events) {
1237
1249
  if (this.aborted)
@@ -1418,6 +1430,11 @@ export class AiSdkTurn {
1418
1430
  consumeWatchdogTimeout();
1419
1431
  providerOutcome = await result.outcome;
1420
1432
  if (providerOutcome.kind === 'completed') {
1433
+ // A compacted overflow retry only needs the conservative cap for
1434
+ // that one request. Once the provider accepts the request, the
1435
+ // next step can use the fresh provider count to derive a larger
1436
+ // safe cap again.
1437
+ overflowRecoveryMaxOutputTokens = undefined;
1421
1438
  runtimeSteps += 1;
1422
1439
  const stepUsage = providerOutcome.usage;
1423
1440
  providerStepUsage = stepUsage;
@@ -1616,6 +1633,7 @@ export class AiSdkTurn {
1616
1633
  : undefined;
1617
1634
  if (recovered) {
1618
1635
  attemptMessages = recovered.messages;
1636
+ overflowRecoveryMaxOutputTokens = Math.min(this.deps.modelAdapter.maxOutputTokens() ?? CONTEXT_RECOVERY_MAX_OUTPUT_TOKENS, CONTEXT_RECOVERY_MAX_OUTPUT_TOKENS);
1619
1637
  continue;
1620
1638
  }
1621
1639
  // Window suggestion (#4559): the provider rejected a request and
@@ -16,17 +16,30 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { parseCodexV4aPatch } from './codex-v4a-patch.js';
19
+ import { modelApplyPatchEnabled } from '@maka/core/model-thinking';
20
+ import { z } from 'zod';
21
+ import { CODEX_PATCH_DESCRIPTION, parseCodexV4aPatch } from './codex-v4a-patch.js';
20
22
  import { openAiModelSupportsApplyPatch } from './openai-apply-patch.js';
21
- /** Resolve the exact provider/model/wire contract; unknown combinations fail closed. */
23
+ const portableApplyPatchParameters = z.object({ patch: z.string() });
24
+ /** Project a provider-native ApplyPatch tool into the portable client-executed shape. */
25
+ export function portableApplyPatchTool(tool) {
26
+ return {
27
+ ...tool,
28
+ description: CODEX_PATCH_DESCRIPTION,
29
+ parameters: portableApplyPatchParameters,
30
+ providerTool: undefined,
31
+ };
32
+ }
33
+ /** User overrides take precedence; new models can opt in through ordinary function calling. */
22
34
  export function resolveApplyPatchProfile(runtime, modelId) {
23
- if (runtime.wire !== 'openai-responses' || !runtime.applyPatchProtocol)
35
+ if (!modelApplyPatchEnabled(modelId, { applyPatch: runtime.enabled }))
24
36
  return null;
25
- const id = modelId.trim().toLowerCase();
26
- if (runtime.applyPatchProtocol === 'openai-structured' && openAiModelSupportsApplyPatch(id)) {
37
+ const structured = runtime.wire === 'openai-responses' &&
38
+ runtime.applyPatchProtocol === 'openai-structured' &&
39
+ openAiModelSupportsApplyPatch(modelId.trim().toLowerCase());
40
+ if (structured)
27
41
  return { kind: 'openai-structured' };
28
- }
29
- return null;
42
+ return runtime.customTools ? { kind: 'codex-v4a-freeform' } : { kind: 'portable-v4a' };
30
43
  }
31
44
  /** Project one verified profile into an exclusive model-facing editing surface. */
32
45
  export function routeApplyPatchTools(tools, profile) {
@@ -35,19 +48,40 @@ export function routeApplyPatchTools(tools, profile) {
35
48
  return [...tools];
36
49
  if (!profile)
37
50
  return tools.filter((tool) => tool !== applyPatchTool);
38
- return tools.filter((tool) => tool.name !== 'Write' && tool.name !== 'Edit');
51
+ const routed = tools.filter((tool) => tool.name !== 'Write' && tool.name !== 'Edit');
52
+ if (profile.kind === 'openai-structured')
53
+ return routed;
54
+ return routed.map((tool) => tool !== applyPatchTool
55
+ ? tool
56
+ : profile.kind === 'codex-v4a-freeform'
57
+ ? {
58
+ ...tool,
59
+ description: CODEX_PATCH_DESCRIPTION,
60
+ parameters: z.string(),
61
+ providerTool: { kind: 'codex-apply-patch' },
62
+ }
63
+ : portableApplyPatchTool(tool));
39
64
  }
40
- /** Convert historical freeform calls for a structured target, or reject an undeclared target. */
65
+ /** Re-encode history for the current tool transport, or preserve it as facts when disabled. */
41
66
  export function normalizeApplyPatchReplayInput(profile, toolCallId, input) {
42
67
  // A missing profile means the target request does not advertise ApplyPatch.
43
68
  // Returning the historical input would serialize a call to an undeclared
44
69
  // tool; route it through the durable-fact downgrade instead.
45
70
  if (!profile)
46
71
  return null;
47
- if (typeof input !== 'string')
48
- return input;
72
+ const patch = patchText(input);
73
+ if (profile.kind !== 'openai-structured') {
74
+ const text = patch ?? structuredPatchText(input);
75
+ if (text === null)
76
+ return null;
77
+ if (profile.kind === 'codex-v4a-freeform')
78
+ return text;
79
+ return patch !== null && typeof input === 'object' ? input : { patch: text };
80
+ }
81
+ if (patch === null)
82
+ return structuredApplyPatchOperation(input) ? input : null;
49
83
  try {
50
- const operations = parseCodexV4aPatch(input);
84
+ const operations = parseCodexV4aPatch(patch);
51
85
  return operations.length === 1 ? { callId: toolCallId, operation: operations[0] } : null;
52
86
  }
53
87
  catch {
@@ -57,9 +91,10 @@ export function normalizeApplyPatchReplayInput(profile, toolCallId, input) {
57
91
  /** Preserve an executed multi-file patch when the target wire cannot replay one call for it. */
58
92
  export function applyPatchReplayFactText(input, output, isError) {
59
93
  let operations;
60
- if (typeof input === 'string') {
94
+ const patch = patchText(input);
95
+ if (patch !== null) {
61
96
  try {
62
- operations = parseCodexV4aPatch(input);
97
+ operations = parseCodexV4aPatch(patch);
63
98
  }
64
99
  catch {
65
100
  return null;
@@ -129,3 +164,18 @@ function structuredApplyPatchOperation(input) {
129
164
  }
130
165
  return null;
131
166
  }
167
+ function patchText(input) {
168
+ if (typeof input === 'string')
169
+ return input;
170
+ if (input && typeof input === 'object' && 'patch' in input && typeof input.patch === 'string')
171
+ return input.patch;
172
+ return null;
173
+ }
174
+ function structuredPatchText(input) {
175
+ const op = structuredApplyPatchOperation(input);
176
+ if (!op)
177
+ return null;
178
+ const action = op.type === 'create_file' ? 'Add' : op.type === 'delete_file' ? 'Delete' : 'Update';
179
+ const body = op.type === 'delete_file' ? '' : op.diff.endsWith('\n') ? op.diff : `${op.diff}\n`;
180
+ return `*** Begin Patch\n*** ${action} File: ${op.path}\n${body}*** End Patch`;
181
+ }
@@ -161,10 +161,11 @@ export function buildBuiltinTools(options = {}) {
161
161
  providerTool: { kind: 'openai-apply-patch' },
162
162
  executionFacts,
163
163
  impl: async (input, ctx) => {
164
- if (typeof input !== 'string') {
164
+ const patch = typeof input === 'string' ? input : input.patch;
165
+ if (typeof patch !== 'string') {
165
166
  return await filesystem.applyPatch({ operation: input.operation, ...filesystemCall(ctx) });
166
167
  }
167
- const operations = parseCodexV4aPatch(input);
168
+ const operations = parseCodexV4aPatch(patch);
168
169
  return await executeApplyPatchOperations(operations, async (operation) => {
169
170
  await filesystem.applyPatch({ operation, ...filesystemCall(ctx) });
170
171
  }, ctx.abortSignal);
@@ -16,6 +16,24 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ export const CODEX_PATCH_DESCRIPTION = `Edit files using a patch enclosed in *** Begin Patch and *** End Patch.
20
+ Use *** Add File: path with each content line prefixed by +; *** Delete File: path;
21
+ or *** Update File: path with @@ context markers and lines prefixed by space (unchanged),
22
+ - (removed), or + (added). Include enough unchanged context to locate each edit.
23
+ One patch may contain multiple file operations. Moves are not supported: add the destination
24
+ and delete the source instead. Read existing files before patching them.`;
25
+ // Match the executor's supported subset: intentionally no Move to production.
26
+ export const CODEX_PATCH_GRAMMAR = String.raw `start: begin_patch hunk+ end_patch
27
+ begin_patch: "*** Begin Patch" LF
28
+ end_patch: "*** End Patch" LF?
29
+ hunk: add_hunk | delete_hunk | update_hunk
30
+ add_hunk: "*** Add File: " filename LF add_line+
31
+ delete_hunk: "*** Delete File: " filename LF
32
+ update_hunk: "*** Update File: " filename LF change+
33
+ filename: /[^\r\n]+/
34
+ add_line: "+" /[^\r\n]+/? LF
35
+ change: ("@@" | "@@ " /[^\r\n]+/) LF | ("+" | "-" | " ") /[^\r\n]+/? LF | "*** End of File" LF
36
+ %import common.LF`;
19
37
  export class CodexV4aPatchError extends Error {
20
38
  constructor(message) {
21
39
  super(message);
@@ -22,13 +22,14 @@ import { providerAuthRequiresSecret, } from '@maka/core/llm-connections';
22
22
  import { lookupModelMetadata } from '@maka/core/model-metadata';
23
23
  import { rawFinishReasonString } from './model-protocol.js';
24
24
  import { resolveModelRuntime } from './model-runtime.js';
25
+ import { resolveSelectedModelContextWindow } from './context-budget-policy.js';
25
26
  import { plaintextResponsesReasoningProviderOptions, withoutMakaResponsesState, safePlaintextResponsesReasoningItemId, } from './responses-reasoning-state.js';
26
27
  import { classifyError, providerModelFailure } from './provider-error-classification.js';
27
28
  import { withProviderStreamTracking, } from './provider-request-telemetry.js';
28
29
  import { createOpenAiChatReasoningTransportState, openAiChatReasoningFieldProviderOptions, restoreOpenAiChatEmptyReasoning, } from './openai-chat-reasoning-transport.js';
29
30
  import { mergeOpenAiResponsesProviderOptions, planOpenAiResponsesContinuation, } from './openai-responses-continuation.js';
30
31
  import { createOpenAiResponsesTransportState, OPENAI_RESPONSES_LANE_HEADER, } from './openai-responses-websocket.js';
31
- import { openAiApplyPatchProviderTool } from './openai-apply-patch.js';
32
+ import { openAiApplyPatchProviderTool, codexApplyPatchProviderTool } from './openai-apply-patch.js';
32
33
  import { TOOL_SEARCH_NAME, TOOL_SEARCH_PROVIDER_NAME } from './tool-availability.js';
33
34
  export class ModelAdapter {
34
35
  input;
@@ -97,12 +98,40 @@ export class ModelAdapter {
97
98
  maxOutputTokens() {
98
99
  return selectedModelMaxOutputTokens(this.input.connection, this.input.modelId, this.input.providerOptions, this.runtime);
99
100
  }
101
+ /**
102
+ * Keep a provider-derived output limit from making a resumed request
103
+ * impossible. The token count is the last request the provider accepted, so
104
+ * it is a conservative lower bound for the next request. Leave a small
105
+ * amount of room for newly appended user/tool content because Runtime does
106
+ * not estimate the final prompt locally.
107
+ */
108
+ maxOutputTokensForInput(knownInputTokens) {
109
+ const outputLimit = this.maxOutputTokens();
110
+ if (outputLimit === undefined ||
111
+ knownInputTokens === undefined ||
112
+ !Number.isFinite(knownInputTokens) ||
113
+ knownInputTokens < 0) {
114
+ return outputLimit;
115
+ }
116
+ const contextWindow = resolveSelectedModelContextWindow(this.input.connection, this.input.modelId);
117
+ if (contextWindow === undefined)
118
+ return outputLimit;
119
+ const thinkingBudget = this.runtime.wire === 'anthropic-messages'
120
+ ? fixedAnthropicThinkingBudget(this.input.providerOptions)
121
+ : 0;
122
+ const available = contextWindow - Math.ceil(knownInputTokens) - CONTEXT_INPUT_GROWTH_HEADROOM - thinkingBudget;
123
+ // Do not turn a near-window request into a one-token success. A useful
124
+ // floor deliberately lets the provider reject the request, which keeps
125
+ // the existing reactive compaction path in control of recovery.
126
+ return Math.max(Math.min(outputLimit, CONTEXT_RECOVERY_OUTPUT_FLOOR), Math.min(outputLimit, available));
127
+ }
100
128
  async startStream(input) {
101
129
  const ai = await import('ai').catch((err) => {
102
130
  throw new Error(`Failed to load 'ai' package. Run \`npm install ai\`. Inner: ${err.message}`);
103
131
  });
104
132
  const { streamText, wrapLanguageModel } = ai;
105
- const maxOutputTokens = selectedModelMaxOutputTokens(this.input.connection, this.input.modelId, this.input.providerOptions, this.runtime);
133
+ const maxOutputTokens = input.maxOutputTokens ??
134
+ selectedModelMaxOutputTokens(this.input.connection, this.input.modelId, this.input.providerOptions, this.runtime);
106
135
  let settleAccounting;
107
136
  const terminalModel = withProviderFinishBoundary(input.model, wrapLanguageModel);
108
137
  const trackedModel = input.providerRequestTracker
@@ -127,7 +156,7 @@ export class ModelAdapter {
127
156
  sdkTools[TOOL_SEARCH_PROVIDER_NAME] = sdkTools[TOOL_SEARCH_NAME];
128
157
  delete sdkTools[TOOL_SEARCH_NAME];
129
158
  }
130
- const fullMessages = input.messages;
159
+ const fullMessages = this.runtime.wire === 'openai-chat' ? lowerChatToolImages(input.messages) : input.messages;
131
160
  const responsesLane = input.continuationKey && usesNativeOpenAiResponses(this.input.connection, this.runtime)
132
161
  ? input.continuationKey
133
162
  : undefined;
@@ -441,6 +470,12 @@ function failedStepOutcome(failure, usage) {
441
470
  continuation: 'none',
442
471
  };
443
472
  }
473
+ /**
474
+ * A persisted provider count describes the preceding request, not the new
475
+ * user message or tool result that may be appended after a restart.
476
+ */
477
+ const CONTEXT_INPUT_GROWTH_HEADROOM = 8_000;
478
+ const CONTEXT_RECOVERY_OUTPUT_FLOOR = 8_000;
444
479
  function selectedModelMaxOutputTokens(connection, modelId, providerOptions, runtime) {
445
480
  const anthropicMessages = runtime.wire === 'anthropic-messages';
446
481
  const kimiOpenAiChat = connection.providerType === 'kimi-coding-plan' && runtime.wire === 'openai-chat';
@@ -835,6 +870,46 @@ function translateChunk(chunk, openAiChatReasoningTransportState, runtime, runti
835
870
  return [];
836
871
  }
837
872
  }
873
+ function lowerChatToolImages(messages) {
874
+ const result = [];
875
+ let images = [];
876
+ const flush = () => {
877
+ if (images.length === 0)
878
+ return;
879
+ result.push({ role: 'user', content: images });
880
+ images = [];
881
+ };
882
+ for (const message of messages) {
883
+ if (message.role !== 'tool') {
884
+ flush();
885
+ result.push(message);
886
+ continue;
887
+ }
888
+ result.push({
889
+ ...message,
890
+ content: message.content.map((part) => {
891
+ if (part.type !== 'tool-result' || part.output.type !== 'content')
892
+ return part;
893
+ return {
894
+ ...part,
895
+ output: {
896
+ ...part.output,
897
+ value: part.output.value.map((content) => {
898
+ if (content.type !== 'file' || !content.mediaType.startsWith('image/'))
899
+ return content;
900
+ images.push({ type: 'text', text: `Image from tool ${part.toolName} (${part.toolCallId}):` }, content);
901
+ return { type: 'text', text: 'Image supplied below.' };
902
+ }),
903
+ },
904
+ };
905
+ }),
906
+ });
907
+ }
908
+ // Chat tool messages are text-only; attach images after the whole result group
909
+ // so an assistant's parallel tool calls stay paired before the next user message.
910
+ flush();
911
+ return result;
912
+ }
838
913
  /**
839
914
  * OpenAI Responses reserves the provider name `tool_search`. Keep Maka's
840
915
  * persisted/history name intact and translate only the provider-bound copy.
@@ -898,6 +973,8 @@ function compileProviderTool(tool) {
898
973
  switch (tool.kind) {
899
974
  case 'openai-apply-patch':
900
975
  return openAiApplyPatchProviderTool;
976
+ case 'codex-apply-patch':
977
+ return codexApplyPatchProviderTool;
901
978
  case 'openai-web-search':
902
979
  return openai.tools.webSearch({
903
980
  ...(tool.searchContextSize ? { searchContextSize: tool.searchContextSize } : {}),
@@ -19,6 +19,7 @@
19
19
  import { PROVIDER_REGISTRY, effectiveBaseUrl, } from '@maka/core/llm-connections';
20
20
  import { lookupModelMetadata, lookupModelRuntimeOverride, openAiAdapterApiProtocol, } from '@maka/core/model-metadata';
21
21
  import { isRetiredProvider } from '@maka/core/provider-registry';
22
+ import { modelOverride } from '@maka/core/model-thinking';
22
23
  import { anthropicV1BaseUrl, googleV1BetaBaseUrl, openAiResponsesBaseUrl, } from './provider-urls.js';
23
24
  import { resolveApplyPatchProfile } from './apply-patch-profile.js';
24
25
  export function resolveModelRuntime(connection, modelId) {
@@ -76,6 +77,11 @@ export function resolveModelRuntime(connection, modelId) {
76
77
  applyPatchProfile: resolveApplyPatchProfile({
77
78
  wire,
78
79
  applyPatchProtocol: adapter.applyPatchProtocol,
80
+ enabled: modelOverride(connection, modelId)?.applyPatch,
81
+ customTools: wire === 'openai-responses' &&
82
+ (connection.providerType === 'openai' || connection.providerType === 'openai-codex') &&
83
+ replay.kind === 'responses' &&
84
+ replay.contract.adapter === 'openai',
79
85
  }, modelId),
80
86
  };
81
87
  }