@kaelio/ktx 0.9.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.
- package/assets/python/{kaelio_ktx-0.9.0-py3-none-any.whl → kaelio_ktx-0.10.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/clack.d.ts +6 -0
- package/dist/clack.js +17 -2
- package/dist/cli-program.d.ts +3 -0
- package/dist/cli-program.js +42 -2
- package/dist/cli-runtime.d.ts +3 -0
- package/dist/cli-runtime.js +44 -0
- package/dist/commands/setup-commands.js +2 -3
- package/dist/connection.js +23 -1
- package/dist/connectors/bigquery/connector.d.ts +2 -5
- package/dist/connectors/bigquery/connector.js +2 -2
- package/dist/connectors/clickhouse/connector.d.ts +2 -5
- package/dist/connectors/clickhouse/connector.js +2 -2
- package/dist/connectors/mysql/connector.d.ts +7 -6
- package/dist/connectors/mysql/connector.js +25 -5
- package/dist/connectors/mysql/dialect.d.ts +1 -1
- package/dist/connectors/mysql/dialect.js +12 -2
- package/dist/connectors/postgres/connector.d.ts +2 -5
- package/dist/connectors/postgres/connector.js +2 -2
- package/dist/connectors/snowflake/connector.d.ts +2 -5
- package/dist/connectors/snowflake/connector.js +2 -2
- package/dist/connectors/sqlite/connector.d.ts +2 -5
- package/dist/connectors/sqlite/connector.js +2 -2
- package/dist/connectors/sqlserver/connector.d.ts +2 -5
- package/dist/connectors/sqlserver/connector.js +2 -2
- package/dist/context/connections/drivers.d.ts +0 -1
- package/dist/context/connections/drivers.js +0 -7
- package/dist/context/connections/query-executor.d.ts +2 -1
- package/dist/context/core/abort.d.ts +9 -0
- package/dist/context/core/abort.js +36 -0
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +1 -0
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +6 -2
- package/dist/context/ingest/context-candidates/curator-pagination.service.d.ts +1 -5
- package/dist/context/ingest/context-candidates/curator-pagination.service.js +1 -3
- package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
- package/dist/context/ingest/final-gate-repair.d.ts +1 -0
- package/dist/context/ingest/final-gate-repair.js +1 -0
- package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
- package/dist/context/ingest/ingest-bundle.runner.js +127 -53
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +1 -0
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +1 -0
- package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
- package/dist/context/ingest/local-bundle-runtime.js +11 -4
- package/dist/context/ingest/local-ingest.d.ts +1 -0
- package/dist/context/ingest/local-ingest.js +13 -3
- package/dist/context/ingest/memory-flow/events.js +1 -1
- package/dist/context/ingest/memory-flow/schema.js +8 -3
- package/dist/context/ingest/memory-flow/types.d.ts +7 -3
- package/dist/context/ingest/ports.d.ts +3 -5
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +1 -4
- package/dist/context/ingest/stages/stage-3-work-units.js +5 -1
- package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +1 -4
- package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
- package/dist/context/ingest/types.d.ts +1 -0
- package/dist/context/llm/ai-sdk-runtime.d.ts +3 -0
- package/dist/context/llm/ai-sdk-runtime.js +152 -16
- package/dist/context/llm/claude-code-runtime.d.ts +6 -4
- package/dist/context/llm/claude-code-runtime.js +127 -48
- package/dist/context/llm/codex-runtime.d.ts +3 -3
- package/dist/context/llm/codex-runtime.js +90 -47
- package/dist/context/llm/local-config.d.ts +15 -5
- package/dist/context/llm/local-config.js +6 -1
- package/dist/context/llm/rate-limit-governor.d.ts +103 -0
- package/dist/context/llm/rate-limit-governor.js +285 -0
- package/dist/context/llm/runtime-port.d.ts +3 -6
- package/dist/context/mcp/context-tools.js +43 -13
- package/dist/context/project/config.d.ts +12 -0
- package/dist/context/project/config.js +35 -0
- package/dist/context/scan/types.d.ts +15 -2
- package/dist/context/scan/types.js +12 -0
- package/dist/context/sl/description-normalization.js +4 -14
- package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
- package/dist/context-build-view.d.ts +13 -0
- package/dist/context-build-view.js +60 -1
- package/dist/demo-metrics.d.ts +0 -2
- package/dist/demo-metrics.js +1 -11
- package/dist/ingest.d.ts +1 -0
- package/dist/ingest.js +32 -3
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- package/dist/memory-flow-hud.js +8 -16
- package/dist/public-ingest.js +50 -15
- package/dist/reveal-password-prompt.d.ts +24 -0
- package/dist/reveal-password-prompt.js +78 -0
- package/dist/scan.js +18 -2
- package/dist/setup-databases.d.ts +1 -0
- package/dist/setup-databases.js +23 -3
- package/dist/setup-demo-tour.js +1 -0
- package/dist/setup-embeddings.js +1 -1
- package/dist/setup-models.d.ts +1 -14
- package/dist/setup-models.js +116 -340
- package/dist/setup-prompts.js +3 -2
- package/dist/setup-sources.js +7 -7
- package/dist/setup.d.ts +1 -1
- package/dist/setup.js +1 -1
- package/dist/sl.d.ts +2 -2
- package/dist/sl.js +20 -4
- package/dist/sql.js +18 -2
- package/dist/star-prompt/cache.d.ts +16 -0
- package/dist/star-prompt/cache.js +45 -0
- package/dist/star-prompt/star-count.d.ts +7 -0
- package/dist/star-prompt/star-count.js +66 -0
- package/dist/star-prompt/star-line.d.ts +12 -0
- package/dist/star-prompt/star-line.js +26 -0
- package/dist/telemetry/emitter.d.ts +10 -0
- package/dist/telemetry/emitter.js +31 -0
- package/dist/telemetry/events.d.ts +24 -0
- package/dist/telemetry/events.js +15 -0
- package/dist/telemetry/exception.d.ts +18 -0
- package/dist/telemetry/exception.js +162 -0
- package/dist/telemetry/index.d.ts +3 -2
- package/dist/telemetry/index.js +2 -1
- package/dist/telemetry/redaction-secrets.d.ts +11 -0
- package/dist/telemetry/redaction-secrets.js +92 -0
- package/dist/update-check/cache.d.ts +21 -0
- package/dist/update-check/cache.js +38 -0
- package/dist/update-check/channel.d.ts +15 -0
- package/dist/update-check/channel.js +30 -0
- package/dist/update-check/registry.d.ts +1 -0
- package/dist/update-check/registry.js +45 -0
- package/dist/update-check/update-check.d.ts +43 -0
- package/dist/update-check/update-check.js +116 -0
- package/package.json +8 -1
- package/dist/context/connections/local-query-executor.d.ts +0 -6
- package/dist/context/connections/local-query-executor.js +0 -39
- package/dist/context/connections/postgres-query-executor.d.ts +0 -25
- package/dist/context/connections/postgres-query-executor.js +0 -53
- package/dist/context/connections/sqlite-query-executor.d.ts +0 -4
- package/dist/context/connections/sqlite-query-executor.js +0 -74
|
@@ -8,6 +8,7 @@ import { SessionWorktreeService } from '../../context/core/session-worktree.serv
|
|
|
8
8
|
import { createRuntimeToolDescriptorFromAiTool } from '../../context/llm/runtime-tools.js';
|
|
9
9
|
import { createLocalKtxLlmRuntimeFromConfig } from '../../context/llm/local-config.js';
|
|
10
10
|
import { KtxIngestEmbeddingPortAdapter } from '../../context/llm/embedding-port.js';
|
|
11
|
+
import { createRateLimitGovernorConfig, RateLimitGovernor } from '../../context/llm/rate-limit-governor.js';
|
|
11
12
|
import { RuntimeAgentRunner } from '../../context/llm/runtime-port.js';
|
|
12
13
|
import { ktxLocalStateDbPath } from '../../context/project/local-state-db.js';
|
|
13
14
|
import { PromptService } from '../../context/prompts/prompt.service.js';
|
|
@@ -491,15 +492,16 @@ function localIngestLlmProviderGuardMessage(projectDir) {
|
|
|
491
492
|
'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, claude-code, or codex, or an injected agentRunner.',
|
|
492
493
|
'Configure a local Claude Code/Codex session or API-backed LLM, then rerun ingest:',
|
|
493
494
|
` ktx setup --project-dir ${projectDir} --llm-backend claude-code --no-input`,
|
|
494
|
-
` ktx setup --project-dir ${projectDir} --llm-backend codex --
|
|
495
|
-
` ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --
|
|
495
|
+
` ktx setup --project-dir ${projectDir} --llm-backend codex --no-input`,
|
|
496
|
+
` ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --no-input`,
|
|
496
497
|
].join('\n');
|
|
497
498
|
}
|
|
498
|
-
function resolveAgentRunner(options) {
|
|
499
|
+
function resolveAgentRunner(options, rateLimitGovernor) {
|
|
499
500
|
const llmRuntime = options.llmRuntime ??
|
|
500
501
|
(options.createLlmRuntime ?? createLocalKtxLlmRuntimeFromConfig)(options.project.config.llm, {
|
|
501
502
|
projectDir: options.project.projectDir,
|
|
502
503
|
env: process.env,
|
|
504
|
+
rateLimitGovernor,
|
|
503
505
|
}) ??
|
|
504
506
|
undefined;
|
|
505
507
|
if (options.agentRunner) {
|
|
@@ -536,7 +538,11 @@ export function createLocalBundleIngestRuntime(options) {
|
|
|
536
538
|
const knowledgeIndex = new LocalKnowledgeIndex(options.project, embedding);
|
|
537
539
|
const knowledgeEvents = new NoopKnowledgeEventPort();
|
|
538
540
|
const wikiService = new KnowledgeWikiService(rootFileStore, embedding, knowledgeIndex, options.project.git, logger);
|
|
539
|
-
const
|
|
541
|
+
const rateLimitGovernor = new RateLimitGovernor(createRateLimitGovernorConfig({
|
|
542
|
+
...options.project.config.ingest.rateLimit,
|
|
543
|
+
maxConcurrency: options.project.config.ingest.workUnits.maxConcurrency,
|
|
544
|
+
}));
|
|
545
|
+
const { agentRunner, llmRuntime } = resolveAgentRunner(options, rateLimitGovernor);
|
|
540
546
|
const promptService = new PromptService({ promptsDir, partials: [], logger });
|
|
541
547
|
const storage = new LocalIngestStorage(options.project);
|
|
542
548
|
const registry = registerAdapters(options.adapters);
|
|
@@ -575,6 +581,7 @@ export function createLocalBundleIngestRuntime(options) {
|
|
|
575
581
|
workUnitMaxConcurrency: options.project.config.ingest.workUnits.maxConcurrency,
|
|
576
582
|
workUnitStepBudget: options.project.config.ingest.workUnits.stepBudget,
|
|
577
583
|
workUnitFailureMode: options.project.config.ingest.workUnits.failureMode,
|
|
584
|
+
rateLimitGovernor,
|
|
578
585
|
profileIngest: options.project.config.ingest.profile,
|
|
579
586
|
ingestTraceLevel: ingestTraceLevelFromEnv(),
|
|
580
587
|
},
|
|
@@ -25,6 +25,7 @@ export interface RunLocalIngestOptions {
|
|
|
25
25
|
queryExecutor?: KtxSqlQueryExecutorPort;
|
|
26
26
|
logger?: KtxLogger;
|
|
27
27
|
embeddingProvider?: import('../../llm/types.js').KtxEmbeddingProvider | null;
|
|
28
|
+
abortSignal?: AbortSignal;
|
|
28
29
|
}
|
|
29
30
|
export interface LocalIngestResult {
|
|
30
31
|
result: IngestBundleResult;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { cp, mkdir, rm } from 'node:fs/promises';
|
|
3
3
|
import { isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { createAbortError, isAbortError } from '../../context/core/abort.js';
|
|
4
5
|
import { ktxLocalStateDbPath } from '../../context/project/local-state-db.js';
|
|
5
6
|
import { planMetabaseFanoutChildren } from './adapters/metabase/fanout-planner.js';
|
|
6
7
|
import { KtxYamlMetabaseSourceStateReader, LocalMetabaseDiscoveryCache } from './adapters/metabase/local-source-state-store.js';
|
|
@@ -36,10 +37,11 @@ function findAdapter(adapters, source) {
|
|
|
36
37
|
}
|
|
37
38
|
return adapter;
|
|
38
39
|
}
|
|
39
|
-
function localJobContext(jobId, memoryFlow) {
|
|
40
|
+
function localJobContext(jobId, memoryFlow, abortSignal) {
|
|
40
41
|
return {
|
|
41
42
|
jobId,
|
|
42
43
|
...(memoryFlow ? { memoryFlow } : {}),
|
|
44
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
43
45
|
startPhase() {
|
|
44
46
|
return new LocalIngestPhase();
|
|
45
47
|
},
|
|
@@ -62,7 +64,7 @@ async function runScheduledPullJob(options) {
|
|
|
62
64
|
sourceKey: options.adapter.source,
|
|
63
65
|
trigger: options.trigger ?? 'manual_resync',
|
|
64
66
|
bundleRef: { kind: 'scheduled_pull', config: options.pullConfig },
|
|
65
|
-
}, localJobContext(jobId, options.memoryFlow));
|
|
67
|
+
}, localJobContext(jobId, options.memoryFlow, options.abortSignal));
|
|
66
68
|
const report = await runtime.store.findByJobId(jobId);
|
|
67
69
|
if (!report) {
|
|
68
70
|
throw new Error(`Local ingest report for job "${jobId}" was not created`);
|
|
@@ -102,6 +104,7 @@ export async function runLocalIngest(options) {
|
|
|
102
104
|
queryExecutor: options.queryExecutor,
|
|
103
105
|
logger: options.logger,
|
|
104
106
|
embeddingProvider: options.embeddingProvider,
|
|
107
|
+
abortSignal: options.abortSignal,
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
110
|
const result = await runtime.runner.run({
|
|
@@ -110,7 +113,7 @@ export async function runLocalIngest(options) {
|
|
|
110
113
|
sourceKey: adapter.source,
|
|
111
114
|
trigger: options.trigger ?? (options.sourceDir ? 'upload' : 'manual_resync'),
|
|
112
115
|
bundleRef,
|
|
113
|
-
}, localJobContext(jobId, options.memoryFlow));
|
|
116
|
+
}, localJobContext(jobId, options.memoryFlow, options.abortSignal));
|
|
114
117
|
const report = await runtime.store.findByJobId(jobId);
|
|
115
118
|
if (!report) {
|
|
116
119
|
throw new Error(`Local ingest report for job "${jobId}" was not created`);
|
|
@@ -226,6 +229,9 @@ export async function runLocalMetabaseIngest(options) {
|
|
|
226
229
|
});
|
|
227
230
|
const children = [];
|
|
228
231
|
for (const childPlan of childPlans) {
|
|
232
|
+
if (options.abortSignal?.aborted) {
|
|
233
|
+
throw createAbortError();
|
|
234
|
+
}
|
|
229
235
|
const targetConnectionId = safeSegment('target connection id', childPlan.targetConnectionId);
|
|
230
236
|
if (!options.project.config.connections[targetConnectionId]) {
|
|
231
237
|
throw new Error(`Target connection "${targetConnectionId}" is not configured in ktx.yaml`);
|
|
@@ -255,9 +261,13 @@ export async function runLocalMetabaseIngest(options) {
|
|
|
255
261
|
queryExecutor: options.queryExecutor,
|
|
256
262
|
logger: options.logger,
|
|
257
263
|
embeddingProvider: options.embeddingProvider,
|
|
264
|
+
abortSignal: options.abortSignal,
|
|
258
265
|
});
|
|
259
266
|
}
|
|
260
267
|
catch (error) {
|
|
268
|
+
if (isAbortError(error)) {
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
261
271
|
child = await recordLocalMetabaseChildFailure({
|
|
262
272
|
project: options.project,
|
|
263
273
|
jobId: childJobId,
|
|
@@ -127,7 +127,7 @@ export function ingestReportToMemoryFlowReplay(report, options = {}) {
|
|
|
127
127
|
}
|
|
128
128
|
const actions = allReportActions(report);
|
|
129
129
|
const workUnitEvents = report.body.workUnits.flatMap((workUnit) => [
|
|
130
|
-
{ type: 'work_unit_started', unitKey: workUnit.unitKey, skills: []
|
|
130
|
+
{ type: 'work_unit_started', unitKey: workUnit.unitKey, skills: [] },
|
|
131
131
|
...workUnit.actions.map((action) => ({
|
|
132
132
|
type: 'candidate_action',
|
|
133
133
|
unitKey: workUnit.unitKey,
|
|
@@ -64,17 +64,22 @@ const memoryFlowEventSchema = z.discriminatedUnion('type', [
|
|
|
64
64
|
message: z.string().min(1),
|
|
65
65
|
transient: z.boolean().optional(),
|
|
66
66
|
}),
|
|
67
|
+
eventSchema({
|
|
68
|
+
type: z.literal('rate_limit_wait'),
|
|
69
|
+
provider: z.string(),
|
|
70
|
+
rateLimitType: z.string().optional(),
|
|
71
|
+
resumeAtMs: z.number().int().nonnegative(),
|
|
72
|
+
remainingMs: z.number().int().nonnegative(),
|
|
73
|
+
}),
|
|
67
74
|
eventSchema({
|
|
68
75
|
type: z.literal('work_unit_started'),
|
|
69
76
|
unitKey: z.string().min(1),
|
|
70
77
|
skills: z.array(z.string().min(1)),
|
|
71
|
-
stepBudget: z.number().int().min(0),
|
|
72
78
|
}),
|
|
73
79
|
eventSchema({
|
|
74
80
|
type: z.literal('work_unit_step'),
|
|
75
81
|
unitKey: z.string().min(1),
|
|
76
|
-
|
|
77
|
-
stepBudget: z.number().int().min(0),
|
|
82
|
+
toolCalls: z.number().int().min(0),
|
|
78
83
|
}),
|
|
79
84
|
eventSchema({
|
|
80
85
|
type: z.literal('candidate_action'),
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
abortSignal: ctx.abortSignal,
|
|
15
15
|
});
|
|
16
16
|
return { skipped: false, stopReason: run.stopReason, error: run.error, ...(run.metrics ? { metrics: run.metrics } : {}) };
|
|
17
17
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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>;
|