@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.
Files changed (39) hide show
  1. package/dist/agent-run-engine.d.ts +1 -1
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-gates.d.ts.map +1 -1
  4. package/dist/agent-run-gates.js +2 -1
  5. package/dist/agent-run-gates.js.map +1 -1
  6. package/dist/agent-run-store.d.ts +33 -0
  7. package/dist/agent-run-store.d.ts.map +1 -0
  8. package/dist/agent-run-store.js +167 -0
  9. package/dist/agent-run-store.js.map +1 -0
  10. package/dist/answer-loop.d.ts +10 -2
  11. package/dist/answer-loop.d.ts.map +1 -1
  12. package/dist/answer-loop.js +109 -12
  13. package/dist/answer-loop.js.map +1 -1
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/metadata/analytical-policy.d.ts +40 -2
  19. package/dist/metadata/analytical-policy.d.ts.map +1 -1
  20. package/dist/metadata/analytical-policy.js +276 -53
  21. package/dist/metadata/analytical-policy.js.map +1 -1
  22. package/dist/metadata/block-fit.js +9 -1
  23. package/dist/metadata/block-fit.js.map +1 -1
  24. package/dist/metadata/catalog.d.ts.map +1 -1
  25. package/dist/metadata/catalog.js +10 -3
  26. package/dist/metadata/catalog.js.map +1 -1
  27. package/dist/metadata/dbt-first-safety.d.ts +8 -1
  28. package/dist/metadata/dbt-first-safety.d.ts.map +1 -1
  29. package/dist/metadata/dbt-first-safety.js +6 -1
  30. package/dist/metadata/dbt-first-safety.js.map +1 -1
  31. package/dist/metadata/health.d.ts +19 -0
  32. package/dist/metadata/health.d.ts.map +1 -0
  33. package/dist/metadata/health.js +72 -0
  34. package/dist/metadata/health.js.map +1 -0
  35. package/dist/metadata/sql-context-validation.js +27 -1
  36. package/dist/metadata/sql-context-validation.js.map +1 -1
  37. package/dist/tools/registry.js +9 -9
  38. package/dist/tools/registry.js.map +1 -1
  39. package/package.json +4 -4
@@ -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.message ?? 'DQL could not prove a safe analytical relationship path.'
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
- ? refusalCodeForAnalyticalPolicy(analyticalPlan.code)
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
- const text = `I could not safely prepare this review-required DQL artifact from the inspected context. The SQL preview failed validation: ${contextValidation.error}`;
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: [...contextValidation.warnings, ...deepCandidateNotes],
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
- const text = dbtFirstJoinSafety.message
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: refusalCodeForAnalyticalPolicy(dbtFirstJoinSafety.code, Boolean(exploratoryCandidate)),
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: text,
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')