@kaelio/ktx 0.7.0 → 0.9.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.
Files changed (142) hide show
  1. package/assets/python/{kaelio_ktx-0.7.0-py3-none-any.whl → kaelio_ktx-0.9.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cli-program.js +7 -0
  5. package/dist/cli-runtime.js +50 -3
  6. package/dist/command-schemas.d.ts +1 -1
  7. package/dist/command-tree.js +5 -1
  8. package/dist/commands/completion-commands.d.ts +3 -0
  9. package/dist/commands/completion-commands.js +38 -0
  10. package/dist/commands/ingest-commands.js +0 -4
  11. package/dist/commands/knowledge-commands.js +15 -2
  12. package/dist/commands/setup-commands.js +3 -3
  13. package/dist/commands/sl-commands.js +19 -7
  14. package/dist/completion/complete-engine.d.ts +19 -0
  15. package/dist/completion/complete-engine.js +128 -0
  16. package/dist/completion/completion-scripts.d.ts +1 -0
  17. package/dist/completion/completion-scripts.js +36 -0
  18. package/dist/completion/dynamic-candidates.d.ts +6 -0
  19. package/dist/completion/dynamic-candidates.js +98 -0
  20. package/dist/connection-drivers.d.ts +3 -0
  21. package/dist/connection-drivers.js +17 -0
  22. package/dist/connection-recovery.d.ts +34 -0
  23. package/dist/connection-recovery.js +82 -0
  24. package/dist/connection.js +3 -1
  25. package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
  26. package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
  27. package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
  28. package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
  29. package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
  30. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +29 -0
  31. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +190 -0
  32. package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
  33. package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
  34. package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
  35. package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
  36. package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
  37. package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
  38. package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
  39. package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
  40. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  41. package/dist/context/ingest/ingest-bundle.runner.d.ts +8 -0
  42. package/dist/context/ingest/ingest-bundle.runner.js +72 -15
  43. package/dist/context/ingest/ingest-profile.d.ts +102 -0
  44. package/dist/context/ingest/ingest-profile.js +306 -0
  45. package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
  46. package/dist/context/ingest/isolated-diff/work-unit-executor.js +25 -2
  47. package/dist/context/ingest/local-adapters.js +21 -4
  48. package/dist/context/ingest/local-bundle-runtime.js +4 -2
  49. package/dist/context/ingest/local-ingest.d.ts +1 -1
  50. package/dist/context/ingest/local-ingest.js +6 -4
  51. package/dist/context/ingest/memory-flow/events.js +2 -1
  52. package/dist/context/ingest/ports.d.ts +2 -0
  53. package/dist/context/ingest/reports.d.ts +3 -0
  54. package/dist/context/ingest/reports.js +10 -0
  55. package/dist/context/ingest/stages/stage-3-work-units.d.ts +3 -1
  56. package/dist/context/ingest/stages/stage-3-work-units.js +2 -0
  57. package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +2 -1
  58. package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
  59. package/dist/context/ingest/tools/tool-call-logger.d.ts +6 -0
  60. package/dist/context/ingest/tools/tool-call-logger.js +36 -1
  61. package/dist/context/llm/ai-sdk-runtime.js +32 -3
  62. package/dist/context/llm/claude-code-runtime.js +35 -2
  63. package/dist/context/llm/codex-exec-events.d.ts +20 -0
  64. package/dist/context/llm/codex-exec-events.js +155 -0
  65. package/dist/context/llm/codex-isolation.d.ts +3 -0
  66. package/dist/context/llm/codex-isolation.js +5 -0
  67. package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
  68. package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
  69. package/dist/context/llm/codex-models.d.ts +2 -0
  70. package/dist/context/llm/codex-models.js +17 -0
  71. package/dist/context/llm/codex-runtime-config.d.ts +16 -0
  72. package/dist/context/llm/codex-runtime-config.js +19 -0
  73. package/dist/context/llm/codex-runtime.d.ts +37 -0
  74. package/dist/context/llm/codex-runtime.js +304 -0
  75. package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
  76. package/dist/context/llm/codex-sdk-runner.js +63 -0
  77. package/dist/context/llm/local-config.d.ts +2 -0
  78. package/dist/context/llm/local-config.js +12 -1
  79. package/dist/context/llm/runtime-port.d.ts +25 -0
  80. package/dist/context/mcp/context-tools.d.ts +2 -1
  81. package/dist/context/mcp/context-tools.js +82 -15
  82. package/dist/context/mcp/server.js +4 -0
  83. package/dist/context/mcp/types.d.ts +15 -1
  84. package/dist/context/project/config.d.ts +3 -0
  85. package/dist/context/project/config.js +6 -2
  86. package/dist/context/project/driver-schemas.js +1 -1
  87. package/dist/context/search/discover.js +4 -3
  88. package/dist/context/sl/local-sl.d.ts +15 -0
  89. package/dist/context/sl/local-sl.js +30 -0
  90. package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
  91. package/dist/context/sql-analysis/ports.d.ts +12 -2
  92. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  93. package/dist/context/wiki/local-knowledge.d.ts +10 -0
  94. package/dist/context/wiki/local-knowledge.js +22 -0
  95. package/dist/context-build-view.d.ts +0 -3
  96. package/dist/context-build-view.js +5 -39
  97. package/dist/ingest.js +7 -10
  98. package/dist/io/buffered-command-io.d.ts +11 -0
  99. package/dist/io/buffered-command-io.js +28 -0
  100. package/dist/knowledge.d.ts +5 -0
  101. package/dist/knowledge.js +10 -1
  102. package/dist/llm/types.d.ts +1 -1
  103. package/dist/local-adapters.d.ts +10 -2
  104. package/dist/local-adapters.js +19 -3
  105. package/dist/next-steps.js +1 -2
  106. package/dist/progress-port-adapter.d.ts +6 -0
  107. package/dist/progress-port-adapter.js +18 -0
  108. package/dist/public-ingest-copy.js +1 -1
  109. package/dist/public-ingest.d.ts +20 -8
  110. package/dist/public-ingest.js +198 -61
  111. package/dist/scan.js +3 -1
  112. package/dist/setup-context.d.ts +2 -0
  113. package/dist/setup-context.js +138 -64
  114. package/dist/setup-databases.d.ts +17 -1
  115. package/dist/setup-databases.js +366 -326
  116. package/dist/setup-models.d.ts +10 -1
  117. package/dist/setup-models.js +90 -2
  118. package/dist/setup-ready-menu.d.ts +16 -2
  119. package/dist/setup-ready-menu.js +37 -5
  120. package/dist/setup-sources.js +141 -33
  121. package/dist/setup.js +24 -12
  122. package/dist/skills/analytics/SKILL.md +6 -1
  123. package/dist/sl.d.ts +6 -1
  124. package/dist/sl.js +32 -8
  125. package/dist/status-project.d.ts +11 -0
  126. package/dist/status-project.js +50 -1
  127. package/dist/telemetry/command-hook.d.ts +1 -0
  128. package/dist/telemetry/command-hook.js +3 -1
  129. package/dist/telemetry/emitter.js +1 -1
  130. package/dist/telemetry/events.d.ts +15 -9
  131. package/dist/telemetry/events.js +17 -5
  132. package/dist/telemetry/identity.d.ts +1 -2
  133. package/dist/telemetry/identity.js +13 -10
  134. package/dist/telemetry/index.d.ts +13 -1
  135. package/dist/telemetry/index.js +18 -3
  136. package/dist/telemetry/scrubber.d.ts +10 -0
  137. package/dist/telemetry/scrubber.js +20 -0
  138. package/package.json +20 -19
  139. package/dist/ingest-depth.d.ts +0 -8
  140. package/dist/ingest-depth.js +0 -56
  141. package/dist/setup-database-context-depth.d.ts +0 -23
  142. package/dist/setup-database-context-depth.js +0 -84
@@ -0,0 +1,155 @@
1
+ function record(value) {
2
+ return value && typeof value === 'object' ? value : undefined;
3
+ }
4
+ /**
5
+ * Codex thread items that represent a discrete agent action consuming one loop
6
+ * step. The step budget caps the total number of these regardless of which
7
+ * capability the agent reaches for, so built-in `command_execution` (and any
8
+ * file/web action the public Codex surface still exposes) count alongside our
9
+ * own `mcp_tool_call` items rather than only the MCP ones.
10
+ */
11
+ const AGENT_STEP_ITEM_TYPES = new Set(['command_execution', 'mcp_tool_call', 'file_change', 'web_search']);
12
+ export function isCompletedAgentStep(event) {
13
+ const eventRecord = record(event);
14
+ if (eventRecord?.type !== 'item.completed') {
15
+ return false;
16
+ }
17
+ const itemType = record(eventRecord.item)?.type;
18
+ return typeof itemType === 'string' && AGENT_STEP_ITEM_TYPES.has(itemType);
19
+ }
20
+ function text(value) {
21
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
22
+ }
23
+ function numberValue(value) {
24
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
25
+ }
26
+ function usageFrom(value) {
27
+ const usage = record(value);
28
+ if (!usage) {
29
+ return {};
30
+ }
31
+ const inputTokens = numberValue(usage.input_tokens ?? usage.inputTokens);
32
+ const outputTokens = numberValue(usage.output_tokens ?? usage.outputTokens);
33
+ const explicitTotalTokens = numberValue(usage.total_tokens ?? usage.totalTokens);
34
+ const totalTokens = explicitTotalTokens ??
35
+ (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
36
+ return {
37
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
38
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
39
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
40
+ };
41
+ }
42
+ function stopReasonFrom(value) {
43
+ const reason = text(value)?.toLowerCase();
44
+ if (reason && /(budget|max_turn|max-turn|limit)/.test(reason)) {
45
+ return 'budget';
46
+ }
47
+ return 'natural';
48
+ }
49
+ function errorMessageFrom(value) {
50
+ if (value instanceof Error) {
51
+ return value.message;
52
+ }
53
+ const asRecord = record(value);
54
+ const message = text(asRecord?.message);
55
+ return message ?? text(value) ?? 'Codex turn failed';
56
+ }
57
+ /**
58
+ * Codex serializes API failures as a JSON envelope inside the event message
59
+ * (e.g. `{"type":"error","status":400,"error":{"message":"…"}}`). Surface the
60
+ * human-readable inner message so callers don't leak raw JSON; pass plain
61
+ * strings through unchanged.
62
+ */
63
+ function unwrapCodexApiErrorMessage(raw) {
64
+ const trimmed = raw.trim();
65
+ if (!trimmed.startsWith('{')) {
66
+ return raw;
67
+ }
68
+ try {
69
+ const parsed = record(JSON.parse(trimmed));
70
+ return text(record(parsed?.error)?.message) ?? text(parsed?.message) ?? raw;
71
+ }
72
+ catch {
73
+ return raw;
74
+ }
75
+ }
76
+ /** @internal */
77
+ export function parseCodexExecEventLine(line) {
78
+ try {
79
+ return JSON.parse(line);
80
+ }
81
+ catch (error) {
82
+ throw new Error(`Codex JSONL event stream was malformed: ${error instanceof Error ? error.message : String(error)}`);
83
+ }
84
+ }
85
+ export function summarizeCodexExecEvents(events, options = {}) {
86
+ const startedAt = options.startedAt ?? Date.now();
87
+ const now = options.now ?? Date.now;
88
+ let finalText = '';
89
+ let stopReason = 'natural';
90
+ let usage = {};
91
+ let turnCount = 0;
92
+ let completedStepCount = 0;
93
+ const stepBoundariesMs = [];
94
+ let toolCallCount = 0;
95
+ const toolFailures = [];
96
+ let error;
97
+ for (const event of events) {
98
+ const eventRecord = record(event);
99
+ const eventType = text(eventRecord?.type);
100
+ if (!eventRecord || !eventType) {
101
+ continue;
102
+ }
103
+ if (eventType === 'turn.started') {
104
+ turnCount += 1;
105
+ continue;
106
+ }
107
+ const item = record(eventRecord.item);
108
+ const itemType = text(item?.type);
109
+ if (eventType === 'item.started' && itemType === 'mcp_tool_call') {
110
+ toolCallCount += 1;
111
+ continue;
112
+ }
113
+ if (isCompletedAgentStep(event)) {
114
+ completedStepCount += 1;
115
+ stepBoundariesMs.push(now() - startedAt);
116
+ // Only MCP tool calls fail the loop: a non-zero `command_execution` exit
117
+ // is normal agent exploration, not a runtime error. `status` is the
118
+ // authoritative signal (the SDK always sets it); the SDK also serializes
119
+ // `error: null` on successful calls, so an explicit-null `error` must NOT
120
+ // be read as a failure — only a populated error object counts.
121
+ if (itemType === 'mcp_tool_call' && (item?.status === 'failed' || (item?.error !== undefined && item?.error !== null))) {
122
+ const name = text(item?.name) ?? text(item?.tool) ?? text(item?.tool_name) ?? 'unknown';
123
+ toolFailures.push(`${name}: ${errorMessageFrom(item?.error)}`);
124
+ }
125
+ continue;
126
+ }
127
+ if (eventType === 'item.completed' && itemType === 'agent_message') {
128
+ finalText = text(item?.text) ?? finalText;
129
+ continue;
130
+ }
131
+ if (eventType === 'turn.completed') {
132
+ usage = usageFrom(eventRecord.usage);
133
+ if (completedStepCount === 0) {
134
+ stepBoundariesMs.push(now() - startedAt);
135
+ }
136
+ stopReason = stopReasonFrom(eventRecord.reason ?? eventRecord.stop_reason ?? eventRecord.terminal_reason);
137
+ continue;
138
+ }
139
+ if (eventType === 'turn.failed' || eventType === 'error') {
140
+ stopReason = 'error';
141
+ error = new Error(unwrapCodexApiErrorMessage(errorMessageFrom(eventRecord.error ?? eventRecord.message)));
142
+ continue;
143
+ }
144
+ }
145
+ return {
146
+ finalText,
147
+ stopReason,
148
+ usage,
149
+ stepCount: completedStepCount > 0 ? completedStepCount : turnCount,
150
+ stepBoundariesMs,
151
+ toolCallCount,
152
+ toolFailures,
153
+ ...(error ? { error } : {}),
154
+ };
155
+ }
@@ -0,0 +1,3 @@
1
+ export declare const CODEX_ISOLATION_WARNING = "Codex backend isolation is limited by the public Codex SDK/CLI surface: ktx restricts the runtime MCP server to the current ktx tool set, disables Codex web search, asks for a read-only sandbox, and sets approval_policy=never, but Codex may still load user Codex config and built-in command execution or read-only file capabilities.";
2
+ export declare const CODEX_ISOLATION_WARNING_FIX = "Use llm.provider.backend: claude-code when you need stricter Claude-Code-style runtime tool isolation, or remove host Codex MCP/tool config before running untrusted prompts through the codex backend.";
3
+ export declare function formatCodexIsolationWarning(): string;
@@ -0,0 +1,5 @@
1
+ export const CODEX_ISOLATION_WARNING = 'Codex backend isolation is limited by the public Codex SDK/CLI surface: ktx restricts the runtime MCP server to the current ktx tool set, disables Codex web search, asks for a read-only sandbox, and sets approval_policy=never, but Codex may still load user Codex config and built-in command execution or read-only file capabilities.';
2
+ export const CODEX_ISOLATION_WARNING_FIX = 'Use llm.provider.backend: claude-code when you need stricter Claude-Code-style runtime tool isolation, or remove host Codex MCP/tool config before running untrusted prompts through the codex backend.';
3
+ export function formatCodexIsolationWarning() {
4
+ return `${CODEX_ISOLATION_WARNING} ${CODEX_ISOLATION_WARNING_FIX}`;
5
+ }
@@ -0,0 +1,24 @@
1
+ import type { KtxMcpServerLike } from '../mcp/types.js';
2
+ import { runKtxMcpHttpServer } from '../../mcp-http-server.js';
3
+ import type { KtxRuntimeToolSet } from './runtime-port.js';
4
+ /** @internal */
5
+ export interface CreateCodexRuntimeMcpServerInput {
6
+ server?: KtxMcpServerLike;
7
+ toolSet: KtxRuntimeToolSet;
8
+ }
9
+ export interface CodexRuntimeMcpServerHandle {
10
+ url: string;
11
+ bearerTokenEnvVar: 'KTX_CODEX_RUNTIME_MCP_TOKEN';
12
+ bearerToken: string;
13
+ close(): Promise<void>;
14
+ }
15
+ type RunServer = typeof runKtxMcpHttpServer;
16
+ export interface StartCodexRuntimeMcpServerInput {
17
+ projectDir: string;
18
+ toolSet: KtxRuntimeToolSet;
19
+ runServer?: RunServer;
20
+ }
21
+ /** @internal */
22
+ export declare function createCodexRuntimeMcpServer(input: CreateCodexRuntimeMcpServerInput): KtxMcpServerLike;
23
+ export declare function startCodexRuntimeMcpServer(input: StartCodexRuntimeMcpServerInput): Promise<CodexRuntimeMcpServerHandle>;
24
+ export {};
@@ -0,0 +1,51 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { runKtxMcpHttpServer } from '../../mcp-http-server.js';
4
+ import { normalizeKtxRuntimeToolOutput } from './runtime-tools.js';
5
+ /** @internal */
6
+ export function createCodexRuntimeMcpServer(input) {
7
+ const server = input.server ??
8
+ new McpServer({
9
+ name: 'ktx-runtime',
10
+ version: '0.0.0',
11
+ });
12
+ for (const descriptor of Object.values(input.toolSet)) {
13
+ server.registerTool(descriptor.name, {
14
+ description: descriptor.description,
15
+ inputSchema: descriptor.inputSchema.shape,
16
+ }, async (toolInput) => {
17
+ const normalized = normalizeKtxRuntimeToolOutput(await descriptor.execute(toolInput));
18
+ return {
19
+ content: [{ type: 'text', text: normalized.markdown }],
20
+ ...(normalized.structured !== undefined && normalized.structured !== null && typeof normalized.structured === 'object'
21
+ ? { structuredContent: normalized.structured }
22
+ : {}),
23
+ };
24
+ });
25
+ }
26
+ return server;
27
+ }
28
+ function serverPort(server, fallback) {
29
+ const address = server.address();
30
+ return typeof address === 'object' && address ? address.port : fallback;
31
+ }
32
+ export async function startCodexRuntimeMcpServer(input) {
33
+ const bearerToken = randomBytes(32).toString('hex');
34
+ const runServer = input.runServer ?? runKtxMcpHttpServer;
35
+ const handle = (await runServer({
36
+ projectDir: input.projectDir,
37
+ host: '127.0.0.1',
38
+ port: 0,
39
+ token: bearerToken,
40
+ allowedHosts: ['127.0.0.1', 'localhost'],
41
+ allowedOrigins: [],
42
+ createMcpServer: () => createCodexRuntimeMcpServer({ toolSet: input.toolSet }),
43
+ }));
44
+ const port = serverPort(handle.server, 0);
45
+ return {
46
+ url: `http://127.0.0.1:${port}/mcp`,
47
+ bearerTokenEnvVar: 'KTX_CODEX_RUNTIME_MCP_TOKEN',
48
+ bearerToken,
49
+ close: () => handle.close(),
50
+ };
51
+ }
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_CODEX_MODEL = "gpt-5.5";
2
+ export declare function resolveCodexModel(model: string): string;
@@ -0,0 +1,17 @@
1
+ export const DEFAULT_CODEX_MODEL = 'gpt-5.5';
2
+ const CODEX_MODEL_ALIASES = {
3
+ codex: DEFAULT_CODEX_MODEL,
4
+ default: DEFAULT_CODEX_MODEL,
5
+ };
6
+ const EXPLICIT_CODEX_MODEL_ID = /^(?:gpt|codex)-[a-z0-9][a-z0-9._-]*$/i;
7
+ export function resolveCodexModel(model) {
8
+ const normalized = model.trim();
9
+ const alias = CODEX_MODEL_ALIASES[normalized];
10
+ if (alias) {
11
+ return alias;
12
+ }
13
+ if (EXPLICIT_CODEX_MODEL_ID.test(normalized)) {
14
+ return normalized;
15
+ }
16
+ throw new Error(`Unsupported Codex model "${model}". Use codex, default, or a gpt-* / codex-* model id.`);
17
+ }
@@ -0,0 +1,16 @@
1
+ interface CodexRuntimeMcpConfig {
2
+ url: string;
3
+ bearerTokenEnvVar: string;
4
+ bearerToken: string;
5
+ toolNames: string[];
6
+ }
7
+ export interface BuildCodexRuntimeConfigInput {
8
+ model: string;
9
+ mcp?: CodexRuntimeMcpConfig;
10
+ }
11
+ export interface CodexRuntimeConfig {
12
+ configOverrides: Record<string, unknown>;
13
+ env: Record<string, string>;
14
+ }
15
+ export declare function buildCodexRuntimeConfig(input: BuildCodexRuntimeConfigInput): CodexRuntimeConfig;
16
+ export {};
@@ -0,0 +1,19 @@
1
+ export function buildCodexRuntimeConfig(input) {
2
+ const configOverrides = {
3
+ history: { persistence: 'none' },
4
+ };
5
+ const env = {};
6
+ if (input.mcp) {
7
+ configOverrides.mcp_servers = {
8
+ ktx: {
9
+ url: input.mcp.url,
10
+ bearer_token_env_var: input.mcp.bearerTokenEnvVar,
11
+ enabled_tools: input.mcp.toolNames,
12
+ default_tools_approval_mode: 'approve',
13
+ required: true,
14
+ },
15
+ };
16
+ env[input.mcp.bearerTokenEnvVar] = input.mcp.bearerToken;
17
+ }
18
+ return { configOverrides, env };
19
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import { type KtxLogger } from '../core/config.js';
3
+ import { type CodexRuntimeMcpServerHandle } from './codex-mcp-runtime-server.js';
4
+ import { type CodexSdkRunner } from './codex-sdk-runner.js';
5
+ import type { KtxGenerateObjectInput, KtxGenerateTextInput, KtxLlmRuntimePort, KtxRuntimeToolSet, RunLoopParams, RunLoopResult } from './runtime-port.js';
6
+ export interface CodexKtxLlmRuntimeDeps {
7
+ projectDir: string;
8
+ modelSlots: {
9
+ default: string;
10
+ } & Partial<Record<string, string>>;
11
+ runner?: CodexSdkRunner;
12
+ startMcpServer?: (input: {
13
+ projectDir: string;
14
+ toolSet: KtxRuntimeToolSet;
15
+ }) => Promise<CodexRuntimeMcpServerHandle>;
16
+ logger?: KtxLogger;
17
+ }
18
+ export declare class CodexKtxLlmRuntime implements KtxLlmRuntimePort {
19
+ private readonly deps;
20
+ private readonly runner;
21
+ private readonly logger;
22
+ constructor(deps: CodexKtxLlmRuntimeDeps);
23
+ generateText(input: KtxGenerateTextInput): Promise<string>;
24
+ generateObject<TOutput, TSchema extends z.ZodType<TOutput>>(input: KtxGenerateObjectInput<TOutput, TSchema>): Promise<TOutput>;
25
+ runAgentLoop(params: RunLoopParams): Promise<RunLoopResult>;
26
+ }
27
+ export declare function runCodexAuthProbe(input: {
28
+ projectDir: string;
29
+ model: string;
30
+ runner?: CodexSdkRunner;
31
+ }): Promise<{
32
+ ok: true;
33
+ } | {
34
+ ok: false;
35
+ message: string;
36
+ fix: string;
37
+ }>;
@@ -0,0 +1,304 @@
1
+ import { z } from 'zod';
2
+ import { noopLogger } from '../core/config.js';
3
+ import { isCompletedAgentStep, summarizeCodexExecEvents } from './codex-exec-events.js';
4
+ import { startCodexRuntimeMcpServer, } from './codex-mcp-runtime-server.js';
5
+ import { resolveCodexModel } from './codex-models.js';
6
+ import { buildCodexRuntimeConfig } from './codex-runtime-config.js';
7
+ import { CodexSdkCliRunner } from './codex-sdk-runner.js';
8
+ function modelForRole(modelSlots, role) {
9
+ return resolveCodexModel(modelSlots[role] ?? modelSlots.default);
10
+ }
11
+ function promptWithSystem(system, prompt) {
12
+ return [system, prompt].filter(Boolean).join('\n\n');
13
+ }
14
+ function eventRecord(value) {
15
+ return value && typeof value === 'object' ? value : undefined;
16
+ }
17
+ function isTurnCompleted(event) {
18
+ return eventRecord(event)?.type === 'turn.completed';
19
+ }
20
+ /**
21
+ * Drains the Codex stream once, emitting a step as each agent action completes
22
+ * so callers see live progress and the step budget is enforced mid-run. Every
23
+ * completed agent-action item counts (see {@link isCompletedAgentStep}), so
24
+ * built-in `command_execution` steps decrement the budget the same as
25
+ * `mcp_tool_call`s. A turn that produced no actions still counts as one step,
26
+ * matching the metrics summary and the AI SDK backend.
27
+ */
28
+ async function collectEvents(events, options = {}) {
29
+ const collected = [];
30
+ let completedSteps = 0;
31
+ let sawActionStep = false;
32
+ let budgetExceeded = false;
33
+ let streamError;
34
+ // The SDK yields every stdout event, then throws on a non-zero codex exec
35
+ // exit. Catch that throw so the events already collected (which carry the
36
+ // real `turn.failed`/`error` reason) survive for the summary; the masked
37
+ // exit message is kept only as a fallback when no error event was emitted.
38
+ try {
39
+ for await (const event of events) {
40
+ collected.push(event);
41
+ const isActionStep = isCompletedAgentStep(event);
42
+ if (isActionStep) {
43
+ sawActionStep = true;
44
+ }
45
+ else if (sawActionStep || !isTurnCompleted(event)) {
46
+ // Only fall back to counting a bare turn as a step when the turn produced
47
+ // no agent actions; a completed turn is terminal, so it never aborts.
48
+ continue;
49
+ }
50
+ completedSteps += 1;
51
+ await options.onStep?.(completedSteps);
52
+ if (isActionStep && options.stepBudget !== undefined && completedSteps >= options.stepBudget) {
53
+ budgetExceeded = true;
54
+ options.abortController?.abort();
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ catch (error) {
60
+ streamError = error instanceof Error ? error : new Error(String(error));
61
+ }
62
+ return { events: collected, budgetExceeded, ...(streamError ? { streamError } : {}) };
63
+ }
64
+ function metrics(summary, startedAt) {
65
+ return { totalMs: Date.now() - startedAt, usage: summary.usage };
66
+ }
67
+ function summaryError(summary, streamError) {
68
+ // A `turn.failed`/`error` event carries the real reason; prefer it over the
69
+ // SDK's generic non-zero-exit throw. Fall back to the stream error only when
70
+ // no event explained the failure (e.g. spawn failure or auth before a turn).
71
+ if (summary.error) {
72
+ return summary.error;
73
+ }
74
+ if (summary.toolFailures.length > 0) {
75
+ return new Error(`Codex runtime tool call failed: ${summary.toolFailures.join('; ')}`);
76
+ }
77
+ return streamError;
78
+ }
79
+ function assertSuccessfulText(summary, streamError) {
80
+ const error = summaryError(summary, streamError);
81
+ if (error) {
82
+ throw error;
83
+ }
84
+ if (!summary.finalText.trim()) {
85
+ throw new Error('Codex completed without an agent message');
86
+ }
87
+ return summary.finalText;
88
+ }
89
+ function parseStructuredOutput(schema, text) {
90
+ try {
91
+ return schema.parse(JSON.parse(text));
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ throw new Error(`Codex structured output failed validation: ${message}`);
96
+ }
97
+ }
98
+ async function mcpForTools(input) {
99
+ if (!input.toolSet || Object.keys(input.toolSet).length === 0) {
100
+ return undefined;
101
+ }
102
+ return (input.startMcpServer ?? startCodexRuntimeMcpServer)({
103
+ projectDir: input.projectDir,
104
+ toolSet: input.toolSet,
105
+ });
106
+ }
107
+ function runtimeToolNames(toolSet) {
108
+ return Object.values(toolSet ?? {}).map((descriptor) => descriptor.name);
109
+ }
110
+ export class CodexKtxLlmRuntime {
111
+ deps;
112
+ runner;
113
+ logger;
114
+ constructor(deps) {
115
+ this.deps = deps;
116
+ this.runner = deps.runner ?? new CodexSdkCliRunner();
117
+ this.logger = deps.logger ?? noopLogger;
118
+ }
119
+ async generateText(input) {
120
+ const startedAt = Date.now();
121
+ const model = modelForRole(this.deps.modelSlots, input.role);
122
+ const mcp = await mcpForTools({
123
+ projectDir: this.deps.projectDir,
124
+ toolSet: input.tools,
125
+ startMcpServer: this.deps.startMcpServer,
126
+ });
127
+ try {
128
+ const config = buildCodexRuntimeConfig({
129
+ model,
130
+ ...(mcp
131
+ ? {
132
+ mcp: {
133
+ url: mcp.url,
134
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
135
+ bearerToken: mcp.bearerToken,
136
+ toolNames: runtimeToolNames(input.tools),
137
+ },
138
+ }
139
+ : {}),
140
+ });
141
+ const collected = await collectEvents(await this.runner.runStreamed({
142
+ projectDir: this.deps.projectDir,
143
+ model,
144
+ prompt: promptWithSystem(input.system, input.prompt),
145
+ configOverrides: config.configOverrides,
146
+ env: config.env,
147
+ }));
148
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
149
+ input.onMetrics?.(metrics(summary, startedAt));
150
+ return assertSuccessfulText(summary, collected.streamError);
151
+ }
152
+ finally {
153
+ await mcp?.close();
154
+ }
155
+ }
156
+ async generateObject(input) {
157
+ const startedAt = Date.now();
158
+ const model = modelForRole(this.deps.modelSlots, input.role);
159
+ const mcp = await mcpForTools({
160
+ projectDir: this.deps.projectDir,
161
+ toolSet: input.tools,
162
+ startMcpServer: this.deps.startMcpServer,
163
+ });
164
+ try {
165
+ const config = buildCodexRuntimeConfig({
166
+ model,
167
+ ...(mcp
168
+ ? {
169
+ mcp: {
170
+ url: mcp.url,
171
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
172
+ bearerToken: mcp.bearerToken,
173
+ toolNames: runtimeToolNames(input.tools),
174
+ },
175
+ }
176
+ : {}),
177
+ });
178
+ const collected = await collectEvents(await this.runner.runStreamed({
179
+ projectDir: this.deps.projectDir,
180
+ model,
181
+ prompt: promptWithSystem(input.system, input.prompt),
182
+ configOverrides: config.configOverrides,
183
+ env: config.env,
184
+ outputSchema: z.toJSONSchema(input.schema, { target: 'draft-7' }),
185
+ }));
186
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
187
+ input.onMetrics?.(metrics(summary, startedAt));
188
+ return parseStructuredOutput(input.schema, assertSuccessfulText(summary, collected.streamError));
189
+ }
190
+ finally {
191
+ await mcp?.close();
192
+ }
193
+ }
194
+ async runAgentLoop(params) {
195
+ const startedAt = Date.now();
196
+ const model = modelForRole(this.deps.modelSlots, params.modelRole);
197
+ let mcp;
198
+ try {
199
+ mcp = await mcpForTools({
200
+ projectDir: this.deps.projectDir,
201
+ toolSet: params.toolSet,
202
+ startMcpServer: this.deps.startMcpServer,
203
+ });
204
+ const config = buildCodexRuntimeConfig({
205
+ model,
206
+ ...(mcp
207
+ ? {
208
+ mcp: {
209
+ url: mcp.url,
210
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
211
+ bearerToken: mcp.bearerToken,
212
+ toolNames: runtimeToolNames(params.toolSet),
213
+ },
214
+ }
215
+ : {}),
216
+ });
217
+ const abortController = new AbortController();
218
+ const onStep = async (stepIndex) => {
219
+ try {
220
+ await params.onStepFinish?.({ stepIndex, stepBudget: params.stepBudget });
221
+ }
222
+ catch (error) {
223
+ this.logger.warn(`[codex-runner] onStepFinish callback threw; ignoring: ${error instanceof Error ? error.message : String(error)}`);
224
+ }
225
+ };
226
+ const collected = await collectEvents(await this.runner.runStreamed({
227
+ projectDir: this.deps.projectDir,
228
+ model,
229
+ prompt: promptWithSystem(params.systemPrompt, params.userPrompt),
230
+ configOverrides: config.configOverrides,
231
+ env: config.env,
232
+ signal: abortController.signal,
233
+ }), { stepBudget: params.stepBudget, abortController, onStep });
234
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
235
+ const error = summaryError(summary, collected.streamError);
236
+ const stopReason = collected.budgetExceeded ? 'budget' : error ? 'error' : summary.stopReason;
237
+ return {
238
+ stopReason,
239
+ ...(stopReason === 'error' && error ? { error } : {}),
240
+ metrics: {
241
+ totalMs: Date.now() - startedAt,
242
+ usage: summary.usage,
243
+ stepCount: summary.stepCount,
244
+ stepBoundariesMs: summary.stepBoundariesMs,
245
+ },
246
+ };
247
+ }
248
+ catch (error) {
249
+ const err = error instanceof Error ? error : new Error(String(error));
250
+ return {
251
+ stopReason: 'error',
252
+ error: err,
253
+ metrics: { totalMs: Date.now() - startedAt, usage: {}, stepCount: 0, stepBoundariesMs: [] },
254
+ };
255
+ }
256
+ finally {
257
+ await mcp?.close();
258
+ }
259
+ }
260
+ }
261
+ // A rejected model is not an auth failure: Codex authenticated, connected, and
262
+ // the API refused the model id. These markers come from the API error envelope
263
+ // (e.g. "model is not supported", "invalid_request_error").
264
+ const MODEL_UNAVAILABLE_MARKERS = /\bnot supported\b|\bnot available\b|\bdoes not exist\b|invalid_request_error|\bunknown model\b|\bunsupported model\b/i;
265
+ function describeCodexProbeFailure(model, message) {
266
+ if (MODEL_UNAVAILABLE_MARKERS.test(message)) {
267
+ const fix = `Run \`codex\` to see the models your account supports, then set llm.models.default in ktx.yaml (or rerun \`ktx setup\`).`;
268
+ return {
269
+ message: `Codex is authenticated, but the configured model "${model}" is not available for this Codex account. ${fix} Details: ${message}`,
270
+ fix,
271
+ };
272
+ }
273
+ const fix = `Authenticate Codex locally with the Codex CLI, verify the Codex CLI is installed, then rerun setup or \`ktx status\`.`;
274
+ return {
275
+ message: `Codex authentication is not usable. ${fix} Details: ${message}`,
276
+ fix,
277
+ };
278
+ }
279
+ export async function runCodexAuthProbe(input) {
280
+ let model;
281
+ try {
282
+ model = resolveCodexModel(input.model);
283
+ }
284
+ catch (error) {
285
+ return {
286
+ ok: false,
287
+ message: error instanceof Error ? error.message : String(error),
288
+ fix: 'Set llm.models.default in ktx.yaml to a supported codex model (codex, default, or a gpt-* / codex-* id), or rerun `ktx setup`.',
289
+ };
290
+ }
291
+ const runtime = new CodexKtxLlmRuntime({
292
+ projectDir: input.projectDir,
293
+ modelSlots: { default: model },
294
+ ...(input.runner ? { runner: input.runner } : {}),
295
+ });
296
+ try {
297
+ await runtime.generateText({ role: 'default', prompt: 'Reply with exactly: ok' });
298
+ return { ok: true };
299
+ }
300
+ catch (error) {
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ return { ok: false, ...describeCodexProbeFailure(model, message) };
303
+ }
304
+ }
@@ -0,0 +1,21 @@
1
+ export interface CodexSdkRunnerInput {
2
+ projectDir: string;
3
+ model: string;
4
+ prompt: string;
5
+ configOverrides?: Record<string, unknown>;
6
+ env?: Record<string, string>;
7
+ outputSchema?: Record<string, unknown>;
8
+ signal?: AbortSignal;
9
+ }
10
+ export interface CodexSdkRunner {
11
+ runStreamed(input: CodexSdkRunnerInput): Promise<AsyncIterable<unknown>>;
12
+ }
13
+ export interface CodexSdkCliRunnerOptions {
14
+ envBase?: NodeJS.ProcessEnv;
15
+ codexPathOverride?: string;
16
+ }
17
+ export declare class CodexSdkCliRunner implements CodexSdkRunner {
18
+ private readonly options;
19
+ constructor(options?: CodexSdkCliRunnerOptions);
20
+ runStreamed(input: CodexSdkRunnerInput): Promise<AsyncIterable<unknown>>;
21
+ }