@kaelio/ktx 0.11.0 → 0.12.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.11.0-py3-none-any.whl → kaelio_ktx-0.12.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin.js +1 -1
- package/dist/clack.d.ts +16 -0
- package/dist/clack.js +37 -6
- package/dist/claude-code-prompt-caching.js +1 -1
- package/dist/cli-program.js +3 -3
- package/dist/cli-runtime.js +2 -2
- package/dist/commands/connection-commands.js +1 -1
- package/dist/commands/ingest-commands.js +4 -4
- package/dist/commands/mcp-commands.js +12 -12
- package/dist/commands/runtime-commands.js +4 -4
- package/dist/commands/setup-commands.js +6 -5
- package/dist/commands/sl-commands.js +1 -1
- package/dist/commands/sql-commands.js +1 -1
- package/dist/commands/status-commands.js +1 -1
- package/dist/connection.js +1 -1
- package/dist/connectors/clickhouse/connector.js +1 -1
- package/dist/connectors/mysql/connector.js +1 -1
- package/dist/connectors/snowflake/connector.d.ts +1 -1
- package/dist/connectors/sqlite/connector.js +2 -25
- package/dist/connectors/sqlserver/connector.js +3 -3
- package/dist/context/connections/connection-type.d.ts +1 -1
- package/dist/context/connections/read-only-sql.d.ts +1 -0
- package/dist/context/connections/read-only-sql.js +116 -2
- package/dist/context/core/git.service.d.ts +23 -0
- package/dist/context/core/git.service.js +71 -8
- package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
- package/dist/context/ingest/adapters/looker/client.js +7 -2
- package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
- package/dist/context/ingest/adapters/looker/factory.js +9 -0
- package/dist/context/ingest/adapters/looker/mapping.js +1 -1
- package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.js +1 -1
- package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
- package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
- package/dist/context/ingest/artifact-gates.d.ts +2 -6
- package/dist/context/ingest/artifact-gates.js +5 -47
- package/dist/context/ingest/constrained-repair.d.ts +55 -0
- package/dist/context/ingest/constrained-repair.js +167 -0
- package/dist/context/ingest/final-gate-repair.d.ts +9 -11
- package/dist/context/ingest/final-gate-repair.js +40 -128
- package/dist/context/ingest/finalization-scope.d.ts +1 -1
- package/dist/context/ingest/finalization-scope.js +15 -15
- package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
- package/dist/context/ingest/ingest-bundle.runner.js +101 -67
- package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
- package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
- package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
- package/dist/context/ingest/local-bundle-runtime.js +9 -10
- package/dist/context/ingest/local-ingest.d.ts +2 -0
- package/dist/context/ingest/local-ingest.js +2 -0
- package/dist/context/ingest/memory-flow/view-model.js +1 -1
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
- package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
- package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
- package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
- package/dist/context/llm/ai-sdk-runtime.js +2 -2
- package/dist/context/llm/claude-code-runtime.js +1 -1
- package/dist/context/llm/local-config.js +1 -1
- package/dist/context/llm/runtime-tools.js +2 -2
- package/dist/context/mcp/context-tools.js +7 -7
- package/dist/context/mcp/local-project-ports.js +23 -54
- package/dist/context/memory/local-memory.js +4 -1
- package/dist/context/memory/memory-agent.service.js +1 -1
- package/dist/context/project/config.d.ts +11 -4
- package/dist/context/project/config.js +85 -30
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/project/mappings-yaml-schema.js +2 -2
- package/dist/context/project/project.js +12 -4
- package/dist/context/scan/description-generation.js +4 -4
- package/dist/context/scan/local-enrichment-artifacts.js +2 -1
- package/dist/context/scan/local-scan.js +2 -2
- package/dist/context/scan/local-structural-artifacts.js +5 -5
- package/dist/context/scan/relationship-benchmark-report.js +1 -1
- package/dist/context/scan/relationship-discovery.js +3 -3
- package/dist/context/scan/relationship-llm-proposal.js +3 -3
- package/dist/context/sl/local-query.js +3 -33
- package/dist/context/sl/local-sl.d.ts +0 -8
- package/dist/context/sl/local-sl.js +44 -69
- package/dist/context/sl/semantic-layer.service.d.ts +25 -8
- package/dist/context/sl/semantic-layer.service.js +109 -56
- package/dist/context/sl/source-files.d.ts +46 -0
- package/dist/context/sl/source-files.js +131 -0
- package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
- package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
- package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
- package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
- package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
- package/dist/context/sql-analysis/dialect.d.ts +2 -0
- package/dist/context/sql-analysis/dialect.js +20 -0
- package/dist/context/tools/base-tool.d.ts +6 -19
- package/dist/context/tools/base-tool.js +0 -14
- package/dist/context-build-view.js +5 -5
- package/dist/database-tree-picker.js +18 -3
- package/dist/demo-assets.js +0 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -23
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +44 -0
- package/dist/ingest.d.ts +1 -1
- package/dist/ingest.js +8 -2
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- package/dist/io/tty.d.ts +8 -0
- package/dist/io/tty.js +16 -0
- package/dist/llm/embedding-health.js +1 -1
- package/dist/llm/embedding-provider.js +3 -3
- package/dist/llm/model-provider.js +1 -1
- package/dist/local-adapters.d.ts +1 -0
- package/dist/local-adapters.js +2 -2
- package/dist/local-scan-connectors.js +1 -1
- package/dist/managed-local-embeddings.js +17 -8
- package/dist/managed-mcp-daemon.js +3 -3
- package/dist/managed-python-command.d.ts +7 -0
- package/dist/managed-python-command.js +34 -8
- package/dist/managed-python-daemon.js +2 -2
- package/dist/managed-python-http.js +3 -3
- package/dist/managed-python-runtime.d.ts +30 -1
- package/dist/managed-python-runtime.js +134 -18
- package/dist/managed-uv-release.d.ts +7 -0
- package/dist/managed-uv-release.js +11 -0
- package/dist/mcp-http-server.js +4 -4
- package/dist/mcp-server-factory.js +3 -3
- package/dist/mcp-stdio-server.js +1 -1
- package/dist/memory-flow-hud.js +2 -2
- package/dist/next-steps.js +2 -2
- package/dist/prompt-navigation.d.ts +17 -0
- package/dist/prompt-navigation.js +49 -3
- package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
- package/dist/prompts/memory_agent_external_ingest.md +2 -2
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.js +3 -3
- package/dist/release-version.js +1 -1
- package/dist/runtime-requirements.js +1 -1
- package/dist/runtime.js +9 -9
- package/dist/scan.js +1 -1
- package/dist/setup-agents.js +21 -30
- package/dist/setup-banner.d.ts +20 -0
- package/dist/setup-banner.js +39 -0
- package/dist/setup-context.js +24 -15
- package/dist/setup-databases.js +31 -59
- package/dist/setup-demo-tour.js +12 -8
- package/dist/setup-embeddings.js +9 -9
- package/dist/setup-interrupt.js +1 -1
- package/dist/setup-models.d.ts +4 -1
- package/dist/setup-models.js +54 -28
- package/dist/setup-project.js +29 -5
- package/dist/setup-prompts.js +16 -1
- package/dist/setup-ready-menu.js +1 -1
- package/dist/setup-sources.js +27 -7
- package/dist/setup.js +13 -13
- package/dist/skills/analytics/SKILL.md +3 -3
- package/dist/skills/dbt_ingest/SKILL.md +3 -3
- package/dist/skills/looker_ingest/SKILL.md +3 -3
- package/dist/skills/lookml_ingest/SKILL.md +7 -7
- package/dist/skills/metabase_ingest/SKILL.md +4 -4
- package/dist/skills/metricflow_ingest/SKILL.md +15 -15
- package/dist/skills/notion_synthesize/SKILL.md +1 -1
- package/dist/skills/sl/SKILL.md +3 -3
- package/dist/skills/sl_capture/SKILL.md +1 -1
- package/dist/skills/wiki_capture/SKILL.md +1 -1
- package/dist/source-mapping.js +1 -1
- package/dist/startup-profile.js +1 -1
- package/dist/status-project.d.ts +0 -2
- package/dist/status-project.js +4 -6
- package/dist/telemetry/events.d.ts +1 -1
- package/dist/telemetry/exception.js +14 -0
- package/dist/text-ingest.js +1 -1
- package/dist/tree-picker-tui.d.ts +0 -1
- package/dist/tree-picker-tui.js +2 -3
- package/package.json +1 -1
|
@@ -20,6 +20,23 @@ export interface WorktreeEntry {
|
|
|
20
20
|
branch: string | null;
|
|
21
21
|
head: string | null;
|
|
22
22
|
}
|
|
23
|
+
export type KtxRepoOwnership = 'unowned' | 'ktx-managed' | 'foreign';
|
|
24
|
+
export declare class KtxForeignGitRepositoryError extends Error {
|
|
25
|
+
constructor(configDir: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify whether ktx may own a git repository rooted exactly at `dir`. A root
|
|
29
|
+
* `ktx.yaml` is the ownership signal; the working tree decides, not git history,
|
|
30
|
+
* because older ktx versions left `ktx.yaml` uncommitted (it holds secret refs).
|
|
31
|
+
*
|
|
32
|
+
* - `unowned`: no repo here (including a missing or non-directory path) → ktx may `git init`.
|
|
33
|
+
* - `ktx-managed`: `<dir>/.git` is a directory and `ktx.yaml` sits at the root.
|
|
34
|
+
* - `foreign`: any other repo — no root `ktx.yaml`, or a `.git` *file* (a linked
|
|
35
|
+
* worktree). ktx must never adopt or mutate it.
|
|
36
|
+
*
|
|
37
|
+
* Reads only `<dir>` itself; never walks up, so a parent repo cannot change the answer.
|
|
38
|
+
*/
|
|
39
|
+
export declare function classifyKtxRepoOwnership(dir: string): Promise<KtxRepoOwnership>;
|
|
23
40
|
export type SquashMergeResult = {
|
|
24
41
|
ok: true;
|
|
25
42
|
squashSha: string;
|
|
@@ -102,6 +119,12 @@ export declare class GitService {
|
|
|
102
119
|
path: string;
|
|
103
120
|
}>>;
|
|
104
121
|
changedPaths(): Promise<string[]>;
|
|
122
|
+
/**
|
|
123
|
+
* List all paths matching `pathSpec` as they exist at `commitHash`. Reads from
|
|
124
|
+
* git object storage, so it's safe against concurrent working-tree mutations
|
|
125
|
+
* and can recover paths (e.g. a human-renamed file) that no longer exist on disk.
|
|
126
|
+
*/
|
|
127
|
+
listFilesAtCommit(pathSpec: string, commitHash: string): Promise<string[]>;
|
|
105
128
|
/**
|
|
106
129
|
* List all paths under the working tree that match `pathSpec`, scoped to HEAD.
|
|
107
130
|
* Used for the reconciler's first-ever run when there's no watermark to diff from.
|
|
@@ -2,6 +2,53 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { noopLogger, resolveConfigDir } from './config.js';
|
|
4
4
|
import { createSimpleGit } from './git-env.js';
|
|
5
|
+
export class KtxForeignGitRepositoryError extends Error {
|
|
6
|
+
constructor(configDir) {
|
|
7
|
+
super(`${configDir} is already a git repository that ktx did not create. ` +
|
|
8
|
+
'ktx maintains its context in a repository it owns; run ktx in a dedicated directory or move the existing repository aside.');
|
|
9
|
+
this.name = 'KtxForeignGitRepositoryError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function isNodeErrnoException(error) {
|
|
13
|
+
return error instanceof Error && 'code' in error;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Classify whether ktx may own a git repository rooted exactly at `dir`. A root
|
|
17
|
+
* `ktx.yaml` is the ownership signal; the working tree decides, not git history,
|
|
18
|
+
* because older ktx versions left `ktx.yaml` uncommitted (it holds secret refs).
|
|
19
|
+
*
|
|
20
|
+
* - `unowned`: no repo here (including a missing or non-directory path) → ktx may `git init`.
|
|
21
|
+
* - `ktx-managed`: `<dir>/.git` is a directory and `ktx.yaml` sits at the root.
|
|
22
|
+
* - `foreign`: any other repo — no root `ktx.yaml`, or a `.git` *file* (a linked
|
|
23
|
+
* worktree). ktx must never adopt or mutate it.
|
|
24
|
+
*
|
|
25
|
+
* Reads only `<dir>` itself; never walks up, so a parent repo cannot change the answer.
|
|
26
|
+
*/
|
|
27
|
+
export async function classifyKtxRepoOwnership(dir) {
|
|
28
|
+
let dotGitIsDirectory;
|
|
29
|
+
try {
|
|
30
|
+
dotGitIsDirectory = (await fs.lstat(join(dir, '.git'))).isDirectory();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// ENOENT: `<dir>/.git` is absent. ENOTDIR: `<dir>` itself is a file, so it
|
|
34
|
+
// can hold no repo. Either way there is nothing for ktx to avoid here.
|
|
35
|
+
if (isNodeErrnoException(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
|
|
36
|
+
return 'unowned';
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (!dotGitIsDirectory) {
|
|
41
|
+
return 'foreign';
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
// stat (not lstat): follow symlinks, matching what `loadKtxProject`'s
|
|
45
|
+
// readFile accepts — a dir that loads as a ktx project classifies as one.
|
|
46
|
+
return (await fs.stat(join(dir, 'ktx.yaml'))).isFile() ? 'ktx-managed' : 'foreign';
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return 'foreign';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
5
52
|
function mergeErrorMessage(error) {
|
|
6
53
|
if (error instanceof Error) {
|
|
7
54
|
return error.message;
|
|
@@ -58,12 +105,15 @@ export class GitService {
|
|
|
58
105
|
}
|
|
59
106
|
async initialize() {
|
|
60
107
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
108
|
+
const ownership = await classifyKtxRepoOwnership(this.configDir);
|
|
109
|
+
if (ownership === 'foreign') {
|
|
110
|
+
throw new KtxForeignGitRepositoryError(this.configDir);
|
|
111
|
+
}
|
|
112
|
+
if (ownership === 'unowned') {
|
|
64
113
|
await this.git.init();
|
|
65
|
-
this.logger.log('Initialized git repository');
|
|
114
|
+
this.logger.log('Initialized ktx-managed git repository');
|
|
66
115
|
}
|
|
116
|
+
// ownership === 'ktx-managed' → ktx's own repo; proceed with the normal re-run path.
|
|
67
117
|
// Keep any auto-maintenance triggered by writes in-process. Detached maintenance can
|
|
68
118
|
// keep object-pack directories alive briefly after awaited git commands complete,
|
|
69
119
|
// which makes temp-project cleanup flaky in CI.
|
|
@@ -81,6 +131,11 @@ export class GitService {
|
|
|
81
131
|
}
|
|
82
132
|
}
|
|
83
133
|
catch (error) {
|
|
134
|
+
// The foreign-repo error is already typed and actionable; surface it verbatim so every
|
|
135
|
+
// command that loads the project shows the same clear guidance instead of a generic wrapper.
|
|
136
|
+
if (error instanceof KtxForeignGitRepositoryError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
84
139
|
this.logger.error('Failed to initialize git repository', error);
|
|
85
140
|
// Preserve the underlying git error: the generic message alone is undiagnosable in
|
|
86
141
|
// telemetry and unactionable for the user. The exception reporter walks `cause` and
|
|
@@ -442,12 +497,13 @@ export class GitService {
|
|
|
442
497
|
return [...new Set(paths)].sort();
|
|
443
498
|
}
|
|
444
499
|
/**
|
|
445
|
-
* List all paths
|
|
446
|
-
*
|
|
500
|
+
* List all paths matching `pathSpec` as they exist at `commitHash`. Reads from
|
|
501
|
+
* git object storage, so it's safe against concurrent working-tree mutations
|
|
502
|
+
* and can recover paths (e.g. a human-renamed file) that no longer exist on disk.
|
|
447
503
|
*/
|
|
448
|
-
async
|
|
504
|
+
async listFilesAtCommit(pathSpec, commitHash) {
|
|
449
505
|
try {
|
|
450
|
-
const raw = await this.git.raw(['ls-tree', '-r', '-z', '--name-only',
|
|
506
|
+
const raw = await this.git.raw(['ls-tree', '-r', '-z', '--name-only', commitHash, '--', pathSpec]);
|
|
451
507
|
if (!raw) {
|
|
452
508
|
return [];
|
|
453
509
|
}
|
|
@@ -457,6 +513,13 @@ export class GitService {
|
|
|
457
513
|
return [];
|
|
458
514
|
}
|
|
459
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* List all paths under the working tree that match `pathSpec`, scoped to HEAD.
|
|
518
|
+
* Used for the reconciler's first-ever run when there's no watermark to diff from.
|
|
519
|
+
*/
|
|
520
|
+
async listFilesAtHead(pathSpec) {
|
|
521
|
+
return this.listFilesAtCommit(pathSpec, 'HEAD');
|
|
522
|
+
}
|
|
460
523
|
/**
|
|
461
524
|
* Collapse all commits between `preHead` and current HEAD into a single commit with the given
|
|
462
525
|
* message. Used by the memory agent to squash N per-tool-call commits into one ingest commit.
|
|
@@ -2,6 +2,7 @@ import { access, mkdir, readdir, readFile, rename, writeFile } from 'node:fs/pro
|
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
4
|
import { rawSourcesDirForSync } from '../../raw-sources-paths.js';
|
|
5
|
+
import { isSlYamlPath } from '../../../sl/source-files.js';
|
|
5
6
|
import { mergeUsagePreservingExternal } from '../live-database/manifest.js';
|
|
6
7
|
import { historicSqlEvidenceEnvelopeSchema } from './evidence.js';
|
|
7
8
|
import { stagedManifestSchema } from './types.js';
|
|
@@ -197,7 +198,7 @@ export async function projectHistoricSqlEvidence(input) {
|
|
|
197
198
|
const tableEvidence = evidence.filter((entry) => entry.kind === 'table_usage');
|
|
198
199
|
const patternEvidence = evidence.filter((entry) => entry.kind === 'pattern');
|
|
199
200
|
const schemaRoot = join(input.workdir, 'semantic-layer', input.connectionId, '_schema');
|
|
200
|
-
for (const file of (await walkFiles(schemaRoot)).filter(
|
|
201
|
+
for (const file of (await walkFiles(schemaRoot)).filter(isSlYamlPath)) {
|
|
201
202
|
const path = join(schemaRoot, file);
|
|
202
203
|
const before = await readFile(path, 'utf-8');
|
|
203
204
|
const shard = (YAML.parse(before) ?? {});
|
|
@@ -12,13 +12,18 @@ const defaultLogger = {
|
|
|
12
12
|
class InlineLookerSettings extends NodeSettings {
|
|
13
13
|
params;
|
|
14
14
|
constructor(params) {
|
|
15
|
-
|
|
15
|
+
// @looker/sdk-rtl boundary: NodeSettings consumes a string-valued config
|
|
16
|
+
// section (read back via the readConfig override below), but its constructor
|
|
17
|
+
// is typed to accept a fully-realized IApiSettings. The string record is the
|
|
18
|
+
// shape the library actually reads, so narrow to IApiSection first.
|
|
19
|
+
const settings = {
|
|
16
20
|
base_url: normalizeBaseUrl(params.base_url),
|
|
17
21
|
client_id: params.client_id,
|
|
18
22
|
client_secret: params.client_secret, // pragma: allowlist secret
|
|
19
23
|
verify_ssl: 'true',
|
|
20
24
|
timeout: '120',
|
|
21
|
-
}
|
|
25
|
+
};
|
|
26
|
+
super('', settings);
|
|
22
27
|
this.params = params;
|
|
23
28
|
}
|
|
24
29
|
readConfig(_section) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FetchContext } from '../../types.js';
|
|
2
|
-
import { type LookerClientDeps, type LookerConnectionParams } from './client.js';
|
|
2
|
+
import { LookerClient, type LookerClientDeps, type LookerConnectionParams } from './client.js';
|
|
3
3
|
import type { LookerClientFactory, LookerRuntimeClient } from './fetch.js';
|
|
4
4
|
import type { LookerPullConfig } from './types.js';
|
|
5
5
|
export interface LookerCredentialResolver {
|
|
@@ -14,6 +14,13 @@ export declare class DefaultLookerConnectionClientFactory implements LookerConne
|
|
|
14
14
|
private readonly deps;
|
|
15
15
|
constructor(resolver: LookerCredentialResolver, deps?: LookerClientDeps);
|
|
16
16
|
createClient(lookerConnectionId: string): Promise<LookerRuntimeClient>;
|
|
17
|
+
/**
|
|
18
|
+
* Like {@link createClient} but preserves the concrete {@link LookerClient}
|
|
19
|
+
* type, so callers that need methods outside the `LookerRuntimeClient`
|
|
20
|
+
* contract (e.g. `listLookerConnections`, `testConnection`) keep them without
|
|
21
|
+
* a cast.
|
|
22
|
+
*/
|
|
23
|
+
createLookerClient(lookerConnectionId: string): Promise<LookerClient>;
|
|
17
24
|
}
|
|
18
25
|
/** @internal */
|
|
19
26
|
export declare class DefaultLookerClientFactory implements LookerClientFactory {
|
|
@@ -7,6 +7,15 @@ export class DefaultLookerConnectionClientFactory {
|
|
|
7
7
|
this.deps = deps;
|
|
8
8
|
}
|
|
9
9
|
async createClient(lookerConnectionId) {
|
|
10
|
+
return this.createLookerClient(lookerConnectionId);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Like {@link createClient} but preserves the concrete {@link LookerClient}
|
|
14
|
+
* type, so callers that need methods outside the `LookerRuntimeClient`
|
|
15
|
+
* contract (e.g. `listLookerConnections`, `testConnection`) keep them without
|
|
16
|
+
* a cast.
|
|
17
|
+
*/
|
|
18
|
+
async createLookerClient(lookerConnectionId) {
|
|
10
19
|
const credentials = await this.resolver.resolve(lookerConnectionId);
|
|
11
20
|
return new LookerClient(credentials, this.deps);
|
|
12
21
|
}
|
|
@@ -120,7 +120,7 @@ export function validateLookerMappings(args) {
|
|
|
120
120
|
if (!args.knownKtxConnectionIds.has(mapping.ktxConnectionId)) {
|
|
121
121
|
errors.push({
|
|
122
122
|
key: mapping.lookerConnectionName,
|
|
123
|
-
reason: `
|
|
123
|
+
reason: `ktx connection ${mapping.ktxConnectionId} does not exist`,
|
|
124
124
|
});
|
|
125
125
|
continue;
|
|
126
126
|
}
|
|
@@ -12,6 +12,7 @@ export declare const lookerPullConfigSchema: z.ZodObject<{
|
|
|
12
12
|
lookUpdatedSince: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
|
|
13
13
|
connectionMappings: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
14
14
|
connectionTypes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
15
|
+
PLAIN: "PLAIN";
|
|
15
16
|
BIGQUERY: "BIGQUERY";
|
|
16
17
|
SNOWFLAKE: "SNOWFLAKE";
|
|
17
18
|
MYSQL: "MYSQL";
|
|
@@ -31,7 +32,6 @@ export declare const lookerPullConfigSchema: z.ZodObject<{
|
|
|
31
32
|
METABASE: "METABASE";
|
|
32
33
|
LOOKER: "LOOKER";
|
|
33
34
|
NOTION: "NOTION";
|
|
34
|
-
PLAIN: "PLAIN";
|
|
35
35
|
BETTERSTACK: "BETTERSTACK";
|
|
36
36
|
}>>>;
|
|
37
37
|
parsedTargetTables: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -18,7 +18,7 @@ export declare const DEFAULT_METABASE_CLIENT_CONFIG: MetabaseClientConfig;
|
|
|
18
18
|
* Strip Metabase `[[ ... {{ var }} ... ]]` optional-clause blocks from native SQL.
|
|
19
19
|
*
|
|
20
20
|
* The bracketed blocks are emitted only when the embedded `{{ var }}` is supplied at
|
|
21
|
-
* Metabase query time. For
|
|
21
|
+
* Metabase query time. For ktx semantic-layer ingest there's no such runtime
|
|
22
22
|
* parameter — chat-time filters are composed by the SL query planner — so the optional
|
|
23
23
|
* block must be removed before the SQL becomes a permanent SL source. Substituting a
|
|
24
24
|
* dummy value (the alternative) bakes a placeholder filter into the source and silently
|
|
@@ -30,7 +30,7 @@ class MetabaseApiError extends Error {
|
|
|
30
30
|
* Strip Metabase `[[ ... {{ var }} ... ]]` optional-clause blocks from native SQL.
|
|
31
31
|
*
|
|
32
32
|
* The bracketed blocks are emitted only when the embedded `{{ var }}` is supplied at
|
|
33
|
-
* Metabase query time. For
|
|
33
|
+
* Metabase query time. For ktx semantic-layer ingest there's no such runtime
|
|
34
34
|
* parameter — chat-time filters are composed by the SL query planner — so the optional
|
|
35
35
|
* block must be removed before the SQL becomes a permanent SL source. Substituting a
|
|
36
36
|
* dummy value (the alternative) bakes a placeholder filter into the source and silently
|
|
@@ -15,7 +15,7 @@ export function metabaseRuntimeConfigFromLocalConnection(connectionId, connectio
|
|
|
15
15
|
throw new Error(`Connection "${connectionId}" is not a Metabase connection`);
|
|
16
16
|
}
|
|
17
17
|
if (hasNetworkProxy(connection)) {
|
|
18
|
-
throw new Error(`Standalone
|
|
18
|
+
throw new Error(`Standalone ktx does not support proxy-bearing Metabase connections yet. Use hosted Metabase ingest for "${connectionId}" until the ktx Metabase proxy support spec lands.`);
|
|
19
19
|
}
|
|
20
20
|
const apiUrl = stringField(connection.api_url);
|
|
21
21
|
const literalApiKey = stringField(connection.api_key);
|
|
@@ -93,7 +93,7 @@ export function validateMetabaseMappings(args) {
|
|
|
93
93
|
continue;
|
|
94
94
|
}
|
|
95
95
|
if (!args.knownKtxConnectionIds.has(connectionId)) {
|
|
96
|
-
errors.push({ key, reason: `
|
|
96
|
+
errors.push({ key, reason: `ktx connection ${connectionId} does not exist` });
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
@@ -108,13 +108,13 @@ export function validateMappingPhysicalMatch(mapping, target) {
|
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
110
110
|
if (target.connection_type !== expectedType) {
|
|
111
|
-
return `Metabase database engine '${engine}' does not match
|
|
111
|
+
return `Metabase database engine '${engine}' does not match ktx connection type '${target.connection_type}'`;
|
|
112
112
|
}
|
|
113
113
|
const metabaseDb = normalizeName(mapping.metabaseDbName);
|
|
114
114
|
const targetDb = normalizeName(getTargetDatabase(target));
|
|
115
115
|
if (engine === 'snowflake' || engine === 'bigquery' || engine === 'bigquery-cloud-sdk') {
|
|
116
116
|
if (metabaseDb && targetDb && metabaseDb !== targetDb) {
|
|
117
|
-
return `Metabase database '${mapping.metabaseDbName}' does not match
|
|
117
|
+
return `Metabase database '${mapping.metabaseDbName}' does not match ktx connection database '${displayValue(getTargetDatabase(target))}'`;
|
|
118
118
|
}
|
|
119
119
|
return null;
|
|
120
120
|
}
|
|
@@ -122,10 +122,10 @@ export function validateMappingPhysicalMatch(mapping, target) {
|
|
|
122
122
|
const metabaseHost = normalizeHost(mapping.metabaseHost);
|
|
123
123
|
const targetHost = normalizeHost(target.host);
|
|
124
124
|
if (metabaseHost && targetHost && metabaseHost !== targetHost) {
|
|
125
|
-
return `Metabase host '${mapping.metabaseHost}' does not match
|
|
125
|
+
return `Metabase host '${mapping.metabaseHost}' does not match ktx connection host '${displayValue(target.host)}'`;
|
|
126
126
|
}
|
|
127
127
|
if (metabaseDb && targetDb && metabaseDb !== targetDb) {
|
|
128
|
-
return `Metabase database '${mapping.metabaseDbName}' does not match
|
|
128
|
+
return `Metabase database '${mapping.metabaseDbName}' does not match ktx connection database '${displayValue(getTargetDatabase(target))}'`;
|
|
129
129
|
}
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
@@ -157,7 +157,7 @@ export async function refreshMetabaseMapping(args) {
|
|
|
157
157
|
if (!target) {
|
|
158
158
|
physicalMismatches.push({
|
|
159
159
|
mappingId: String(mapping.id),
|
|
160
|
-
reason: `
|
|
160
|
+
reason: `ktx connection ${mapping.ktxConnectionId} does not exist`,
|
|
161
161
|
});
|
|
162
162
|
continue;
|
|
163
163
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import type { SemanticLayerService } from '../../context/sl/semantic-layer.service.js';
|
|
2
2
|
import type { TouchedSlSource } from '../../context/tools/touched-sl-sources.js';
|
|
3
3
|
import type { KnowledgeWikiService } from '../../context/wiki/knowledge-wiki.service.js';
|
|
4
|
-
|
|
5
|
-
invalidSources: string[];
|
|
6
|
-
validSources: string[];
|
|
7
|
-
}
|
|
4
|
+
import type { WuValidationResult } from './stages/validate-wu-sources.js';
|
|
8
5
|
export interface FinalArtifactGateInput {
|
|
9
6
|
connectionIds: string[];
|
|
10
7
|
changedWikiPageKeys: string[];
|
|
11
8
|
touchedSlSources: TouchedSlSource[];
|
|
12
9
|
wikiService: KnowledgeWikiService;
|
|
13
10
|
semanticLayerService: SemanticLayerService;
|
|
14
|
-
validateTouchedSources(touched: TouchedSlSource[]): Promise<
|
|
11
|
+
validateTouchedSources(touched: TouchedSlSource[]): Promise<WuValidationResult>;
|
|
15
12
|
tableExists(connectionId: string, tableRef: string): Promise<boolean>;
|
|
16
13
|
}
|
|
17
14
|
export interface ProvenanceRawPathValidationInput {
|
|
@@ -23,4 +20,3 @@ export interface ProvenanceRawPathValidationInput {
|
|
|
23
20
|
}
|
|
24
21
|
export declare function validateFinalIngestArtifacts(input: FinalArtifactGateInput): Promise<void>;
|
|
25
22
|
export declare function validateProvenanceRawPaths(input: ProvenanceRawPathValidationInput): void;
|
|
26
|
-
export {};
|
|
@@ -13,50 +13,6 @@ function slEntityNames(source) {
|
|
|
13
13
|
...(source.segments ?? []).map((segment) => segment.name),
|
|
14
14
|
]);
|
|
15
15
|
}
|
|
16
|
-
function uniqueTouchedSources(sources) {
|
|
17
|
-
const seen = new Set();
|
|
18
|
-
const unique = [];
|
|
19
|
-
for (const source of sources) {
|
|
20
|
-
const key = `${source.connectionId}:${source.sourceName}`;
|
|
21
|
-
if (seen.has(key)) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
seen.add(key);
|
|
25
|
-
unique.push(source);
|
|
26
|
-
}
|
|
27
|
-
return unique.sort((left, right) => {
|
|
28
|
-
const byConnection = left.connectionId.localeCompare(right.connectionId);
|
|
29
|
-
return byConnection === 0 ? left.sourceName.localeCompare(right.sourceName) : byConnection;
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
async function expandTouchedSlSourcesWithDirectJoinNeighbors(input) {
|
|
33
|
-
const expanded = [...input.touchedSlSources];
|
|
34
|
-
const touchedByConnection = new Map();
|
|
35
|
-
for (const source of input.touchedSlSources) {
|
|
36
|
-
const bucket = touchedByConnection.get(source.connectionId) ?? new Set();
|
|
37
|
-
bucket.add(source.sourceName);
|
|
38
|
-
touchedByConnection.set(source.connectionId, bucket);
|
|
39
|
-
}
|
|
40
|
-
for (const connectionId of input.connectionIds) {
|
|
41
|
-
const touched = touchedByConnection.get(connectionId);
|
|
42
|
-
if (!touched || touched.size === 0) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const { sources } = await input.semanticLayerService.loadAllSources(connectionId);
|
|
46
|
-
for (const source of sources) {
|
|
47
|
-
const sourceIsTouched = touched.has(source.name);
|
|
48
|
-
if (sourceIsTouched) {
|
|
49
|
-
for (const join of source.joins ?? []) {
|
|
50
|
-
expanded.push({ connectionId, sourceName: join.to });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if ((source.joins ?? []).some((join) => touched.has(join.to))) {
|
|
54
|
-
expanded.push({ connectionId, sourceName: source.name });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return uniqueTouchedSources(expanded);
|
|
59
|
-
}
|
|
60
16
|
async function validateWikiSlRefs(input) {
|
|
61
17
|
const errors = [];
|
|
62
18
|
const sourcesByConnection = new Map();
|
|
@@ -112,9 +68,11 @@ async function validateWikiRefs(input) {
|
|
|
112
68
|
return dangling;
|
|
113
69
|
}
|
|
114
70
|
export async function validateFinalIngestArtifacts(input) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
71
|
+
// Join-neighbor expansion happens inside validateTouchedSources so work-unit
|
|
72
|
+
// validation and this gate check the same set — a source that passes one
|
|
73
|
+
// passes the other.
|
|
74
|
+
const validation = await input.validateTouchedSources(input.touchedSlSources);
|
|
75
|
+
const errors = validation.invalidSources.map((invalid) => `semantic-layer validation failed for ${invalid.source}: ${invalid.errors.join('; ')}`);
|
|
118
76
|
errors.push(...(await validateWikiSlRefs(input)));
|
|
119
77
|
const danglingWikiRefs = await validateWikiRefs(input);
|
|
120
78
|
if (danglingWikiRefs.length > 0) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AgentRunnerPort, KtxRuntimeToolSet } from '../../context/llm/runtime-port.js';
|
|
2
|
+
import type { IngestTraceWriter } from './ingest-trace.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared loop for the two integration-time repair agents (semantic gate
|
|
5
|
+
* repair, textual conflict resolution). Success is decided by re-running the
|
|
6
|
+
* failed check — `verify` — never by whether the agent edited files: an
|
|
7
|
+
* ineffective edit fails, and an explicit no-change declaration that verifies
|
|
8
|
+
* succeeds.
|
|
9
|
+
*/
|
|
10
|
+
export type RepairVerification = {
|
|
11
|
+
ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
export type ConstrainedRepairResult = {
|
|
17
|
+
status: 'repaired';
|
|
18
|
+
attempts: number;
|
|
19
|
+
changedPaths: string[];
|
|
20
|
+
} | {
|
|
21
|
+
status: 'failed';
|
|
22
|
+
attempts: number;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
export interface ConstrainedRepairToolContext {
|
|
26
|
+
workdir: string;
|
|
27
|
+
allowedPaths: ReadonlySet<string>;
|
|
28
|
+
editedPaths: Set<string>;
|
|
29
|
+
declareNoChange(reason: string): void;
|
|
30
|
+
}
|
|
31
|
+
export interface ConstrainedRepairLoopInput {
|
|
32
|
+
agentRunner: AgentRunnerPort;
|
|
33
|
+
workdir: string;
|
|
34
|
+
allowedPaths: string[];
|
|
35
|
+
trace: IngestTraceWriter;
|
|
36
|
+
tracePhase: string;
|
|
37
|
+
traceEventName: string;
|
|
38
|
+
traceData: Record<string, unknown>;
|
|
39
|
+
systemPrompt: string;
|
|
40
|
+
buildUserPrompt(input: {
|
|
41
|
+
attempt: number;
|
|
42
|
+
maxAttempts: number;
|
|
43
|
+
previousFailure: string | null;
|
|
44
|
+
}): string;
|
|
45
|
+
buildExtraTools?(context: ConstrainedRepairToolContext): KtxRuntimeToolSet;
|
|
46
|
+
verify(changedPaths: string[]): Promise<RepairVerification>;
|
|
47
|
+
/** Failure reason when an attempt neither edits nor declares no-change. */
|
|
48
|
+
noChangeFailureReason: string;
|
|
49
|
+
telemetryTags: Record<string, string>;
|
|
50
|
+
maxAttempts?: number;
|
|
51
|
+
stepBudget?: number;
|
|
52
|
+
abortSignal?: AbortSignal;
|
|
53
|
+
}
|
|
54
|
+
export declare function buildDeleteRepairFileTool(context: ConstrainedRepairToolContext): KtxRuntimeToolSet;
|
|
55
|
+
export declare function runConstrainedRepairLoop(input: ConstrainedRepairLoopInput): Promise<ConstrainedRepairResult>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { traceTimed } from './ingest-trace.js';
|
|
5
|
+
const readRepairFileSchema = z.object({
|
|
6
|
+
path: z.string().min(1),
|
|
7
|
+
});
|
|
8
|
+
const writeRepairFileSchema = z.object({
|
|
9
|
+
path: z.string().min(1),
|
|
10
|
+
content: z.string(),
|
|
11
|
+
});
|
|
12
|
+
function normalizeRepoPath(path) {
|
|
13
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
14
|
+
const parts = normalized.split('/').filter((part) => part.length > 0);
|
|
15
|
+
if (parts.length === 0 || parts.some((part) => part === '.' || part === '..')) {
|
|
16
|
+
throw new Error(`repair path must be a repository-relative path: ${path}`);
|
|
17
|
+
}
|
|
18
|
+
return parts.join('/');
|
|
19
|
+
}
|
|
20
|
+
function assertAllowedPath(path, allowedPaths) {
|
|
21
|
+
const normalized = normalizeRepoPath(path);
|
|
22
|
+
if (!allowedPaths.has(normalized)) {
|
|
23
|
+
throw new Error(`repair path not allowed: ${normalized}`);
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
async function readOptionalFile(path) {
|
|
28
|
+
try {
|
|
29
|
+
return { exists: true, content: await readFile(path, 'utf-8') };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
33
|
+
return { exists: false, content: '' };
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function buildRepairFileTools(context) {
|
|
39
|
+
return {
|
|
40
|
+
read_repair_file: {
|
|
41
|
+
name: 'read_repair_file',
|
|
42
|
+
description: 'Read one allowed file from the integration worktree.',
|
|
43
|
+
inputSchema: readRepairFileSchema,
|
|
44
|
+
execute: async ({ path }) => {
|
|
45
|
+
const normalized = assertAllowedPath(path, context.allowedPaths);
|
|
46
|
+
const file = await readOptionalFile(join(context.workdir, normalized));
|
|
47
|
+
return {
|
|
48
|
+
markdown: file.exists ? file.content : `(missing file: ${normalized})`,
|
|
49
|
+
structured: { path: normalized, exists: file.exists },
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
write_repair_file: {
|
|
54
|
+
name: 'write_repair_file',
|
|
55
|
+
description: 'Replace one allowed integration worktree file with repaired text content.',
|
|
56
|
+
inputSchema: writeRepairFileSchema,
|
|
57
|
+
execute: async ({ path, content }) => {
|
|
58
|
+
const normalized = assertAllowedPath(path, context.allowedPaths);
|
|
59
|
+
const fullPath = join(context.workdir, normalized);
|
|
60
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
61
|
+
await writeFile(fullPath, content, 'utf-8');
|
|
62
|
+
context.editedPaths.add(normalized);
|
|
63
|
+
return {
|
|
64
|
+
markdown: `Wrote ${normalized}`,
|
|
65
|
+
structured: { path: normalized, bytes: Buffer.byteLength(content) },
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function buildDeleteRepairFileTool(context) {
|
|
72
|
+
const deleteRepairFileSchema = z.object({
|
|
73
|
+
path: z.string().min(1),
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
delete_repair_file: {
|
|
77
|
+
name: 'delete_repair_file',
|
|
78
|
+
description: 'Delete one allowed integration worktree file when the failed patch proves the deletion is correct.',
|
|
79
|
+
inputSchema: deleteRepairFileSchema,
|
|
80
|
+
execute: async ({ path }) => {
|
|
81
|
+
const normalized = assertAllowedPath(path, context.allowedPaths);
|
|
82
|
+
await rm(join(context.workdir, normalized), { force: true });
|
|
83
|
+
context.editedPaths.add(normalized);
|
|
84
|
+
return {
|
|
85
|
+
markdown: `Deleted ${normalized}`,
|
|
86
|
+
structured: { path: normalized },
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export async function runConstrainedRepairLoop(input) {
|
|
93
|
+
const allowedPaths = new Set(input.allowedPaths.map(normalizeRepoPath));
|
|
94
|
+
const sortedAllowedPaths = [...allowedPaths].sort();
|
|
95
|
+
const maxAttempts = input.maxAttempts ?? 2;
|
|
96
|
+
const stepBudget = input.stepBudget ?? 16;
|
|
97
|
+
// Edits persist in the worktree across attempts, so the verified set and the
|
|
98
|
+
// reported changedPaths accumulate over the whole loop.
|
|
99
|
+
const editedPaths = new Set();
|
|
100
|
+
let lastFailure = 'repair did not run';
|
|
101
|
+
let previousFailure = null;
|
|
102
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
103
|
+
let noChangeDeclaration = null;
|
|
104
|
+
const toolContext = {
|
|
105
|
+
workdir: input.workdir,
|
|
106
|
+
allowedPaths,
|
|
107
|
+
editedPaths,
|
|
108
|
+
declareNoChange: (reason) => {
|
|
109
|
+
noChangeDeclaration = reason;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const traceData = {
|
|
113
|
+
...input.traceData,
|
|
114
|
+
attempt,
|
|
115
|
+
maxAttempts,
|
|
116
|
+
allowedPaths: sortedAllowedPaths,
|
|
117
|
+
};
|
|
118
|
+
const result = await traceTimed(input.trace, input.tracePhase, input.traceEventName, traceData, async () => input.agentRunner.runLoop({
|
|
119
|
+
modelRole: 'repair',
|
|
120
|
+
systemPrompt: input.systemPrompt,
|
|
121
|
+
userPrompt: input.buildUserPrompt({ attempt, maxAttempts, previousFailure }),
|
|
122
|
+
toolSet: {
|
|
123
|
+
...buildRepairFileTools(toolContext),
|
|
124
|
+
...(input.buildExtraTools?.(toolContext) ?? {}),
|
|
125
|
+
},
|
|
126
|
+
stepBudget,
|
|
127
|
+
telemetryTags: input.telemetryTags,
|
|
128
|
+
abortSignal: input.abortSignal,
|
|
129
|
+
}));
|
|
130
|
+
if (result.stopReason === 'error') {
|
|
131
|
+
lastFailure = result.error?.message ?? 'repair agent loop errored';
|
|
132
|
+
previousFailure = lastFailure;
|
|
133
|
+
await input.trace.event('error', input.tracePhase, `${input.traceEventName}_failed`, traceData, result.error);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const changedPaths = [...editedPaths].sort();
|
|
137
|
+
if (changedPaths.length === 0 && noChangeDeclaration === null) {
|
|
138
|
+
// Nothing changed and nothing was claimed: the failed check would fail
|
|
139
|
+
// identically, so skip verification and retry.
|
|
140
|
+
lastFailure = input.noChangeFailureReason;
|
|
141
|
+
previousFailure = lastFailure;
|
|
142
|
+
await input.trace.event('error', input.tracePhase, `${input.traceEventName}_failed`, {
|
|
143
|
+
...traceData,
|
|
144
|
+
reason: lastFailure,
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const verification = await input.verify(changedPaths);
|
|
149
|
+
if (!verification.ok) {
|
|
150
|
+
lastFailure = verification.reason;
|
|
151
|
+
previousFailure = lastFailure;
|
|
152
|
+
await input.trace.event('error', input.tracePhase, `${input.traceEventName}_failed`, {
|
|
153
|
+
...traceData,
|
|
154
|
+
changedPaths,
|
|
155
|
+
reason: lastFailure,
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
await input.trace.event('debug', input.tracePhase, `${input.traceEventName}_repaired`, {
|
|
160
|
+
...traceData,
|
|
161
|
+
changedPaths,
|
|
162
|
+
...(noChangeDeclaration !== null ? { noChangeDeclaration } : {}),
|
|
163
|
+
});
|
|
164
|
+
return { status: 'repaired', attempts: attempt, changedPaths };
|
|
165
|
+
}
|
|
166
|
+
return { status: 'failed', attempts: maxAttempts, reason: lastFailure };
|
|
167
|
+
}
|