@duckcodeailabs/dql-agent 1.8.2 → 1.8.3
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/dist/agent-run-engine.d.ts +1 -1
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +2 -1
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-store.d.ts +33 -0
- package/dist/agent-run-store.d.ts.map +1 -0
- package/dist/agent-run-store.js +167 -0
- package/dist/agent-run-store.js.map +1 -0
- package/dist/answer-loop.d.ts +10 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +109 -12
- package/dist/answer-loop.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/metadata/analytical-policy.d.ts +40 -2
- package/dist/metadata/analytical-policy.d.ts.map +1 -1
- package/dist/metadata/analytical-policy.js +276 -53
- package/dist/metadata/analytical-policy.js.map +1 -1
- package/dist/metadata/block-fit.js +9 -1
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +10 -3
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/dbt-first-safety.d.ts +8 -1
- package/dist/metadata/dbt-first-safety.d.ts.map +1 -1
- package/dist/metadata/dbt-first-safety.js +6 -1
- package/dist/metadata/dbt-first-safety.js.map +1 -1
- package/dist/metadata/health.d.ts +19 -0
- package/dist/metadata/health.d.ts.map +1 -0
- package/dist/metadata/health.js +72 -0
- package/dist/metadata/health.js.map +1 -0
- package/dist/metadata/sql-context-validation.js +27 -1
- package/dist/metadata/sql-context-validation.js.map +1 -1
- package/dist/tools/registry.js +9 -9
- package/dist/tools/registry.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -26,7 +26,7 @@ import { createContextLedger } from './grounding/context-ledger.js';
|
|
|
26
26
|
import { validateAnswerResultShape } from './answer-shape.js';
|
|
27
27
|
import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
|
|
28
28
|
import { evaluateDbtFirstGeneratedSql } from './metadata/dbt-first-safety.js';
|
|
29
|
-
import { planAnalyticalPath } from './metadata/analytical-policy.js';
|
|
29
|
+
import { planAnalyticalPath, humanizeAnalyticalEntityId, analyticalPolicyUserFacingReason, } from './metadata/analytical-policy.js';
|
|
30
30
|
import { planCertifiedAdaptation } from './metadata/block-adapt.js';
|
|
31
31
|
import { compactSqlSnippet, extractSimpleSelectShape, selectExpressionOutputName, } from './metadata/sql-shape.js';
|
|
32
32
|
import { composeSemanticQueryForQuestion, composeSemanticQueryFromMembers, } from './semantic-bridge/compose.js';
|
|
@@ -158,6 +158,35 @@ const BUSINESS_CONTEXT_KINDS = ['term', 'business_view', 'domain', 'skill', 'rel
|
|
|
158
158
|
const ARTIFACT_KINDS = [...EXECUTABLE_ARTIFACT_KINDS, ...BUSINESS_CONTEXT_KINDS];
|
|
159
159
|
const SEMANTIC_KINDS = ['metric', 'dimension', 'measure', 'entity', 'semantic_model', 'saved_query'];
|
|
160
160
|
const MANIFEST_KINDS = ['dbt_model', 'dbt_source'];
|
|
161
|
+
/**
|
|
162
|
+
* Business-language explanation for a failed SQL context validation. The chat
|
|
163
|
+
* surface shows THIS; the validator's machine message (relation ids, repair
|
|
164
|
+
* tool guidance) belongs in refusalDetails/validationWarnings only.
|
|
165
|
+
*/
|
|
166
|
+
function renderContextValidationRefusalForUser(code, machineError, memberBindings) {
|
|
167
|
+
switch (code) {
|
|
168
|
+
case 'unknown_relation':
|
|
169
|
+
return 'I drafted a query, but it uses a table that was not part of the metadata retrieved for this question, so I did not run it. Ask again (retrieval usually finds it on a follow-up), name the table or metric explicitly, or re-sync the dbt metadata.';
|
|
170
|
+
case 'unknown_column':
|
|
171
|
+
return 'I drafted a query, but it references a column that is not in the inspected metadata for those tables, so I did not run it. Name the exact field you mean, or re-sync the dbt metadata if the column is new.';
|
|
172
|
+
case 'misbound_filter': {
|
|
173
|
+
const binding = memberBindings?.[0];
|
|
174
|
+
const target = binding ? `${humanizeAnalyticalEntityId(binding.dimension)} "${binding.values[0] ?? ''}"` : 'the requested value';
|
|
175
|
+
return `I need to filter this answer to ${target}, but the query I drafted did not apply that filter to a matching column, so I did not run it. Tell me which column identifies ${binding ? humanizeAnalyticalEntityId(binding.dimension) : 'that value'}, or rephrase with the exact field name.`;
|
|
176
|
+
}
|
|
177
|
+
case 'ambiguous_filter':
|
|
178
|
+
return 'The value you asked about matches more than one column in the inspected data, so I did not guess. Tell me which field it belongs to and I will run it.';
|
|
179
|
+
case 'missing_baseline':
|
|
180
|
+
return 'This comparison needs a baseline period or value that the drafted query did not include. Say what to compare against (for example, the prior month) and I will run it.';
|
|
181
|
+
case 'unsafe_sql':
|
|
182
|
+
return 'The drafted query used a statement type that is not allowed in this governed preview, so I did not run it.';
|
|
183
|
+
case 'insufficient_context':
|
|
184
|
+
default:
|
|
185
|
+
return machineError.trim().length > 0 && !/inspect_metadata_context/i.test(machineError)
|
|
186
|
+
? `I could not prepare a governed query yet: ${machineError.replace(/\s*Use inspect_metadata_context[^.]*\.\s*$/i, '').trim()}`
|
|
187
|
+
: 'I could not prepare a governed query from the retrieved metadata. Name the specific metric or table and how to break it down, and I can generate a review-required draft.';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
161
190
|
function refusalCodeForValidation(code) {
|
|
162
191
|
if (code === 'unknown_relation' || code === 'unknown_column' || code === 'insufficient_context' || code === 'missing_baseline') {
|
|
163
192
|
return 'grounding_gap';
|
|
@@ -187,6 +216,22 @@ function refusalCodeForAnalyticalPolicy(code, hasExploratoryCandidate = false) {
|
|
|
187
216
|
* denials and must remain terminal in this loop.
|
|
188
217
|
*/
|
|
189
218
|
function exploratoryCandidateFromDbtFirstGuard(sql, decision) {
|
|
219
|
+
// The policy service's disposition is authoritative: a declared draft path
|
|
220
|
+
// re-bound to this SQL's actual joins enters the bounded lane directly.
|
|
221
|
+
if (decision.disposition === 'exploratory_candidate') {
|
|
222
|
+
return {
|
|
223
|
+
kind: 'dbt_grounded_exploration',
|
|
224
|
+
reason: 'relationship_not_certified',
|
|
225
|
+
sql,
|
|
226
|
+
message: decision.technicalDetail ?? decision.message
|
|
227
|
+
?? 'This query follows a declared but uncertified DQL relationship path.',
|
|
228
|
+
userFacingReason: decision.userFacingReason,
|
|
229
|
+
modeledEntityIds: decision.entities,
|
|
230
|
+
relationshipIds: decision.relationshipIds,
|
|
231
|
+
exploratoryPath: decision.exploratoryPath,
|
|
232
|
+
executionStatus: 'not_executed',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
190
235
|
const reason = decision.code;
|
|
191
236
|
const missingPathWithoutUnsafeProof = reason === 'unsafe_relationship' && decision.relationshipIds.length === 0;
|
|
192
237
|
if (decision.safe || (!missingPathWithoutUnsafeProof
|
|
@@ -207,6 +252,7 @@ function exploratoryCandidateFromDbtFirstGuard(sql, decision) {
|
|
|
207
252
|
sql,
|
|
208
253
|
message: decision.message
|
|
209
254
|
?? 'This query is grounded in dbt metadata but has no certified DQL relationship path yet.',
|
|
255
|
+
userFacingReason: decision.userFacingReason,
|
|
210
256
|
modeledEntityIds: decision.entities,
|
|
211
257
|
relationshipIds: decision.relationshipIds,
|
|
212
258
|
executionStatus: 'not_executed',
|
|
@@ -1209,7 +1255,12 @@ async function runAnswerLoop(input) {
|
|
|
1209
1255
|
});
|
|
1210
1256
|
parsed = parseProposal(proposed);
|
|
1211
1257
|
}
|
|
1212
|
-
catch {
|
|
1258
|
+
catch (err) {
|
|
1259
|
+
// Deadline/cancellation must surface as itself, never as a policy refusal.
|
|
1260
|
+
if (input.signal?.aborted)
|
|
1261
|
+
throw input.signal.reason ?? err;
|
|
1262
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
|
|
1263
|
+
throw err;
|
|
1213
1264
|
// keep the original decline; fall through to the honest no_answer.
|
|
1214
1265
|
}
|
|
1215
1266
|
}
|
|
@@ -1222,9 +1273,15 @@ async function runAnswerLoop(input) {
|
|
|
1222
1273
|
// consistent, actionable message so the same question yields the same outcome
|
|
1223
1274
|
// every run and every surface — instead of passing through the model's varying
|
|
1224
1275
|
// text. A genuinely context-less ask keeps the plain honest message.
|
|
1276
|
+
// The user-facing text is BUSINESS language; the machine policy detail
|
|
1277
|
+
// (qualified ids, codes) lives only in refusalDetails + validationWarnings.
|
|
1225
1278
|
const declinedDespiteContext = wantsGeneratedData && hasGeneratableContext;
|
|
1279
|
+
const policyDetail = analyticalPlan?.safe === false
|
|
1280
|
+
? analyticalPlan.technicalDetail ?? analyticalPlan.message ?? 'DQL could not prove a safe analytical relationship path.'
|
|
1281
|
+
: undefined;
|
|
1226
1282
|
const text = analyticalPlan?.safe === false
|
|
1227
|
-
? analyticalPlan.
|
|
1283
|
+
? analyticalPlan.userFacingReason
|
|
1284
|
+
?? analyticalPolicyUserFacingReason(analyticalPlan.code ?? 'unsafe_relationship', analyticalPlan.entities)
|
|
1228
1285
|
: declinedDespiteContext
|
|
1229
1286
|
? 'I could not compose a governed query for this from the available tables and metrics. This usually needs a clearer join path or an explicit metric and grouping — name the specific measure and how to break it down, and I can generate a review-required draft.'
|
|
1230
1287
|
: parsed.text || 'No answer (the model declined to propose SQL).';
|
|
@@ -1236,14 +1293,17 @@ async function runAnswerLoop(input) {
|
|
|
1236
1293
|
confidence: 0.1,
|
|
1237
1294
|
text,
|
|
1238
1295
|
refusalCode: analyticalPlan?.safe === false
|
|
1239
|
-
?
|
|
1296
|
+
? analyticalPlan.disposition === 'exploratory_candidate'
|
|
1297
|
+
? 'modeling_gap'
|
|
1298
|
+
: refusalCodeForAnalyticalPolicy(analyticalPlan.code, analyticalPlanAllowsDbtExploration(analyticalPlan))
|
|
1240
1299
|
: 'model_declined',
|
|
1241
1300
|
refusalDetails: {
|
|
1242
1301
|
code: analyticalPlan?.safe === false
|
|
1243
1302
|
? analyticalPlan.code ?? 'unsafe_relationship'
|
|
1244
1303
|
: 'model_declined',
|
|
1245
|
-
message: text,
|
|
1304
|
+
message: policyDetail ?? text,
|
|
1246
1305
|
},
|
|
1306
|
+
validationWarnings: policyDetail ? [`Analytical policy detail: ${policyDetail}`] : undefined,
|
|
1247
1307
|
answer: text,
|
|
1248
1308
|
citations: [],
|
|
1249
1309
|
memoryContext: input.memoryContext,
|
|
@@ -1448,7 +1508,11 @@ async function runAnswerLoop(input) {
|
|
|
1448
1508
|
};
|
|
1449
1509
|
}
|
|
1450
1510
|
}
|
|
1451
|
-
catch {
|
|
1511
|
+
catch (err) {
|
|
1512
|
+
if (input.signal?.aborted)
|
|
1513
|
+
throw input.signal.reason ?? err;
|
|
1514
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
|
|
1515
|
+
throw err;
|
|
1452
1516
|
// Re-grounding is best-effort; the bounded self-repair below still runs.
|
|
1453
1517
|
}
|
|
1454
1518
|
}
|
|
@@ -1503,7 +1567,11 @@ async function runAnswerLoop(input) {
|
|
|
1503
1567
|
}
|
|
1504
1568
|
}
|
|
1505
1569
|
}
|
|
1506
|
-
catch {
|
|
1570
|
+
catch (err) {
|
|
1571
|
+
if (input.signal?.aborted)
|
|
1572
|
+
throw input.signal.reason ?? err;
|
|
1573
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
|
|
1574
|
+
throw err;
|
|
1507
1575
|
// Repair is best-effort — the refusal below stays the honest fallback.
|
|
1508
1576
|
}
|
|
1509
1577
|
}
|
|
@@ -1523,7 +1591,10 @@ async function runAnswerLoop(input) {
|
|
|
1523
1591
|
}
|
|
1524
1592
|
}
|
|
1525
1593
|
if (!contextValidation.ok) {
|
|
1526
|
-
|
|
1594
|
+
// Business-language chat text; the validator's machine message (with
|
|
1595
|
+
// relation ids and tool guidance for the repair prompt) stays in
|
|
1596
|
+
// refusalDetails + validationWarnings for the Inspect surface.
|
|
1597
|
+
const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings);
|
|
1527
1598
|
const analysisPlan = buildAnalysisPlan({
|
|
1528
1599
|
question,
|
|
1529
1600
|
intent,
|
|
@@ -1557,7 +1628,11 @@ async function runAnswerLoop(input) {
|
|
|
1557
1628
|
trustLabel: input.contextPack?.trustLabel,
|
|
1558
1629
|
sourceCertifiedBlock: followUpSourceBlock?.name ?? input.followUp?.sourceBlockName,
|
|
1559
1630
|
contextPackId: input.contextPack?.id,
|
|
1560
|
-
validationWarnings: [
|
|
1631
|
+
validationWarnings: [
|
|
1632
|
+
...contextValidation.warnings,
|
|
1633
|
+
`SQL context validation detail: ${contextValidation.error}`,
|
|
1634
|
+
...deepCandidateNotes,
|
|
1635
|
+
],
|
|
1561
1636
|
selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
|
|
1562
1637
|
citations: [],
|
|
1563
1638
|
memoryContext: input.memoryContext,
|
|
@@ -1582,8 +1657,12 @@ async function runAnswerLoop(input) {
|
|
|
1582
1657
|
? evaluateDbtFirstGeneratedSql(parsed.sql, input.manifest, input.domainContext?.purpose, input.domainContext)
|
|
1583
1658
|
: undefined;
|
|
1584
1659
|
if (dbtFirstJoinSafety && !dbtFirstJoinSafety.safe) {
|
|
1585
|
-
|
|
1660
|
+
// Business-language text for the chat surface; the machine policy message
|
|
1661
|
+
// stays in refusalDetails + validationWarnings for Inspect.
|
|
1662
|
+
const policyDetail = dbtFirstJoinSafety.technicalDetail ?? dbtFirstJoinSafety.message
|
|
1586
1663
|
?? 'DQL could not prove this generated join from certified analytical relationships.';
|
|
1664
|
+
const text = dbtFirstJoinSafety.userFacingReason
|
|
1665
|
+
?? analyticalPolicyUserFacingReason(dbtFirstJoinSafety.code ?? 'unsafe_relationship', dbtFirstJoinSafety.entities);
|
|
1587
1666
|
const exploratoryCandidate = exploratoryCandidateFromDbtFirstGuard(parsed.sql, dbtFirstJoinSafety);
|
|
1588
1667
|
const analysisPlan = buildAnalysisPlan({
|
|
1589
1668
|
question,
|
|
@@ -1611,13 +1690,16 @@ async function runAnswerLoop(input) {
|
|
|
1611
1690
|
proposedSql: parsed.sql,
|
|
1612
1691
|
sql: parsed.sql,
|
|
1613
1692
|
...(exploratoryCandidate ? { exploratoryCandidate } : {}),
|
|
1614
|
-
refusalCode:
|
|
1693
|
+
refusalCode: dbtFirstJoinSafety.disposition === 'exploratory_candidate'
|
|
1694
|
+
? 'modeling_gap'
|
|
1695
|
+
: refusalCodeForAnalyticalPolicy(dbtFirstJoinSafety.code, Boolean(exploratoryCandidate)),
|
|
1615
1696
|
refusalDetails: {
|
|
1616
1697
|
code: dbtFirstJoinSafety.code ?? 'unsafe_relationship',
|
|
1617
|
-
message:
|
|
1698
|
+
message: policyDetail,
|
|
1618
1699
|
},
|
|
1619
1700
|
validationWarnings: [
|
|
1620
1701
|
...contextValidation.warnings,
|
|
1702
|
+
`Analytical policy detail: ${policyDetail}`,
|
|
1621
1703
|
`DQL v3 relationship guard: ${dbtFirstJoinSafety.code ?? 'unsafe relationship'}.`,
|
|
1622
1704
|
...(exploratoryCandidate
|
|
1623
1705
|
? ['A bounded DBT-grounded exploratory route may validate this missing modeled relationship; governed SQL was not executed.']
|
|
@@ -2272,10 +2354,21 @@ function renderAnalyticalPlanPrompt(plan) {
|
|
|
2272
2354
|
return undefined;
|
|
2273
2355
|
if (!plan.safe) {
|
|
2274
2356
|
if (analyticalPlanAllowsDbtExploration(plan)) {
|
|
2357
|
+
const declaredPath = plan.exploratoryPath?.edges.length
|
|
2358
|
+
? [
|
|
2359
|
+
'DECLARED (UNCERTIFIED) DRAFT JOIN PATH — suggestion only, NOT authorization:',
|
|
2360
|
+
...plan.exploratoryPath.edges.map((edge, index) => [
|
|
2361
|
+
`${index + 1}. ${edge.fromEntity} (${edge.fromRelation ?? 'bound dbt model'}) -> ${edge.toEntity} (${edge.toRelation ?? 'bound dbt model'})`,
|
|
2362
|
+
` relationship=${edge.relationshipId}; keys=${edge.keys.map((key) => `${key.from}=${key.to}`).join(', ')}; cardinality=${edge.cardinality}; fanout=${edge.fanout}; status=${edge.lifecycle}`,
|
|
2363
|
+
].join('\n')),
|
|
2364
|
+
'Prefer these declared relations and exact key pairs over inferring joins from column names. The result remains analyst-review-required.',
|
|
2365
|
+
]
|
|
2366
|
+
: [];
|
|
2275
2367
|
return [
|
|
2276
2368
|
'DQL GOVERNED RELATIONSHIP COVERAGE: MISSING.',
|
|
2277
2369
|
plan.message ?? 'No certified analytical relationship path is available.',
|
|
2278
2370
|
`Policy code: ${plan.code ?? 'unsafe_relationship'}.`,
|
|
2371
|
+
...declaredPath,
|
|
2279
2372
|
'This is not authorization for governed SQL. You MAY propose one bounded, read-only DBT-grounded exploratory SELECT/WITH using only the inspected relations and columns supplied below.',
|
|
2280
2373
|
'Use explicit aliases and equality join keys visible in the inspected schema. Preserve the requested member bindings exactly. Do not introduce an uninspected relation, a cross-domain path, an attribution/allocation rule, or more than four joins.',
|
|
2281
2374
|
'The host will independently parse, bound, probe, validate, and execute the candidate as analyst-review-required. If the inspected evidence cannot express the request, return no SQL.',
|
|
@@ -2301,8 +2394,12 @@ function renderAnalyticalPlanPrompt(plan) {
|
|
|
2301
2394
|
/**
|
|
2302
2395
|
* Missing modeling may enter the narrow EXP-001 host-validated lane. Explicit
|
|
2303
2396
|
* unsafe, cross-domain, attribution, expiry, purpose, and proof failures never do.
|
|
2397
|
+
* The planner's `disposition` is authoritative; the code checks remain only for
|
|
2398
|
+
* plans produced by older manifests/paths that predate the disposition contract.
|
|
2304
2399
|
*/
|
|
2305
2400
|
function analyticalPlanAllowsDbtExploration(plan) {
|
|
2401
|
+
if (plan.disposition === 'exploratory_candidate')
|
|
2402
|
+
return true;
|
|
2306
2403
|
if (plan.safe)
|
|
2307
2404
|
return false;
|
|
2308
2405
|
if (plan.code === 'unbound_relation' || plan.code === 'relationship_not_certified')
|