@kaelio/ktx 0.8.0 → 0.10.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 (183) hide show
  1. package/assets/python/{kaelio_ktx-0.8.0-py3-none-any.whl → kaelio_ktx-0.10.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/clack.d.ts +6 -0
  5. package/dist/clack.js +17 -2
  6. package/dist/cli-program.d.ts +3 -0
  7. package/dist/cli-program.js +42 -2
  8. package/dist/cli-runtime.d.ts +3 -0
  9. package/dist/cli-runtime.js +94 -3
  10. package/dist/commands/setup-commands.js +3 -4
  11. package/dist/connection-recovery.d.ts +34 -0
  12. package/dist/connection-recovery.js +82 -0
  13. package/dist/connection.js +26 -2
  14. package/dist/connectors/bigquery/connector.d.ts +2 -5
  15. package/dist/connectors/bigquery/connector.js +2 -2
  16. package/dist/connectors/clickhouse/connector.d.ts +2 -5
  17. package/dist/connectors/clickhouse/connector.js +2 -2
  18. package/dist/connectors/mysql/connector.d.ts +7 -6
  19. package/dist/connectors/mysql/connector.js +25 -5
  20. package/dist/connectors/mysql/dialect.d.ts +1 -1
  21. package/dist/connectors/mysql/dialect.js +12 -2
  22. package/dist/connectors/postgres/connector.d.ts +2 -5
  23. package/dist/connectors/postgres/connector.js +2 -2
  24. package/dist/connectors/snowflake/connector.d.ts +2 -5
  25. package/dist/connectors/snowflake/connector.js +2 -2
  26. package/dist/connectors/sqlite/connector.d.ts +2 -5
  27. package/dist/connectors/sqlite/connector.js +2 -2
  28. package/dist/connectors/sqlserver/connector.d.ts +2 -5
  29. package/dist/connectors/sqlserver/connector.js +2 -2
  30. package/dist/context/connections/drivers.d.ts +0 -1
  31. package/dist/context/connections/drivers.js +0 -7
  32. package/dist/context/connections/query-executor.d.ts +2 -1
  33. package/dist/context/core/abort.d.ts +9 -0
  34. package/dist/context/core/abort.js +36 -0
  35. package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
  36. package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
  37. package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
  38. package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
  39. package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
  40. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +30 -0
  41. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +194 -0
  42. package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
  43. package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
  44. package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
  45. package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
  46. package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
  47. package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
  48. package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
  49. package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
  50. package/dist/context/ingest/context-candidates/curator-pagination.service.d.ts +1 -5
  51. package/dist/context/ingest/context-candidates/curator-pagination.service.js +1 -3
  52. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  53. package/dist/context/ingest/final-gate-repair.d.ts +1 -0
  54. package/dist/context/ingest/final-gate-repair.js +1 -0
  55. package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
  56. package/dist/context/ingest/ingest-bundle.runner.js +127 -53
  57. package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
  58. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +1 -0
  59. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +1 -0
  60. package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
  61. package/dist/context/ingest/local-adapters.js +21 -4
  62. package/dist/context/ingest/local-bundle-runtime.js +13 -5
  63. package/dist/context/ingest/local-ingest.d.ts +1 -0
  64. package/dist/context/ingest/local-ingest.js +13 -3
  65. package/dist/context/ingest/memory-flow/events.js +1 -1
  66. package/dist/context/ingest/memory-flow/schema.js +8 -3
  67. package/dist/context/ingest/memory-flow/types.d.ts +7 -3
  68. package/dist/context/ingest/ports.d.ts +3 -5
  69. package/dist/context/ingest/stages/stage-3-work-units.d.ts +1 -4
  70. package/dist/context/ingest/stages/stage-3-work-units.js +5 -1
  71. package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +1 -4
  72. package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
  73. package/dist/context/ingest/types.d.ts +1 -0
  74. package/dist/context/llm/ai-sdk-runtime.d.ts +3 -0
  75. package/dist/context/llm/ai-sdk-runtime.js +152 -16
  76. package/dist/context/llm/claude-code-runtime.d.ts +6 -4
  77. package/dist/context/llm/claude-code-runtime.js +127 -48
  78. package/dist/context/llm/codex-exec-events.d.ts +20 -0
  79. package/dist/context/llm/codex-exec-events.js +155 -0
  80. package/dist/context/llm/codex-isolation.d.ts +3 -0
  81. package/dist/context/llm/codex-isolation.js +5 -0
  82. package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
  83. package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
  84. package/dist/context/llm/codex-models.d.ts +2 -0
  85. package/dist/context/llm/codex-models.js +17 -0
  86. package/dist/context/llm/codex-runtime-config.d.ts +16 -0
  87. package/dist/context/llm/codex-runtime-config.js +19 -0
  88. package/dist/context/llm/codex-runtime.d.ts +37 -0
  89. package/dist/context/llm/codex-runtime.js +347 -0
  90. package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
  91. package/dist/context/llm/codex-sdk-runner.js +63 -0
  92. package/dist/context/llm/local-config.d.ts +16 -4
  93. package/dist/context/llm/local-config.js +18 -2
  94. package/dist/context/llm/rate-limit-governor.d.ts +103 -0
  95. package/dist/context/llm/rate-limit-governor.js +285 -0
  96. package/dist/context/llm/runtime-port.d.ts +3 -6
  97. package/dist/context/mcp/context-tools.js +43 -13
  98. package/dist/context/project/config.d.ts +14 -0
  99. package/dist/context/project/config.js +37 -2
  100. package/dist/context/scan/types.d.ts +15 -2
  101. package/dist/context/scan/types.js +12 -0
  102. package/dist/context/sl/description-normalization.js +4 -14
  103. package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
  104. package/dist/context/sql-analysis/ports.d.ts +12 -2
  105. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  106. package/dist/context-build-view.d.ts +13 -0
  107. package/dist/context-build-view.js +63 -32
  108. package/dist/demo-metrics.d.ts +0 -2
  109. package/dist/demo-metrics.js +1 -11
  110. package/dist/ingest.d.ts +1 -0
  111. package/dist/ingest.js +32 -3
  112. package/dist/io/buffered-command-io.d.ts +11 -0
  113. package/dist/io/buffered-command-io.js +28 -0
  114. package/dist/io/symbols.d.ts +2 -0
  115. package/dist/io/symbols.js +2 -0
  116. package/dist/llm/types.d.ts +1 -1
  117. package/dist/local-adapters.d.ts +10 -2
  118. package/dist/local-adapters.js +19 -3
  119. package/dist/memory-flow-hud.js +8 -16
  120. package/dist/next-steps.js +1 -2
  121. package/dist/progress-port-adapter.d.ts +6 -0
  122. package/dist/progress-port-adapter.js +18 -0
  123. package/dist/public-ingest.d.ts +20 -1
  124. package/dist/public-ingest.js +228 -42
  125. package/dist/reveal-password-prompt.d.ts +24 -0
  126. package/dist/reveal-password-prompt.js +78 -0
  127. package/dist/scan.js +21 -3
  128. package/dist/setup-context.d.ts +2 -0
  129. package/dist/setup-context.js +133 -27
  130. package/dist/setup-databases.d.ts +18 -1
  131. package/dist/setup-databases.js +378 -249
  132. package/dist/setup-demo-tour.js +1 -0
  133. package/dist/setup-embeddings.js +1 -1
  134. package/dist/setup-models.d.ts +11 -15
  135. package/dist/setup-models.js +140 -276
  136. package/dist/setup-prompts.js +3 -2
  137. package/dist/setup-ready-menu.d.ts +16 -2
  138. package/dist/setup-ready-menu.js +37 -5
  139. package/dist/setup-sources.js +115 -35
  140. package/dist/setup.d.ts +1 -1
  141. package/dist/setup.js +23 -11
  142. package/dist/sl.d.ts +2 -2
  143. package/dist/sl.js +20 -4
  144. package/dist/sql.js +18 -2
  145. package/dist/star-prompt/cache.d.ts +16 -0
  146. package/dist/star-prompt/cache.js +45 -0
  147. package/dist/star-prompt/star-count.d.ts +7 -0
  148. package/dist/star-prompt/star-count.js +66 -0
  149. package/dist/star-prompt/star-line.d.ts +12 -0
  150. package/dist/star-prompt/star-line.js +26 -0
  151. package/dist/status-project.d.ts +11 -0
  152. package/dist/status-project.js +50 -1
  153. package/dist/telemetry/command-hook.d.ts +1 -0
  154. package/dist/telemetry/command-hook.js +3 -1
  155. package/dist/telemetry/emitter.d.ts +10 -0
  156. package/dist/telemetry/emitter.js +31 -0
  157. package/dist/telemetry/events.d.ts +35 -6
  158. package/dist/telemetry/events.js +25 -2
  159. package/dist/telemetry/exception.d.ts +18 -0
  160. package/dist/telemetry/exception.js +162 -0
  161. package/dist/telemetry/identity.d.ts +0 -1
  162. package/dist/telemetry/identity.js +6 -6
  163. package/dist/telemetry/index.d.ts +15 -2
  164. package/dist/telemetry/index.js +15 -3
  165. package/dist/telemetry/redaction-secrets.d.ts +11 -0
  166. package/dist/telemetry/redaction-secrets.js +92 -0
  167. package/dist/telemetry/scrubber.d.ts +10 -0
  168. package/dist/telemetry/scrubber.js +20 -0
  169. package/dist/update-check/cache.d.ts +21 -0
  170. package/dist/update-check/cache.js +38 -0
  171. package/dist/update-check/channel.d.ts +15 -0
  172. package/dist/update-check/channel.js +30 -0
  173. package/dist/update-check/registry.d.ts +1 -0
  174. package/dist/update-check/registry.js +45 -0
  175. package/dist/update-check/update-check.d.ts +43 -0
  176. package/dist/update-check/update-check.js +116 -0
  177. package/package.json +12 -4
  178. package/dist/context/connections/local-query-executor.d.ts +0 -6
  179. package/dist/context/connections/local-query-executor.js +0 -39
  180. package/dist/context/connections/postgres-query-executor.d.ts +0 -25
  181. package/dist/context/connections/postgres-query-executor.js +0 -53
  182. package/dist/context/connections/sqlite-query-executor.d.ts +0 -4
  183. package/dist/context/connections/sqlite-query-executor.js +0 -74
@@ -44,16 +44,20 @@ type MemoryFlowEventPayload = {
44
44
  percent: number;
45
45
  message: string;
46
46
  transient?: boolean;
47
+ } | {
48
+ type: 'rate_limit_wait';
49
+ provider: string;
50
+ rateLimitType?: string;
51
+ resumeAtMs: number;
52
+ remainingMs: number;
47
53
  } | {
48
54
  type: 'work_unit_started';
49
55
  unitKey: string;
50
56
  skills: string[];
51
- stepBudget: number;
52
57
  } | {
53
58
  type: 'work_unit_step';
54
59
  unitKey: string;
55
- stepIndex: number;
56
- stepBudget: number;
60
+ toolCalls: number;
57
61
  } | {
58
62
  type: 'candidate_action';
59
63
  unitKey: string;
@@ -5,6 +5,7 @@ import type { KtxFileStorePort } from '../../context/core/file-store.js';
5
5
  import type { KtxLogger } from '../../context/core/config.js';
6
6
  import type { SessionOutcome } from '../../context/core/session-worktree.service.js';
7
7
  import type { AgentRunnerPort, KtxLlmRuntimePort, KtxRuntimeToolSet } from '../../context/llm/runtime-port.js';
8
+ import type { RateLimitGovernor } from '../llm/rate-limit-governor.js';
8
9
  import type { MemoryAction, MemoryKnowledgeSlRefsPort } from '../../context/memory/types.js';
9
10
  import type { PromptService } from '../../context/prompts/prompt.service.js';
10
11
  import type { SkillsRegistryService } from '../../context/skills/skills-registry.service.js';
@@ -111,6 +112,7 @@ interface IngestSettingsPort {
111
112
  workUnitMaxConcurrency?: number;
112
113
  workUnitStepBudget?: number;
113
114
  workUnitFailureMode?: 'abort' | 'continue';
115
+ rateLimitGovernor?: RateLimitGovernor;
114
116
  /** Print a timing breakdown to stderr at the end of each run (config-driven; see also KTX_PROFILE_INGEST). `'json'` emits the raw structured profile. */
115
117
  profileIngest?: boolean | 'json';
116
118
  ingestTraceLevel?: IngestTraceLevel;
@@ -282,11 +284,7 @@ export interface CuratorPaginationPort {
282
284
  }) => string;
283
285
  buildToolSet: (passNumber: number) => KtxRuntimeToolSet;
284
286
  getReconciliationActions: () => MemoryAction[];
285
- onStepFinish?: (info: {
286
- passNumber: number;
287
- stepIndex: number;
288
- stepBudget: number;
289
- }) => void;
287
+ abortSignal?: AbortSignal;
290
288
  }): Promise<ReconciliationOutcome & {
291
289
  report: CuratorPaginationReport;
292
290
  warnings: string[];
@@ -25,10 +25,7 @@ export interface WorkUnitExecutionDeps {
25
25
  sourceKey: string;
26
26
  connectionId: string;
27
27
  jobId: string;
28
- onStepFinish?: (info: {
29
- stepIndex: number;
30
- stepBudget: number;
31
- }) => void;
28
+ abortSignal?: AbortSignal;
32
29
  toolFailureCount?: (unitKey: string) => number;
33
30
  }
34
31
  export interface WorkUnitOutcome {
@@ -1,3 +1,4 @@
1
+ import { isAbortError } from '../../core/abort.js';
1
2
  import { listTouchedSlSources } from '../../../context/tools/touched-sl-sources.js';
2
3
  const MAX_WORK_UNIT_PROMPT_CHARS = 240_000;
3
4
  export async function executeWorkUnit(deps, wu) {
@@ -51,10 +52,13 @@ export async function executeWorkUnit(deps, wu) {
51
52
  unitKey: wu.unitKey,
52
53
  jobId: deps.jobId,
53
54
  },
54
- onStepFinish: deps.onStepFinish,
55
+ abortSignal: deps.abortSignal,
55
56
  });
56
57
  }
57
58
  catch (error) {
59
+ if (isAbortError(error)) {
60
+ throw error;
61
+ }
58
62
  return failWithResetFromCurrentHead(error instanceof Error ? error.message : String(error));
59
63
  }
60
64
  const postSha = (await deps.sessionWorktreeGit.revParseHead()) ?? preSha;
@@ -14,10 +14,7 @@ export interface ReconciliationContext {
14
14
  sourceKey: string;
15
15
  jobId: string;
16
16
  force?: boolean;
17
- onStepFinish?: (info: {
18
- stepIndex: number;
19
- stepBudget: number;
20
- }) => void;
17
+ abortSignal?: AbortSignal;
21
18
  forceRun?: boolean;
22
19
  }
23
20
  export interface ReconciliationOutcome {
@@ -11,7 +11,7 @@ export async function runReconciliationStage4(ctx) {
11
11
  toolSet: ctx.buildToolSet(),
12
12
  stepBudget: ctx.stepBudget,
13
13
  telemetryTags: { operationName: 'ingest-bundle-reconcile', source: ctx.sourceKey, jobId: ctx.jobId },
14
- onStepFinish: ctx.onStepFinish,
14
+ abortSignal: ctx.abortSignal,
15
15
  });
16
16
  return { skipped: false, stopReason: run.stopReason, error: run.error, ...(run.metrics ? { metrics: run.metrics } : {}) };
17
17
  }
@@ -198,6 +198,7 @@ export interface IngestJobPhase {
198
198
  export interface IngestJobContext {
199
199
  jobId: string;
200
200
  memoryFlow?: MemoryFlowEventSink;
201
+ abortSignal?: AbortSignal;
201
202
  startPhase(weight: number): IngestJobPhase;
202
203
  }
203
204
  export {};
@@ -3,6 +3,7 @@ import { type TelemetrySettings } from 'ai';
3
3
  import type { z } from 'zod';
4
4
  import { type KtxLogger } from '../../context/core/config.js';
5
5
  import { type KtxLlmDebugRequestRecorder } from './debug-request-recorder.js';
6
+ import type { RateLimitGovernor } from './rate-limit-governor.js';
6
7
  import type { KtxGenerateObjectInput, KtxGenerateTextInput, KtxLlmRuntimePort, RunLoopParams, RunLoopResult } from './runtime-port.js';
7
8
  interface AgentTelemetryPort {
8
9
  createTelemetry(tags: Record<string, string>): TelemetrySettings;
@@ -12,11 +13,13 @@ export interface AiSdkKtxLlmRuntimeDeps {
12
13
  telemetry?: AgentTelemetryPort;
13
14
  logger?: KtxLogger;
14
15
  debugRequestRecorder?: KtxLlmDebugRequestRecorder;
16
+ rateLimitGovernor?: Pick<RateLimitGovernor, 'waitForReady' | 'report' | 'maxRetryAttempts'>;
15
17
  }
16
18
  export declare class AiSdkKtxLlmRuntime implements KtxLlmRuntimePort {
17
19
  private readonly deps;
18
20
  private readonly logger;
19
21
  constructor(deps: AiSdkKtxLlmRuntimeDeps);
22
+ private generateTextWithRateLimitRetry;
20
23
  generateText(input: KtxGenerateTextInput): Promise<string>;
21
24
  generateObject<TOutput, TSchema extends z.ZodType<TOutput>>(input: KtxGenerateObjectInput<TOutput, TSchema>): Promise<TOutput>;
22
25
  runAgentLoop(params: RunLoopParams): Promise<RunLoopResult>;
@@ -1,6 +1,7 @@
1
1
  import { KtxMessageBuilder, splitKtxSystemMessages } from '../../llm/message-builder.js';
2
2
  import { generateText, Output, stepCountIs } from 'ai';
3
3
  import { noopLogger } from '../../context/core/config.js';
4
+ import { isAbortError } from '../core/abort.js';
4
5
  import { summarizeKtxLlmDebugRequest } from './debug-request-recorder.js';
5
6
  import { createAiSdkToolSet } from './runtime-tools.js';
6
7
  function toLlmTokenUsage(usage) {
@@ -16,6 +17,108 @@ function toLlmTokenUsage(usage) {
16
17
  function hasTools(tools) {
17
18
  return Object.keys(tools).length > 0;
18
19
  }
20
+ function modelProviderName(model) {
21
+ const provider = model.provider ?? '';
22
+ return provider.includes('vertex') || provider.includes('google') ? 'vertex' : 'anthropic-api';
23
+ }
24
+ const RATE_LIMIT_HEADER_PAIRS = [
25
+ {
26
+ limit: 'anthropic-ratelimit-requests-limit',
27
+ remaining: 'anthropic-ratelimit-requests-remaining',
28
+ rateLimitType: 'rpm',
29
+ },
30
+ {
31
+ limit: 'anthropic-ratelimit-tokens-limit',
32
+ remaining: 'anthropic-ratelimit-tokens-remaining',
33
+ rateLimitType: 'tpm',
34
+ },
35
+ {
36
+ limit: 'anthropic-ratelimit-input-tokens-limit',
37
+ remaining: 'anthropic-ratelimit-input-tokens-remaining',
38
+ rateLimitType: 'itpm',
39
+ },
40
+ {
41
+ limit: 'anthropic-ratelimit-output-tokens-limit',
42
+ remaining: 'anthropic-ratelimit-output-tokens-remaining',
43
+ rateLimitType: 'otpm',
44
+ },
45
+ {
46
+ limit: 'x-ratelimit-limit-requests',
47
+ remaining: 'x-ratelimit-remaining-requests',
48
+ rateLimitType: 'rpm',
49
+ },
50
+ {
51
+ limit: 'x-ratelimit-limit-tokens',
52
+ remaining: 'x-ratelimit-remaining-tokens',
53
+ rateLimitType: 'tpm',
54
+ },
55
+ ];
56
+ function normalizeHeaders(headers) {
57
+ if (!headers || typeof headers !== 'object') {
58
+ return {};
59
+ }
60
+ const get = headers.get;
61
+ if (typeof get === 'function') {
62
+ const out = {};
63
+ for (const pair of RATE_LIMIT_HEADER_PAIRS) {
64
+ const limit = get.call(headers, pair.limit);
65
+ const remaining = get.call(headers, pair.remaining);
66
+ if (typeof limit === 'string')
67
+ out[pair.limit] = limit;
68
+ if (typeof remaining === 'string')
69
+ out[pair.remaining] = remaining;
70
+ }
71
+ return out;
72
+ }
73
+ return Object.fromEntries(Object.entries(headers)
74
+ .filter((entry) => typeof entry[1] === 'string' || typeof entry[1] === 'number')
75
+ .map(([key, value]) => [key.toLowerCase(), String(value)]));
76
+ }
77
+ function numericHeader(headers, key) {
78
+ const value = Number(headers[key]);
79
+ return Number.isFinite(value) && value >= 0 ? value : undefined;
80
+ }
81
+ function utilizationForPair(headers, pair) {
82
+ const limit = numericHeader(headers, pair.limit);
83
+ const remaining = numericHeader(headers, pair.remaining);
84
+ if (limit === undefined || remaining === undefined || limit <= 0) {
85
+ return undefined;
86
+ }
87
+ return 1 - Math.min(limit, remaining) / limit;
88
+ }
89
+ function aiSdkHeaderRateLimitSignal(provider, result) {
90
+ const headers = normalizeHeaders(result.response?.headers);
91
+ let best;
92
+ for (const pair of RATE_LIMIT_HEADER_PAIRS) {
93
+ const utilization = utilizationForPair(headers, pair);
94
+ if (utilization === undefined) {
95
+ continue;
96
+ }
97
+ if (!best || utilization > best.utilization) {
98
+ best = { utilization, rateLimitType: pair.rateLimitType };
99
+ }
100
+ }
101
+ if (!best) {
102
+ return undefined;
103
+ }
104
+ return {
105
+ provider,
106
+ status: 'allowed',
107
+ rateLimitType: best.rateLimitType,
108
+ utilization: Number(best.utilization.toFixed(4)),
109
+ };
110
+ }
111
+ function retryAfterMs(error) {
112
+ const value = error.retryAfter;
113
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
114
+ return value < 1_000 ? value * 1_000 : value;
115
+ }
116
+ return undefined;
117
+ }
118
+ function isAiSdkRateLimitError(error) {
119
+ const record = error;
120
+ return record.name === 'TooManyRequestsError' || record.statusCode === 429 || record.status === 429;
121
+ }
19
122
  export class AiSdkKtxLlmRuntime {
20
123
  deps;
21
124
  logger;
@@ -23,6 +126,37 @@ export class AiSdkKtxLlmRuntime {
23
126
  this.deps = deps;
24
127
  this.logger = deps.logger ?? noopLogger;
25
128
  }
129
+ async generateTextWithRateLimitRetry(provider, abortSignal, run) {
130
+ // maxRetryAttempts() returns 1 when no governor is present or pacing is
131
+ // disabled, so a 429 throws immediately instead of hammering the provider
132
+ // with no backoff; the AI SDK's own maxRetries still handles transient 429s.
133
+ const maxAttempts = this.deps.rateLimitGovernor?.maxRetryAttempts() ?? 1;
134
+ let attempt = 0;
135
+ while (true) {
136
+ await this.deps.rateLimitGovernor?.waitForReady(abortSignal);
137
+ try {
138
+ const result = await run();
139
+ const signal = aiSdkHeaderRateLimitSignal(provider, result);
140
+ if (signal) {
141
+ this.deps.rateLimitGovernor?.report(signal);
142
+ }
143
+ return result;
144
+ }
145
+ catch (error) {
146
+ if (isAbortError(error) || !isAiSdkRateLimitError(error) || attempt >= maxAttempts - 1) {
147
+ throw error;
148
+ }
149
+ attempt += 1;
150
+ const retryAfter = retryAfterMs(error);
151
+ this.deps.rateLimitGovernor?.report({
152
+ provider,
153
+ status: 'rejected',
154
+ rateLimitType: 'http_429',
155
+ ...(retryAfter !== undefined ? { retryAfterMs: retryAfter } : {}),
156
+ });
157
+ }
158
+ }
159
+ }
26
160
  async generateText(input) {
27
161
  const model = this.deps.llmProvider.getModel(input.role);
28
162
  if (model.provider === 'deterministic') {
@@ -37,12 +171,13 @@ export class AiSdkKtxLlmRuntime {
37
171
  });
38
172
  const split = splitKtxSystemMessages(built.messages);
39
173
  const startedAt = Date.now();
40
- const result = await generateText({
174
+ const request = {
41
175
  model,
42
176
  temperature: input.temperature ?? 0,
43
177
  ...(split.system ? { system: split.system } : {}),
44
178
  messages: split.messages,
45
179
  tools: built.tools,
180
+ ...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
46
181
  ...(hasTools(tools)
47
182
  ? {
48
183
  experimental_repairToolCall: this.deps.llmProvider.repairToolCallHandler({
@@ -50,7 +185,8 @@ export class AiSdkKtxLlmRuntime {
50
185
  }),
51
186
  }
52
187
  : {}),
53
- });
188
+ };
189
+ const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
54
190
  input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
55
191
  if (typeof result.text !== 'string') {
56
192
  throw new Error('KTX LLM text generation returned no text');
@@ -68,12 +204,13 @@ export class AiSdkKtxLlmRuntime {
68
204
  });
69
205
  const split = splitKtxSystemMessages(built.messages);
70
206
  const startedAt = Date.now();
71
- const result = await generateText({
207
+ const request = {
72
208
  model,
73
209
  temperature: input.temperature ?? 0,
74
210
  ...(split.system ? { system: split.system } : {}),
75
211
  messages: split.messages,
76
212
  tools: built.tools,
213
+ ...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
77
214
  ...(hasTools(tools)
78
215
  ? {
79
216
  experimental_repairToolCall: this.deps.llmProvider.repairToolCallHandler({
@@ -82,7 +219,8 @@ export class AiSdkKtxLlmRuntime {
82
219
  }
83
220
  : {}),
84
221
  output: Output.object({ schema: input.schema }),
85
- });
222
+ };
223
+ const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
86
224
  input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
87
225
  if (result.output == null) {
88
226
  throw new Error('KTX LLM object generation returned no output');
@@ -114,7 +252,7 @@ export class AiSdkKtxLlmRuntime {
114
252
  messages: built.messages,
115
253
  tools: built.tools,
116
254
  }));
117
- const result = await generateText({
255
+ const request = {
118
256
  model,
119
257
  temperature: 0,
120
258
  stopWhen: stepCountIs(params.stepBudget),
@@ -125,20 +263,15 @@ export class AiSdkKtxLlmRuntime {
125
263
  ...(promptMessages.system ? { system: promptMessages.system } : {}),
126
264
  messages: promptMessages.messages,
127
265
  tools: built.tools,
128
- onStepFinish: async () => {
266
+ ...(params.abortSignal ? { abortSignal: params.abortSignal } : {}),
267
+ // Count model round-trips locally for metrics. `stepCountIs(stepBudget)`
268
+ // caps the loop, so this counter never exceeds the budget.
269
+ onStepFinish: () => {
129
270
  stepIndex += 1;
130
271
  stepBoundariesMs.push(Date.now() - startedAt);
131
- if (!params.onStepFinish) {
132
- return;
133
- }
134
- try {
135
- await params.onStepFinish({ stepIndex, stepBudget: params.stepBudget });
136
- }
137
- catch (err) {
138
- this.logger.warn(`[agent-runner] onStepFinish callback threw; ignoring: ${err instanceof Error ? err.message : String(err)}`);
139
- }
140
272
  },
141
- });
273
+ };
274
+ const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), params.abortSignal, () => generateText(request));
142
275
  return {
143
276
  stopReason: 'natural',
144
277
  metrics: {
@@ -150,6 +283,9 @@ export class AiSdkKtxLlmRuntime {
150
283
  };
151
284
  }
152
285
  catch (error) {
286
+ if (isAbortError(error)) {
287
+ throw error;
288
+ }
153
289
  const err = error instanceof Error ? error : new Error(String(error));
154
290
  this.logger.warn(`[agent-runner] loop failed: ${err.message}`);
155
291
  return {
@@ -1,8 +1,11 @@
1
1
  import { query as defaultQuery, type SDKMessage, type SDKResultMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { z } from 'zod';
3
- import { type KtxLogger } from '../../context/core/config.js';
3
+ import type { RateLimitGovernor } from './rate-limit-governor.js';
4
4
  import type { KtxGenerateObjectInput, KtxGenerateTextInput, KtxLlmRuntimePort, RunLoopParams, RunLoopResult, RunLoopStopReason } from './runtime-port.js';
5
- type QueryFn = (params: Parameters<typeof defaultQuery>[0]) => AsyncIterable<SDKMessage>;
5
+ type QueryResult = AsyncIterable<SDKMessage> & {
6
+ interrupt?: () => void | Promise<void>;
7
+ };
8
+ type QueryFn = (params: Parameters<typeof defaultQuery>[0]) => QueryResult;
6
9
  export interface ClaudeCodeKtxLlmRuntimeDeps {
7
10
  projectDir: string;
8
11
  modelSlots: {
@@ -10,14 +13,13 @@ export interface ClaudeCodeKtxLlmRuntimeDeps {
10
13
  } & Partial<Record<string, string>>;
11
14
  query?: QueryFn;
12
15
  env?: NodeJS.ProcessEnv;
13
- logger?: KtxLogger;
16
+ rateLimitGovernor?: Pick<RateLimitGovernor, 'waitForReady' | 'report' | 'maxRetryAttempts'>;
14
17
  }
15
18
  /** @internal */
16
19
  export declare function mapClaudeCodeStopReason(result: SDKResultMessage): RunLoopStopReason;
17
20
  export declare class ClaudeCodeKtxLlmRuntime implements KtxLlmRuntimePort {
18
21
  private readonly deps;
19
22
  private readonly runQuery;
20
- private readonly logger;
21
23
  constructor(deps: ClaudeCodeKtxLlmRuntimeDeps);
22
24
  generateText(input: KtxGenerateTextInput): Promise<string>;
23
25
  generateObject<TOutput, TSchema extends z.ZodType<TOutput>>(input: KtxGenerateObjectInput<TOutput, TSchema>): Promise<TOutput>;