@bitkyc08/opencodex 2.52.0 → 2.53.0
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/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { normalizeKiroModelId } from "../../providers/kiro-models";
|
|
2
|
+
import { namespacedToolName } from "../../types";
|
|
3
|
+
import type {
|
|
4
|
+
OcxAssistantMessage,
|
|
5
|
+
OcxContentPart,
|
|
6
|
+
OcxParsedRequest,
|
|
7
|
+
OcxTextContent,
|
|
8
|
+
OcxTool,
|
|
9
|
+
OcxToolCall,
|
|
10
|
+
OcxToolResultMessage,
|
|
11
|
+
} from "../../types";
|
|
12
|
+
import {
|
|
13
|
+
KIRO_ANSWER_DELIVERED_MESSAGE,
|
|
14
|
+
KIRO_COMPLETION_INSTRUCTIONS,
|
|
15
|
+
KIRO_COMPLETION_RETRY_MESSAGE,
|
|
16
|
+
KIRO_COMPLETION_TOOL_NAME,
|
|
17
|
+
KIRO_CONTINUATION_MESSAGE,
|
|
18
|
+
KIRO_EMPTY_TOOL_RESULT_MESSAGE,
|
|
19
|
+
KIRO_TOOL_RESULT_CARRIER_MESSAGE,
|
|
20
|
+
MAX_KIRO_INJECTED_INSTRUCTION_CHARS,
|
|
21
|
+
type KiroCompletionMode,
|
|
22
|
+
} from "../kiro-constants";
|
|
23
|
+
import { EMPTY_EXEC_OUTPUT_MESSAGE, annotateCodeModeHostFailure, normalizeEmptyExecToolResultText } from "../exec-tool-result-normalize";
|
|
24
|
+
import { identifyRoutedModel } from "../identity";
|
|
25
|
+
import { extractKiroImages, type KiroImage } from "../kiro-images";
|
|
26
|
+
import { convertKiroToolContext } from "../kiro-tools";
|
|
27
|
+
import { createKiroToolNameRegistry, mapModelId, normalizeToolId, stableConversationId } from "../kiro-wire";
|
|
28
|
+
import { buildNonOpenAIToolCatalogNudgeFromNames, isBareShellBridgeTool, isCodexCodeModeExecTool } from "../tool-catalog-nudge";
|
|
29
|
+
import {
|
|
30
|
+
appendTurnText,
|
|
31
|
+
hasTrailingDeliveredFinalAnswer,
|
|
32
|
+
validateKiroCapabilities,
|
|
33
|
+
validateKiroConversationState,
|
|
34
|
+
type KiroTurn,
|
|
35
|
+
} from "./conversation";
|
|
36
|
+
import { injectKiroThinkingTags, kiroNativeEffortField, KIRO_NATIVE_EFFORTS } from "./reasoning";
|
|
37
|
+
import { kiroPayloadMessages, userContentText } from "./usage";
|
|
38
|
+
import {
|
|
39
|
+
kiroToolWireNames,
|
|
40
|
+
type KiroHistoryEntry,
|
|
41
|
+
type KiroToolResult,
|
|
42
|
+
type KiroToolUse,
|
|
43
|
+
type KiroWireClient,
|
|
44
|
+
} from "./wire";
|
|
45
|
+
|
|
46
|
+
export function boundedInjectedInstruction(text: string, used: { value: number }): string | undefined {
|
|
47
|
+
const remaining = MAX_KIRO_INJECTED_INSTRUCTION_CHARS - used.value;
|
|
48
|
+
if (remaining <= 0 || !text) return undefined;
|
|
49
|
+
let result = text.length <= remaining ? text : text.slice(0, remaining);
|
|
50
|
+
// Never end the slice on a lone high surrogate: encoding it substitutes
|
|
51
|
+
// U+FFFD into the injected instruction. One step back keeps a valid pair
|
|
52
|
+
// out instead of a broken half.
|
|
53
|
+
if (result.length > 0) {
|
|
54
|
+
const last = result.charCodeAt(result.length - 1);
|
|
55
|
+
if (last >= 0xd800 && last <= 0xdbff) result = result.slice(0, -1);
|
|
56
|
+
}
|
|
57
|
+
used.value += result.length;
|
|
58
|
+
return result.length > 0 ? result : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Test-only: exercise the surrogate-safe instruction bound directly. */
|
|
62
|
+
export function boundedInjectedInstructionForTests(text: string, used: { value: number }): string | undefined {
|
|
63
|
+
return boundedInjectedInstruction(text, used);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function kiroCompletionTool(): Record<string, unknown> {
|
|
67
|
+
return {
|
|
68
|
+
toolSpecification: {
|
|
69
|
+
name: KIRO_COMPLETION_TOOL_NAME,
|
|
70
|
+
// The shared tool-catalog nudge enumerates this name next to ordinary tools and tells every
|
|
71
|
+
// listed name to count a call only after its tool result returns. Nothing returns a result
|
|
72
|
+
// here: a valid call becomes the turn's terminal. Left undescribed, the model reads one more
|
|
73
|
+
// deferrable work tool and keeps calling tools with a finished answer already written as
|
|
74
|
+
// commentary. So the description states the distinction, the obligation, and the terminality
|
|
75
|
+
// where the model is actually choosing between tools.
|
|
76
|
+
//
|
|
77
|
+
// It also has to name the blocked-on-user state, for the same reason the prose contract does.
|
|
78
|
+
// This is the surface the model reads while CHOOSING; if it admits only "fully complete", a
|
|
79
|
+
// model holding a question that blocks progress reads this tool as unavailable and keeps
|
|
80
|
+
// working instead, which is the measured defect. The two surfaces must not disagree.
|
|
81
|
+
description: "Terminal completion channel, not an ordinary work tool. When the task is fully complete and no more work or tool calls are needed, you must call this tool exactly once instead of providing the final answer as ordinary assistant text. Call it the same way when you cannot continue until the user supplies a decision, information, or a clarification that only they can give: the question itself is the answer. Put the complete user-facing final answer in `answer`. The call is complete when issued: it ends the turn, returns no tool result, and no text or tool call may follow it.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
json: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
answer: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "The complete final answer to show the user, or the blocking question you need the user to answer before you can continue.",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ["answer"],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildKiroPayload(
|
|
99
|
+
parsed: OcxParsedRequest,
|
|
100
|
+
profileArn: string | undefined,
|
|
101
|
+
forcedCompletionMode?: KiroCompletionMode,
|
|
102
|
+
wireClient: KiroWireClient = "ide",
|
|
103
|
+
): {
|
|
104
|
+
payload: Record<string, unknown>;
|
|
105
|
+
nameMap: Map<string, string>;
|
|
106
|
+
conversationId: string;
|
|
107
|
+
completionMode: KiroCompletionMode;
|
|
108
|
+
} {
|
|
109
|
+
validateKiroCapabilities(parsed);
|
|
110
|
+
const modelId = mapModelId(parsed.modelId);
|
|
111
|
+
const registry = createKiroToolNameRegistry();
|
|
112
|
+
const toolContext = convertKiroToolContext(parsed, registry);
|
|
113
|
+
const ordinaryTools = toolContext.tools;
|
|
114
|
+
// A turn whose history already ENDS with a delivered final answer has nothing to complete.
|
|
115
|
+
// Leaving completion "required" here would keep advertising codex_kiro_final_answer with its
|
|
116
|
+
// instructions, so the model answers again, or replies with ordinary text and trips the
|
|
117
|
+
// `needsFallback` retry, which ends its payload with KIRO_COMPLETION_RETRY_MESSAGE and reopens
|
|
118
|
+
// the finished task. Suppressing the mode is what actually closes that loop; the neutral
|
|
119
|
+
// acknowledgement below only stops the resume wording.
|
|
120
|
+
//
|
|
121
|
+
// Read from parsed messages because `completionMode` is needed to build the tool catalog, which
|
|
122
|
+
// happens before the turn list exists. `forcedCompletionMode` still wins: the fallback retry
|
|
123
|
+
// passes "text_fallback" explicitly and must not be silently downgraded.
|
|
124
|
+
const trailingDeliveredAnswer = hasTrailingDeliveredFinalAnswer(kiroPayloadMessages(parsed), parsed);
|
|
125
|
+
const completionMode: KiroCompletionMode = forcedCompletionMode
|
|
126
|
+
?? (ordinaryTools.length > 0 && !trailingDeliveredAnswer ? "required" : "disabled");
|
|
127
|
+
const kiroTools = completionMode === "disabled"
|
|
128
|
+
? ordinaryTools
|
|
129
|
+
: [...ordinaryTools, kiroCompletionTool()];
|
|
130
|
+
const nameMap = toolContext.nameMap;
|
|
131
|
+
const systemParts: string[] = [];
|
|
132
|
+
const injectedChars = { value: 0 };
|
|
133
|
+
// Name the Kiro model id actually sent on the wire without leaking the proxy identity upstream.
|
|
134
|
+
if (parsed.context.systemPrompt?.length) {
|
|
135
|
+
systemParts.push(identifyRoutedModel(parsed.context.systemPrompt.join("\n\n"), modelId));
|
|
136
|
+
}
|
|
137
|
+
for (const addition of toolContext.systemAdditions) {
|
|
138
|
+
const boundedAddition = boundedInjectedInstruction(addition, injectedChars);
|
|
139
|
+
if (boundedAddition) systemParts.push(boundedAddition);
|
|
140
|
+
}
|
|
141
|
+
// Kiro renames tools to satisfy its wire constraints, so resolve neighbor names through the
|
|
142
|
+
// registry's existing aliases; a bare-name comparison would forbid tools this turn actually
|
|
143
|
+
// advertises. Read the recorded mapping instead of calling `alias()`, which would REGISTER a
|
|
144
|
+
// name for a tool that was never advertised and pollute the collision domain.
|
|
145
|
+
const advertisedAlias = new Map<string, string>();
|
|
146
|
+
for (const [alias, wireName] of registry.nameMap) advertisedAlias.set(wireName, alias);
|
|
147
|
+
// Code mode is decided on the EMITTED catalog, not the requested list.
|
|
148
|
+
//
|
|
149
|
+
// `freeform` only exists on the requested tool objects -- `kiroToolWireNames` has already
|
|
150
|
+
// reduced the emitted catalog to strings -- so the predicates must read the objects. But the
|
|
151
|
+
// SHAPE that matters is the one the model receives: the count/byte budget can drop a requested
|
|
152
|
+
// `exec_command` while `exec` survives, and scanning the requested list would then find a shell
|
|
153
|
+
// bridge the model cannot call and suppress code mode for a catalog that is code-mode-shaped.
|
|
154
|
+
// Intersecting the two keeps `tool_choice: "none"` and budget omission correct for free: both
|
|
155
|
+
// empty the emitted set, so nothing can be named.
|
|
156
|
+
const emittedToolNames = new Set(kiroToolWireNames(kiroTools));
|
|
157
|
+
const emittedAlias = (tool: OcxTool): string | undefined => {
|
|
158
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
159
|
+
// Read the recorded mapping; `registry.alias()` would REGISTER a name here.
|
|
160
|
+
const alias = advertisedAlias.get(wireName) ?? wireName;
|
|
161
|
+
return emittedToolNames.has(alias) ? alias : undefined;
|
|
162
|
+
};
|
|
163
|
+
const requestedTools = parsed.context.tools ?? [];
|
|
164
|
+
const emittedCodeModeExec = requestedTools.find(tool => isCodexCodeModeExecTool(tool) && emittedAlias(tool));
|
|
165
|
+
const emittedShellBridge = requestedTools.some(tool => isBareShellBridgeTool(tool) && emittedAlias(tool));
|
|
166
|
+
const codeModeExecName = emittedCodeModeExec && !emittedShellBridge
|
|
167
|
+
? emittedAlias(emittedCodeModeExec)
|
|
168
|
+
: undefined;
|
|
169
|
+
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeFromNames(
|
|
170
|
+
kiroToolWireNames(kiroTools),
|
|
171
|
+
name => advertisedAlias.get(name) ?? name,
|
|
172
|
+
codeModeExecName,
|
|
173
|
+
);
|
|
174
|
+
const boundedNudge = toolCatalogNudge ? boundedInjectedInstruction(toolCatalogNudge, injectedChars) : undefined;
|
|
175
|
+
if (boundedNudge) systemParts.push(boundedNudge);
|
|
176
|
+
if (completionMode !== "disabled") {
|
|
177
|
+
const boundedCompletion = boundedInjectedInstruction(KIRO_COMPLETION_INSTRUCTIONS, injectedChars);
|
|
178
|
+
if (boundedCompletion) systemParts.push(boundedCompletion);
|
|
179
|
+
}
|
|
180
|
+
const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
|
|
181
|
+
const turns: KiroTurn[] = [];
|
|
182
|
+
const priorCalls = new Map<string, { wireName: string; rawId: string }>();
|
|
183
|
+
const pushUser = (content: string, images: KiroImage[] = [], toolResults: KiroToolResult[] = []): void => {
|
|
184
|
+
const last = turns.at(-1);
|
|
185
|
+
if (last?.kind === "user") {
|
|
186
|
+
last.content = appendTurnText(last.content, content);
|
|
187
|
+
last.images.push(...images);
|
|
188
|
+
last.toolResults.push(...toolResults);
|
|
189
|
+
} else {
|
|
190
|
+
turns.push({ kind: "user", content, images: [...images], toolResults: [...toolResults] });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const pushAssistant = (content: string, toolUses: KiroToolUse[], redactedReasoning?: string, finalAnswer?: boolean): void => {
|
|
194
|
+
const last = turns.at(-1);
|
|
195
|
+
if (last?.kind === "assistant") {
|
|
196
|
+
last.content = appendTurnText(last.content, content);
|
|
197
|
+
last.toolUses.push(...toolUses);
|
|
198
|
+
// Merged turns keep the newest blob: it covers the reasoning up to the merged turn's end.
|
|
199
|
+
if (redactedReasoning) last.redactedReasoning = redactedReasoning;
|
|
200
|
+
// A merged turn is final only if its LAST component was: commentary appended after a final
|
|
201
|
+
// answer means the model kept working, so the turn is no longer terminal.
|
|
202
|
+
last.finalAnswer = finalAnswer === true;
|
|
203
|
+
} else {
|
|
204
|
+
turns.push({
|
|
205
|
+
kind: "assistant",
|
|
206
|
+
content,
|
|
207
|
+
toolUses: [...toolUses],
|
|
208
|
+
...(redactedReasoning ? { redactedReasoning } : {}),
|
|
209
|
+
...(finalAnswer ? { finalAnswer: true } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let adjacentResult: {
|
|
215
|
+
rawId: string;
|
|
216
|
+
result: KiroToolResult;
|
|
217
|
+
texts: string[];
|
|
218
|
+
count: number;
|
|
219
|
+
hasImages: boolean;
|
|
220
|
+
} | undefined;
|
|
221
|
+
const finishAdjacentResult = (): void => {
|
|
222
|
+
if (adjacentResult && adjacentResult.count > 1) {
|
|
223
|
+
if (adjacentResult.texts.some(text => text.trim())) {
|
|
224
|
+
adjacentResult.result.content = adjacentResult.texts.map(text => ({ text }));
|
|
225
|
+
} else if (adjacentResult.hasImages || adjacentResult.result.status === "error") {
|
|
226
|
+
adjacentResult.result.content = [{ text: KIRO_EMPTY_TOOL_RESULT_MESSAGE }];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
adjacentResult = undefined;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
for (const msg of kiroPayloadMessages(parsed)) {
|
|
233
|
+
// Original-message adjacency matters even when a turn is collapsed or skipped below.
|
|
234
|
+
if (msg.role !== "toolResult") finishAdjacentResult();
|
|
235
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
236
|
+
const text = userContentText((msg as { content: string | OcxContentPart[] }).content);
|
|
237
|
+
const images = extractKiroImages((msg as { content: string | OcxContentPart[] }).content);
|
|
238
|
+
pushUser(text, images);
|
|
239
|
+
} else if (msg.role === "assistant") {
|
|
240
|
+
const aMsg = msg as OcxAssistantMessage;
|
|
241
|
+
const text = (aMsg.content || [])
|
|
242
|
+
.filter((b): b is OcxTextContent => b.type === "text")
|
|
243
|
+
.map(b => b.text)
|
|
244
|
+
.join("");
|
|
245
|
+
const toolCalls = (aMsg.content || [])
|
|
246
|
+
.filter((b): b is OcxToolCall => b.type === "toolCall");
|
|
247
|
+
const toolUses: KiroToolUse[] = toolCalls.map(tc => {
|
|
248
|
+
const toolUseId = normalizeToolId(tc.id);
|
|
249
|
+
if (!toolUseId) throw new Error("Kiro history contains a tool call with an empty id");
|
|
250
|
+
if (priorCalls.has(toolUseId)) throw new Error(`Kiro history contains duplicate tool call id ${JSON.stringify(tc.id)}`);
|
|
251
|
+
const wireName = namespacedToolName(tc.namespace, tc.name);
|
|
252
|
+
const name = registry.alias(wireName);
|
|
253
|
+
priorCalls.set(toolUseId, { wireName, rawId: tc.id });
|
|
254
|
+
return { name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
|
|
255
|
+
});
|
|
256
|
+
if (!text && toolUses.length === 0) {
|
|
257
|
+
const hasReasoning = aMsg.content.some(part => part.type === "thinking" && part.thinking.trim());
|
|
258
|
+
if (hasReasoning) continue;
|
|
259
|
+
}
|
|
260
|
+
// `phase` survives the Responses round trip (parser.ts assistant branch), so a replayed
|
|
261
|
+
// final answer is identifiable here rather than guessed from turn position.
|
|
262
|
+
pushAssistant(text, toolUses, aMsg.kiroRedactedReasoning, aMsg.phase === "final_answer" && toolUses.length === 0);
|
|
263
|
+
} else if (msg.role === "toolResult") {
|
|
264
|
+
const tr = msg as OcxToolResultMessage;
|
|
265
|
+
if (tr.containsEncryptedContent) {
|
|
266
|
+
throw new Error(`Kiro cannot translate encrypted output for tool call ${JSON.stringify(tr.toolCallId)}`);
|
|
267
|
+
}
|
|
268
|
+
const text = userContentText(tr.content);
|
|
269
|
+
// An empty code-mode exec result needs the SPECIFIC reason, not the generic fallback: the
|
|
270
|
+
// model otherwise reads a blank result, concludes its earlier context was lost, and restarts
|
|
271
|
+
// the task instead of calling text()/notify(). Checked before `text.trim()` because the
|
|
272
|
+
// wrapper form ("Script completed\nWall time ...\nOutput:\n") is non-blank and would
|
|
273
|
+
// otherwise pass through as if it were real output.
|
|
274
|
+
const execOptions = { toolName: tr.toolName, toolNamespace: tr.toolNamespace };
|
|
275
|
+
const normalizedExecText = normalizeEmptyExecToolResultText(text, execOptions);
|
|
276
|
+
// A host failure string inside a non-empty exec result gets the rule it broke appended, but
|
|
277
|
+
// only when this request's emitted catalog is genuinely code mode (`codeModeExecName` above):
|
|
278
|
+
// a structured tool named exec, or exec beside a shell bridge, never ran the isolate. This is
|
|
279
|
+
// the only substitution the grouping path below also carries: whitespace and empty/failed
|
|
280
|
+
// wrappers keep their existing raw policy.
|
|
281
|
+
const annotatedExecText = normalizedExecText === undefined && codeModeExecName !== undefined
|
|
282
|
+
? annotateCodeModeHostFailure(text, execOptions)
|
|
283
|
+
: undefined;
|
|
284
|
+
const resultText = normalizedExecText ?? annotatedExecText ?? (text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE);
|
|
285
|
+
const images = extractKiroImages(tr.content);
|
|
286
|
+
const toolUseId = normalizeToolId(tr.toolCallId);
|
|
287
|
+
const call = priorCalls.get(toolUseId);
|
|
288
|
+
if (!call || call.rawId !== tr.toolCallId) {
|
|
289
|
+
throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`);
|
|
290
|
+
}
|
|
291
|
+
// Keep real whitespace and failed wrappers, but no empty-success wrapper boilerplate.
|
|
292
|
+
const rawGroupText = text.length > 0 && (!text.trim() || normalizedExecText !== EMPTY_EXEC_OUTPUT_MESSAGE)
|
|
293
|
+
? (annotatedExecText ?? text) : undefined;
|
|
294
|
+
const last = turns.at(-1);
|
|
295
|
+
if (
|
|
296
|
+
adjacentResult?.rawId === tr.toolCallId
|
|
297
|
+
&& last?.kind === "user"
|
|
298
|
+
&& last.toolResults.at(-1) === adjacentResult.result
|
|
299
|
+
) {
|
|
300
|
+
adjacentResult.count += 1;
|
|
301
|
+
adjacentResult.hasImages ||= images.length > 0;
|
|
302
|
+
if (rawGroupText !== undefined) adjacentResult.texts.push(rawGroupText);
|
|
303
|
+
last.images.push(...images);
|
|
304
|
+
if (tr.isError) adjacentResult.result.status = "error";
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
finishAdjacentResult();
|
|
308
|
+
// Carrier text is a placeholder for an OTHERWISE EMPTY tool-result turn, not a prefix.
|
|
309
|
+
// Passing it here would push proxy filler AHEAD of a human instruction that Claude Code
|
|
310
|
+
// sends in the same turn (mid-turn steering / queued_command, issue #543), burying the
|
|
311
|
+
// newest user intent behind boilerplate. Backfill below only when nothing else speaks.
|
|
312
|
+
const result: KiroToolResult = {
|
|
313
|
+
content: [{ text: resultText }],
|
|
314
|
+
status: tr.isError ? "error" : "success",
|
|
315
|
+
toolUseId,
|
|
316
|
+
};
|
|
317
|
+
pushUser("", images, [result]);
|
|
318
|
+
adjacentResult = {
|
|
319
|
+
rawId: tr.toolCallId, result,
|
|
320
|
+
texts: rawGroupText === undefined ? [] : [rawGroupText],
|
|
321
|
+
count: 1, hasImages: images.length > 0,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
finishAdjacentResult();
|
|
326
|
+
|
|
327
|
+
if (turns.length === 0 || turns[0].kind === "assistant") {
|
|
328
|
+
turns.unshift({ kind: "user", content: KIRO_CONTINUATION_MESSAGE, images: [], toolResults: [] });
|
|
329
|
+
}
|
|
330
|
+
// Kiro requires the request to end with a user turn, so a trailing assistant turn always gets
|
|
331
|
+
// one appended (the pop below throws otherwise). What that turn SAYS is the load-bearing part.
|
|
332
|
+
//
|
|
333
|
+
// Normally a trailing assistant turn means the model stopped mid-task, and a continuation/retry
|
|
334
|
+
// prompt is correct. A DELIVERED final answer is the exception: the turn already ended, and
|
|
335
|
+
// telling that model to "continue" or to call the completion tool again reopens finished work —
|
|
336
|
+
// the completed-task-behaves-like-an-open-goal loop. It gets a neutral acknowledgement instead:
|
|
337
|
+
// structurally valid, but carrying no instruction to resume.
|
|
338
|
+
const trailing = turns.at(-1);
|
|
339
|
+
if (trailing?.kind === "assistant") {
|
|
340
|
+
const resumeText = completionMode === "text_fallback" ? KIRO_COMPLETION_RETRY_MESSAGE : KIRO_CONTINUATION_MESSAGE;
|
|
341
|
+
turns.push({
|
|
342
|
+
kind: "user",
|
|
343
|
+
content: trailing.finalAnswer ? KIRO_ANSWER_DELIVERED_MESSAGE : resumeText,
|
|
344
|
+
images: [],
|
|
345
|
+
toolResults: [],
|
|
346
|
+
...(trailing.finalAnswer ? { answerDeliveredAck: true } : {}),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Give tool-result turns a carrier sentence ONLY when they carry no other text. This runs
|
|
351
|
+
// before the pop below so the current turn is covered too: skipping it there would ship an
|
|
352
|
+
// empty current content, which validateKiroConversationState accepts (tool results count as
|
|
353
|
+
// payload) and would therefore fail silently.
|
|
354
|
+
for (const turn of turns) {
|
|
355
|
+
if (turn.kind === "user" && !turn.content.trim() && turn.toolResults.length > 0) {
|
|
356
|
+
turn.content = KIRO_TOOL_RESULT_CARRIER_MESSAGE;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const currentTurn = turns.pop();
|
|
361
|
+
if (!currentTurn || currentTurn.kind !== "user") throw new Error("Kiro request must end with a user turn");
|
|
362
|
+
// Survives the pop as state, so the checks below never infer intent from user-supplied text.
|
|
363
|
+
const answerDeliveredAck = currentTurn.answerDeliveredAck === true;
|
|
364
|
+
const toEntry = (turn: KiroTurn): KiroHistoryEntry => turn.kind === "assistant"
|
|
365
|
+
? {
|
|
366
|
+
assistantResponseMessage: {
|
|
367
|
+
content: turn.content,
|
|
368
|
+
...(turn.toolUses.length > 0 ? { toolUses: turn.toolUses } : {}),
|
|
369
|
+
...(turn.redactedReasoning ? { reasoningContent: { redactedContent: turn.redactedReasoning } } : {}),
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
: {
|
|
373
|
+
userInputMessage: {
|
|
374
|
+
content: turn.content,
|
|
375
|
+
modelId,
|
|
376
|
+
origin: wireClient === "cli" ? "KIRO_CLI" : "AI_EDITOR",
|
|
377
|
+
...(turn.images.length > 0 ? { images: turn.images } : {}),
|
|
378
|
+
...(turn.toolResults.length > 0 ? { userInputMessageContext: { toolResults: turn.toolResults } } : {}),
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
const history = turns.map(toEntry);
|
|
382
|
+
const currentEntry = toEntry(currentTurn);
|
|
383
|
+
const currentUim = currentEntry.userInputMessage!;
|
|
384
|
+
|
|
385
|
+
if (systemPrefix) {
|
|
386
|
+
const firstUser = history.find(e => e.userInputMessage)?.userInputMessage;
|
|
387
|
+
if (firstUser) firstUser.content = systemPrefix + firstUser.content;
|
|
388
|
+
else currentUim.content = systemPrefix + currentUim.content;
|
|
389
|
+
}
|
|
390
|
+
if (kiroTools.length > 0) {
|
|
391
|
+
currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools };
|
|
392
|
+
}
|
|
393
|
+
if (completionMode === "text_fallback") {
|
|
394
|
+
// Never append the retry instruction onto the answer-delivered acknowledgement: it exists
|
|
395
|
+
// precisely to avoid asking a finished turn for another completion call, and appending here
|
|
396
|
+
// would reinstate the loop it prevents.
|
|
397
|
+
if (currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE && !answerDeliveredAck) {
|
|
398
|
+
currentUim.content = appendTurnText(currentUim.content, KIRO_COMPLETION_RETRY_MESSAGE);
|
|
399
|
+
}
|
|
400
|
+
} else if (
|
|
401
|
+
!currentUim.userInputMessageContext?.toolResults
|
|
402
|
+
&& currentUim.content !== KIRO_CONTINUATION_MESSAGE
|
|
403
|
+
&& !answerDeliveredAck
|
|
404
|
+
) {
|
|
405
|
+
currentUim.content = injectKiroThinkingTags(currentUim.content, parsed);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
validateKiroConversationState(history, currentEntry);
|
|
409
|
+
const conversationId = stableConversationId(parsed);
|
|
410
|
+
const payload: Record<string, unknown> = {
|
|
411
|
+
conversationState: {
|
|
412
|
+
chatTriggerType: "MANUAL",
|
|
413
|
+
...(wireClient === "cli" ? {
|
|
414
|
+
agentContinuationId: crypto.randomUUID(),
|
|
415
|
+
agentTaskType: "vibe",
|
|
416
|
+
} : {}),
|
|
417
|
+
conversationId,
|
|
418
|
+
currentMessage: { userInputMessage: currentUim },
|
|
419
|
+
...(history.length > 0 ? { history } : {}),
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
const effort = parsed.options.reasoning;
|
|
423
|
+
const effortField = kiroNativeEffortField(parsed.modelId);
|
|
424
|
+
if (effortField && effort && effort !== "none") {
|
|
425
|
+
if (!KIRO_NATIVE_EFFORTS.includes(effort)) {
|
|
426
|
+
throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`);
|
|
427
|
+
}
|
|
428
|
+
payload.additionalModelRequestFields = { [effortField]: { effort } };
|
|
429
|
+
}
|
|
430
|
+
if (profileArn) payload.profileArn = profileArn;
|
|
431
|
+
return { payload, nameMap, conversationId, completionMode };
|
|
432
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { normalizeKiroModelId } from "../../providers/kiro-models";
|
|
2
|
+
import type { OcxParsedRequest } from "../../types";
|
|
3
|
+
|
|
4
|
+
export type KiroReasoningMode = "native" | "emulated";
|
|
5
|
+
|
|
6
|
+
// Kiro takes a verified native effort field for these models, and each model family names it
|
|
7
|
+
// differently: the Sol-only `reasoning.effort` versus the Claude-specific `output_config.effort`.
|
|
8
|
+
// Models absent from this table fall back to emulated thinking instructions.
|
|
9
|
+
export const KIRO_NATIVE_EFFORT_FIELDS: Record<string, "reasoning" | "output_config"> = {
|
|
10
|
+
"gpt-5.6-sol": "reasoning",
|
|
11
|
+
"claude-opus-5": "output_config",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const KIRO_NATIVE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
15
|
+
|
|
16
|
+
export function kiroNativeEffortField(modelId: string): "reasoning" | "output_config" | undefined {
|
|
17
|
+
return KIRO_NATIVE_EFFORT_FIELDS[normalizeKiroModelId(modelId)];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function kiroReasoningMode(modelId: string): KiroReasoningMode {
|
|
21
|
+
return kiroNativeEffortField(modelId) ? "native" : "emulated";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined {
|
|
25
|
+
const effort = parsed.options.reasoning;
|
|
26
|
+
if (!effort || effort === "none") return undefined;
|
|
27
|
+
const maxTokens = parsed.options.maxOutputTokens || 4096;
|
|
28
|
+
const percent: Record<string, number> = {
|
|
29
|
+
minimal: 0.10,
|
|
30
|
+
low: 0.20,
|
|
31
|
+
medium: 0.50,
|
|
32
|
+
high: 0.80,
|
|
33
|
+
xhigh: 0.90,
|
|
34
|
+
max: 0.95,
|
|
35
|
+
};
|
|
36
|
+
const ratio = percent[effort];
|
|
37
|
+
return ratio === undefined ? undefined : Math.max(1, Math.floor(maxTokens * ratio));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest): string {
|
|
41
|
+
if (kiroReasoningMode(parsed.modelId) !== "emulated") return content;
|
|
42
|
+
const budget = kiroThinkingBudget(parsed);
|
|
43
|
+
if (!budget) return content;
|
|
44
|
+
const instruction = [
|
|
45
|
+
"Think in English for better reasoning quality.",
|
|
46
|
+
"Be thorough and systematic, consider edge cases, challenge assumptions, and verify reasoning before answering.",
|
|
47
|
+
"After thinking, respond in the user's language.",
|
|
48
|
+
].join("\n");
|
|
49
|
+
return [
|
|
50
|
+
"<thinking_mode>enabled</thinking_mode>",
|
|
51
|
+
`<max_thinking_length>${budget}</max_thinking_length>`,
|
|
52
|
+
`<thinking_instruction>${instruction}</thinking_instruction>`,
|
|
53
|
+
"",
|
|
54
|
+
content,
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|