@wrenai/genbi 0.0.4 → 0.0.6

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 (234) hide show
  1. package/README.md +65 -3
  2. package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
  3. package/dist/assets/index-A_Wrq7Ey.css +1 -0
  4. package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
  5. package/dist/index.html +2 -2
  6. package/dist-server/harness/bundle/schema.d.ts +38 -0
  7. package/dist-server/harness/bundle/schema.js +18 -10
  8. package/dist-server/harness/bundle/version.js +1 -1
  9. package/dist-server/harness/cli-args.d.ts +10 -2
  10. package/dist-server/harness/cli-args.js +24 -8
  11. package/dist-server/harness/cli.js +75 -9
  12. package/dist-server/harness/compile/cache.js +4 -4
  13. package/dist-server/harness/compile/compose-profile.d.ts +51 -14
  14. package/dist-server/harness/compile/compose-profile.js +85 -19
  15. package/dist-server/harness/compile/context-loader.d.ts +56 -0
  16. package/dist-server/harness/compile/context-loader.js +237 -0
  17. package/dist-server/harness/compile/errors.d.ts +19 -0
  18. package/dist-server/harness/compile/errors.js +35 -0
  19. package/dist-server/harness/compile/index.d.ts +4 -3
  20. package/dist-server/harness/compile/index.js +4 -3
  21. package/dist-server/harness/compile/pipeline.d.ts +15 -7
  22. package/dist-server/harness/compile/pipeline.js +62 -14
  23. package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
  24. package/dist-server/harness/compile/resolve-binary.js +3 -3
  25. package/dist-server/harness/compile/types.d.ts +29 -0
  26. package/dist-server/harness/compile/warble-identity.d.ts +11 -0
  27. package/dist-server/harness/compile/warble-identity.js +13 -0
  28. package/dist-server/harness/components/ai-step.d.ts +4 -0
  29. package/dist-server/harness/components/ai-step.js +40 -0
  30. package/dist-server/harness/components/broker.d.ts +60 -0
  31. package/dist-server/harness/components/broker.js +117 -0
  32. package/dist-server/harness/components/capability-card.d.ts +98 -0
  33. package/dist-server/harness/components/capability-card.js +169 -0
  34. package/dist-server/harness/components/context.d.ts +2 -0
  35. package/dist-server/harness/components/context.js +30 -0
  36. package/dist-server/harness/components/display.d.ts +6 -0
  37. package/dist-server/harness/components/display.js +29 -0
  38. package/dist-server/harness/components/egress-judge.d.ts +12 -0
  39. package/dist-server/harness/components/egress-judge.js +27 -0
  40. package/dist-server/harness/components/egress.d.ts +142 -0
  41. package/dist-server/harness/components/egress.js +469 -0
  42. package/dist-server/harness/components/in-process.d.ts +7 -0
  43. package/dist-server/harness/components/in-process.js +245 -0
  44. package/dist-server/harness/components/normalize.d.ts +35 -0
  45. package/dist-server/harness/components/normalize.js +245 -0
  46. package/dist-server/harness/components/plan.d.ts +31 -0
  47. package/dist-server/harness/components/plan.js +184 -0
  48. package/dist-server/harness/components/report.d.ts +207 -0
  49. package/dist-server/harness/components/report.js +309 -0
  50. package/dist-server/harness/components/runner.d.ts +193 -0
  51. package/dist-server/harness/components/runner.js +492 -0
  52. package/dist-server/harness/components/semantic-guards.d.ts +20 -0
  53. package/dist-server/harness/components/semantic-guards.js +42 -0
  54. package/dist-server/harness/components/terminal-json.d.ts +18 -0
  55. package/dist-server/harness/components/terminal-json.js +82 -0
  56. package/dist-server/harness/components/usage.d.ts +14 -0
  57. package/dist-server/harness/components/usage.js +19 -0
  58. package/dist-server/harness/components/wren-access.d.ts +29 -0
  59. package/dist-server/harness/components/wren-access.js +233 -0
  60. package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
  61. package/dist-server/harness/components/zone-dry-run.js +107 -0
  62. package/dist-server/harness/components/zone-gate.d.ts +76 -0
  63. package/dist-server/harness/components/zone-gate.js +144 -0
  64. package/dist-server/harness/events/index.d.ts +1 -1
  65. package/dist-server/harness/events/types.d.ts +52 -0
  66. package/dist-server/harness/index.d.ts +9 -2
  67. package/dist-server/harness/index.js +5 -1
  68. package/dist-server/harness/loop/executor.d.ts +1 -0
  69. package/dist-server/harness/loop/executor.js +37 -15
  70. package/dist-server/harness/npm-package-resolve.d.ts +2 -0
  71. package/dist-server/harness/npm-package-resolve.js +16 -4
  72. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
  73. package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
  74. package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
  75. package/dist-server/harness/providers/adapters/openai.js +6 -0
  76. package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
  77. package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
  78. package/dist-server/harness/providers/binding.d.ts +45 -4
  79. package/dist-server/harness/providers/binding.js +44 -8
  80. package/dist-server/harness/providers/index.d.ts +9 -3
  81. package/dist-server/harness/providers/index.js +10 -3
  82. package/dist-server/harness/providers/zone.d.ts +96 -0
  83. package/dist-server/harness/providers/zone.js +112 -0
  84. package/dist-server/harness/render/answering-query.d.ts +29 -0
  85. package/dist-server/harness/render/answering-query.js +62 -0
  86. package/dist-server/harness/render/envelope.d.ts +6 -0
  87. package/dist-server/harness/render/envelope.js +10 -3
  88. package/dist-server/harness/route/adapter-spec.d.ts +8 -9
  89. package/dist-server/harness/route/adapter-spec.js +13 -14
  90. package/dist-server/harness/route/codex-ask-events.js +2 -1
  91. package/dist-server/harness/route/codex-ask.d.ts +4 -0
  92. package/dist-server/harness/route/codex-ask.js +43 -45
  93. package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
  94. package/dist-server/harness/route/codex-local-manifest.js +96 -15
  95. package/dist-server/harness/route/describe.js +45 -6
  96. package/dist-server/harness/route/dispatched.d.ts +20 -0
  97. package/dist-server/harness/route/dispatched.js +122 -24
  98. package/dist-server/harness/route/host-query.d.ts +93 -0
  99. package/dist-server/harness/route/host-query.js +257 -0
  100. package/dist-server/harness/route/in-process.d.ts +22 -1
  101. package/dist-server/harness/route/in-process.js +54 -4
  102. package/dist-server/harness/route/route.js +8 -2
  103. package/dist-server/harness/route/types.d.ts +47 -1
  104. package/dist-server/harness/route/wren-host-proxy.d.ts +33 -0
  105. package/dist-server/harness/route/wren-host-proxy.js +228 -0
  106. package/dist-server/harness/session/run.js +30 -10
  107. package/dist-server/harness/setup/runner.d.ts +17 -3
  108. package/dist-server/harness/setup/runner.js +272 -89
  109. package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
  110. package/dist-server/harness/setup/yaml-scalar.js +62 -0
  111. package/dist-server/harness/tools/setup-native.d.ts +7 -0
  112. package/dist-server/harness/tools/setup-native.js +7 -4
  113. package/dist-server/server/adopt.js +1 -17
  114. package/dist-server/server/app.d.ts +1 -0
  115. package/dist-server/server/app.js +175 -17
  116. package/dist-server/server/auth-choice.d.ts +13 -1
  117. package/dist-server/server/auth-choice.js +66 -1
  118. package/dist-server/server/bin.js +185 -25
  119. package/dist-server/server/codex-native-session.d.ts +25 -0
  120. package/dist-server/server/codex-native-session.js +1 -0
  121. package/dist-server/server/codex-runtime-composition.d.ts +34 -0
  122. package/dist-server/server/codex-runtime-composition.js +250 -0
  123. package/dist-server/server/codex-structured-ask.d.ts +23 -0
  124. package/dist-server/server/codex-structured-ask.js +102 -0
  125. package/dist-server/server/conn-config.js +4 -16
  126. package/dist-server/server/conversation-socket.d.ts +16 -0
  127. package/dist-server/server/conversation-socket.js +65 -0
  128. package/dist-server/server/db.d.ts +46 -0
  129. package/dist-server/server/db.js +88 -9
  130. package/dist-server/server/env-detect.d.ts +6 -0
  131. package/dist-server/server/env-detect.js +7 -0
  132. package/dist-server/server/fold.d.ts +23 -4
  133. package/dist-server/server/fold.js +42 -5
  134. package/dist-server/server/harness.d.ts +2 -2
  135. package/dist-server/server/harness.js +33 -7
  136. package/dist-server/server/host-evidence-grounding.d.ts +21 -0
  137. package/dist-server/server/host-evidence-grounding.js +93 -0
  138. package/dist-server/server/interactive-terminal.d.ts +10 -6
  139. package/dist-server/server/interactive-terminal.js +33 -8
  140. package/dist-server/server/managed-wren-runtime.d.ts +116 -0
  141. package/dist-server/server/managed-wren-runtime.js +670 -0
  142. package/dist-server/server/native-artifacts.d.ts +112 -7
  143. package/dist-server/server/native-artifacts.js +194 -14
  144. package/dist-server/server/native-component-context.d.ts +19 -0
  145. package/dist-server/server/native-component-context.js +76 -0
  146. package/dist-server/server/native-component-preparation.d.ts +20 -0
  147. package/dist-server/server/native-component-preparation.js +129 -0
  148. package/dist-server/server/native-component-runtime.d.ts +36 -0
  149. package/dist-server/server/native-component-runtime.js +92 -0
  150. package/dist-server/server/native-components.d.ts +84 -0
  151. package/dist-server/server/native-components.js +160 -0
  152. package/dist-server/server/native-dispatch-process.d.ts +8 -0
  153. package/dist-server/server/native-dispatch-process.js +65 -0
  154. package/dist-server/server/native-runtime-spec.d.ts +89 -0
  155. package/dist-server/server/native-runtime-spec.js +169 -0
  156. package/dist-server/server/native-sessions.d.ts +172 -19
  157. package/dist-server/server/native-sessions.js +839 -244
  158. package/dist-server/server/native-shutdown.d.ts +6 -0
  159. package/dist-server/server/native-shutdown.js +28 -0
  160. package/dist-server/server/native-wren-home.d.ts +18 -0
  161. package/dist-server/server/native-wren-home.js +262 -0
  162. package/dist-server/server/node-pty-host.d.ts +2 -1
  163. package/dist-server/server/node-pty-host.js +3 -2
  164. package/dist-server/server/profile-registry.d.ts +139 -0
  165. package/dist-server/server/profile-registry.js +620 -0
  166. package/dist-server/server/runtime-binding.d.ts +4 -1
  167. package/dist-server/server/runtime-binding.js +50 -2
  168. package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
  169. package/dist-server/server/runtime-host/claude-component-step.js +186 -0
  170. package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
  171. package/dist-server/server/runtime-host/codex-app-server.js +239 -0
  172. package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
  173. package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
  174. package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
  175. package/dist-server/server/runtime-host/codex-component-step.js +308 -0
  176. package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
  177. package/dist-server/server/runtime-host/codex-conversation.js +295 -0
  178. package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
  179. package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
  180. package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
  181. package/dist-server/server/runtime-host/codex-events.js +66 -0
  182. package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
  183. package/dist-server/server/runtime-host/codex-policy.js +151 -0
  184. package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
  185. package/dist-server/server/runtime-host/codex-process.js +96 -0
  186. package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
  187. package/dist-server/server/runtime-host/codex-rpc.js +182 -0
  188. package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
  189. package/dist-server/server/runtime-host/codex-session.js +492 -0
  190. package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
  191. package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
  192. package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
  193. package/dist-server/server/runtime-host/codex-terminal.js +120 -0
  194. package/dist-server/server/runtime-host/index.d.ts +5 -0
  195. package/dist-server/server/runtime-host/index.js +3 -0
  196. package/dist-server/server/runtime-host/local.d.ts +29 -0
  197. package/dist-server/server/runtime-host/local.js +90 -0
  198. package/dist-server/server/runtime-host/policy.d.ts +8 -0
  199. package/dist-server/server/runtime-host/policy.js +78 -0
  200. package/dist-server/server/runtime-host/types.d.ts +52 -0
  201. package/dist-server/server/runtime-host/types.js +62 -0
  202. package/dist-server/server/structured-query-evidence.d.ts +7 -0
  203. package/dist-server/server/structured-query-evidence.js +47 -0
  204. package/dist-server/server/structured-runtime.d.ts +52 -0
  205. package/dist-server/server/structured-runtime.js +130 -0
  206. package/dist-server/server/structured-scope.d.ts +4 -0
  207. package/dist-server/server/structured-scope.js +12 -0
  208. package/dist-server/server/turn.d.ts +6 -1
  209. package/dist-server/server/turn.js +86 -26
  210. package/dist-server/server/wire-types.d.ts +67 -3
  211. package/dist-server/server/wren-profiles.js +1 -1
  212. package/managed-wren/README.md +60 -0
  213. package/managed-wren/dependency-wheels.json +282 -0
  214. package/managed-wren/manifest.json +373 -0
  215. package/managed-wren/release-inputs.json +45 -0
  216. package/managed-wren/runtime-tree.cjs +115 -0
  217. package/managed-wren/source-inputs.json +40 -0
  218. package/package.json +15 -6
  219. package/profiles/genbi-default/context/binding.yml +17 -7
  220. package/profiles/genbi-default/context/context.json +146 -0
  221. package/profiles/genbi-default/ir.golden.json +39 -43
  222. package/profiles/genbi-default/profile.yml +31 -0
  223. package/profiles/genbi-enrich-context/context/binding.yml +14 -3
  224. package/profiles/genbi-enrich-context/context/context.json +146 -0
  225. package/profiles/genbi-enrich-context/ir.golden.json +4 -5
  226. package/profiles/genbi-monitor/context/binding.yml +13 -5
  227. package/profiles/genbi-monitor/context/context.json +146 -0
  228. package/profiles/genbi-monitor/ir.golden.json +13 -7
  229. package/profiles/genbi-report/context/binding.yml +19 -0
  230. package/profiles/genbi-report/context/context.json +223 -0
  231. package/profiles/genbi-report/ir.golden.json +328 -0
  232. package/profiles/genbi-report/profile.yml +38 -0
  233. package/profiles/genbi-setup/ir.golden.json +3 -1
  234. package/dist/assets/index-Xs4q7pNo.css +0 -1
@@ -153,10 +153,21 @@ CREATE TABLE IF NOT EXISTS native_structured_answers (
153
153
  envelope_json TEXT NOT NULL,
154
154
  digest TEXT NOT NULL,
155
155
  created_at TEXT NOT NULL,
156
+ grounded INTEGER NOT NULL DEFAULT 0,
156
157
  UNIQUE(native_session_id, idempotency_key)
157
158
  );
158
159
  CREATE INDEX IF NOT EXISTS native_structured_answers_session_created_at
159
160
  ON native_structured_answers(native_session_id, created_at DESC);
161
+ -- Warble profile registry: the shipped profiles plus any the operator added. A row's admission
162
+ -- verdict is recomputed when the profile bytes or the warble binary change; rows are never
163
+ -- deleted by admission, only marked unavailable.
164
+ CREATE TABLE IF NOT EXISTS warble_profiles (
165
+ id TEXT PRIMARY KEY, kind TEXT NOT NULL, role TEXT NOT NULL, source_dir TEXT NOT NULL,
166
+ profile_hash TEXT NOT NULL, warble_identity TEXT NOT NULL,
167
+ admission_status TEXT NOT NULL, admission_reason TEXT, entry_kind TEXT, entry_verb TEXT,
168
+ ir_version TEXT, components_json TEXT NOT NULL DEFAULT '[]', rules_version INTEGER NOT NULL DEFAULT 0,
169
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL, admitted_at TEXT
170
+ );
160
171
  `;
161
172
  function boolToInt(value) {
162
173
  return value ? 1 : 0;
@@ -245,6 +256,7 @@ function coerceOrphanedTodoSteps(steps) {
245
256
  }
246
257
  export class Store {
247
258
  db;
259
+ closed = false;
248
260
  onContextSuccessWrite;
249
261
  onEnrichmentTransitionWrite;
250
262
  onEnrichmentCreationWrite;
@@ -362,6 +374,9 @@ export class Store {
362
374
  this.addColumnIfMissing("artifacts", "native_agent", "TEXT");
363
375
  this.addColumnIfMissing("artifacts", "content_digest", "TEXT");
364
376
  this.addColumnIfMissing("artifacts", "idempotency_key", "TEXT");
377
+ // Pre-existing retained answers carry no provenance, so they default to
378
+ // ungrounded: a later save by reference to one of them is Unverified.
379
+ this.addColumnIfMissing("native_structured_answers", "grounded", "INTEGER NOT NULL DEFAULT 0");
365
380
  this.db.exec("CREATE UNIQUE INDEX IF NOT EXISTS artifacts_native_idempotency_unique ON artifacts(native_session_id, idempotency_key) WHERE native_session_id IS NOT NULL AND idempotency_key IS NOT NULL");
366
381
  this.addColumnIfMissing("enrichment_operations", "draft", "TEXT NOT NULL DEFAULT ''");
367
382
  this.addColumnIfMissing("enrichment_operations", "change_kind", "TEXT NOT NULL DEFAULT 'knowledge_append'");
@@ -383,10 +398,14 @@ export class Store {
383
398
  this.addColumnIfMissing("enrichment_approvals", "evidence_ref", "TEXT NOT NULL DEFAULT ''");
384
399
  this.addColumnIfMissing("enrichment_approvals", "nonce", "TEXT NOT NULL DEFAULT ''");
385
400
  this.addColumnIfMissing("enrichment_approvals", "expires_at", "TEXT NOT NULL DEFAULT ''");
401
+ this.addColumnIfMissing("native_sessions", "entry_verb", "TEXT");
402
+ this.addColumnIfMissing("native_sessions", "transport", "TEXT");
386
403
  this.addColumnIfMissing("native_sessions", "dispatch_profile", "TEXT");
387
404
  this.addColumnIfMissing("native_sessions", "dispatch_target", "TEXT");
388
405
  this.addColumnIfMissing("native_sessions", "runtime_generation", "INTEGER");
389
406
  this.addColumnIfMissing("artifacts", "source_answer_id", "TEXT");
407
+ // A database written before admission carried a rule-set version reads 0 and re-admits.
408
+ this.addColumnIfMissing("warble_profiles", "rules_version", "INTEGER NOT NULL DEFAULT 0");
390
409
  this.db.exec("CREATE UNIQUE INDEX IF NOT EXISTS enrichment_approvals_nonce_unique ON enrichment_approvals(nonce) WHERE nonce <> ''");
391
410
  }
392
411
  addColumnIfMissing(table, column, type) {
@@ -398,7 +417,10 @@ export class Store {
398
417
  }
399
418
  close() {
400
419
  this.db.close();
420
+ this.closed = true;
401
421
  }
422
+ /** Lets delayed process-local cleanup avoid touching SQLite after shutdown. */
423
+ isClosed() { return this.closed; }
402
424
  // ---------------------------------------------------------------------
403
425
  // sessions
404
426
  // ---------------------------------------------------------------------
@@ -415,8 +437,8 @@ export class Store {
415
437
  // ---------------------------------------------------------------------
416
438
  createNativeSession(params) {
417
439
  const now = this.now().toISOString();
418
- this.db.prepare(`INSERT INTO native_sessions (id, purpose, vendor, agent, scope_kind, scope_id, project_identity, binding_generation, project_revision, dispatch_profile, dispatch_target, runtime_generation, status, created_at, updated_at, started_at, ended_at, exit_code, failure)
419
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'creating', ?, ?, NULL, NULL, NULL, NULL)`).run(params.id, params.purpose, params.vendor, params.agent, params.scopeKind, params.scopeId, params.projectIdentity ?? null, params.bindingGeneration ?? null, params.projectRevision ?? null, params.dispatchProfile ?? null, params.dispatchTarget ?? null, params.runtimeGeneration ?? null, now, now);
440
+ this.db.prepare(`INSERT INTO native_sessions (id, purpose, vendor, agent, entry_verb, scope_kind, scope_id, project_identity, binding_generation, project_revision, dispatch_profile, dispatch_target, runtime_generation, status, created_at, updated_at, started_at, ended_at, exit_code, failure, transport)
441
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'creating', ?, ?, NULL, NULL, NULL, NULL, ?)`).run(params.id, params.purpose, params.vendor, params.agent, params.entryVerb ?? null, params.scopeKind, params.scopeId, params.projectIdentity ?? null, params.bindingGeneration ?? null, params.projectRevision ?? null, params.dispatchProfile ?? null, params.dispatchTarget ?? null, params.runtimeGeneration ?? null, now, now, params.transport ?? null);
420
442
  return this.getNativeSession(params.id);
421
443
  }
422
444
  /** Stores a sealed provider handle only; callers must never pass plaintext. */
@@ -478,8 +500,8 @@ export class Store {
478
500
  }
479
501
  const now = this.now().toISOString();
480
502
  const child = params.child;
481
- this.db.prepare(`INSERT INTO native_sessions (id, purpose, vendor, agent, scope_kind, scope_id, project_identity, binding_generation, project_revision, dispatch_profile, dispatch_target, runtime_generation, status, created_at, updated_at, started_at, ended_at, exit_code, failure)
482
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'creating', ?, ?, NULL, NULL, NULL, NULL)`).run(child.id, child.purpose, child.vendor, child.agent, child.scopeKind, child.scopeId, child.projectIdentity ?? null, child.bindingGeneration ?? null, child.projectRevision ?? null, child.dispatchProfile ?? null, child.dispatchTarget ?? null, child.runtimeGeneration ?? null, now, now);
503
+ this.db.prepare(`INSERT INTO native_sessions (id, purpose, vendor, agent, entry_verb, scope_kind, scope_id, project_identity, binding_generation, project_revision, dispatch_profile, dispatch_target, runtime_generation, status, created_at, updated_at, started_at, ended_at, exit_code, failure)
504
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'creating', ?, ?, NULL, NULL, NULL, NULL)`).run(child.id, child.purpose, child.vendor, child.agent, child.entryVerb ?? null, child.scopeKind, child.scopeId, child.projectIdentity ?? null, child.bindingGeneration ?? null, child.projectRevision ?? null, child.dispatchProfile ?? null, child.dispatchTarget ?? null, child.runtimeGeneration ?? null, now, now);
483
505
  this.db.prepare(`INSERT INTO native_session_resume_actions (session_id, idempotency_key, scope_fingerprint, resumed_session_id, created_at) VALUES (?, ?, ?, ?, ?)`).run(params.sourceSessionId, params.idempotencyKey, params.scopeFingerprint, child.id, now);
484
506
  this.db.prepare(`UPDATE native_session_resume_handles SET consumed_at = ? WHERE session_id = ? AND consumed_at IS NULL`).run(now, params.sourceSessionId);
485
507
  this.saveNativeSessionResumeHandle(child.id, child.vendor, params.sealedHandle);
@@ -503,8 +525,8 @@ export class Store {
503
525
  const createdAt = this.now().toISOString();
504
526
  this.db.exec("BEGIN IMMEDIATE");
505
527
  try {
506
- this.db.prepare(`INSERT INTO native_structured_answers (id, native_session_id, idempotency_key, envelope_json, digest, created_at)
507
- VALUES (?, ?, ?, ?, ?, ?)`).run(params.id, params.nativeSessionId, params.idempotencyKey, params.envelopeJson, params.digest, createdAt);
528
+ this.db.prepare(`INSERT INTO native_structured_answers (id, native_session_id, idempotency_key, envelope_json, digest, created_at, grounded)
529
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(params.id, params.nativeSessionId, params.idempotencyKey, params.envelopeJson, params.digest, createdAt, boolToInt(params.grounded));
508
530
  this.db.prepare(`DELETE FROM native_structured_answers
509
531
  WHERE id IN (
510
532
  SELECT id FROM native_structured_answers
@@ -539,6 +561,41 @@ export class Store {
539
561
  LIMIT 1`).get(nativeSessionId);
540
562
  return row ? rowToNativeStructuredAnswer(row) : undefined;
541
563
  }
564
+ // ---------------------------------------------------------------------
565
+ // warble profile registry
566
+ // ---------------------------------------------------------------------
567
+ upsertWarbleProfile(row) {
568
+ this.db.prepare(`INSERT INTO warble_profiles (id, kind, role, source_dir, profile_hash, warble_identity, admission_status, admission_reason,
569
+ entry_kind, entry_verb, ir_version, components_json, rules_version, created_at, updated_at, admitted_at)
570
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
571
+ ON CONFLICT(id) DO UPDATE SET
572
+ kind = excluded.kind, role = excluded.role, source_dir = excluded.source_dir,
573
+ profile_hash = excluded.profile_hash, warble_identity = excluded.warble_identity,
574
+ admission_status = excluded.admission_status, admission_reason = excluded.admission_reason,
575
+ entry_kind = excluded.entry_kind, entry_verb = excluded.entry_verb, ir_version = excluded.ir_version,
576
+ components_json = excluded.components_json, rules_version = excluded.rules_version,
577
+ updated_at = excluded.updated_at, admitted_at = excluded.admitted_at`).run(row.id, row.kind, row.role, row.sourceDir, row.profileHash, row.warbleIdentity, row.admissionStatus, row.admissionReason, row.entryKind, row.entryVerb, row.irVersion, row.componentsJson, row.rulesVersion, row.createdAt, row.updatedAt, row.admittedAt);
578
+ return this.getWarbleProfile(row.id);
579
+ }
580
+ getWarbleProfile(id) {
581
+ const row = this.db.prepare(`SELECT * FROM warble_profiles WHERE id = ?`).get(id);
582
+ return row ? rowToWarbleProfile(row) : undefined;
583
+ }
584
+ /** Built-ins first (in registration order), then user profiles by id. */
585
+ listWarbleProfiles() {
586
+ return this.db
587
+ .prepare(`SELECT * FROM warble_profiles ORDER BY CASE kind WHEN 'builtin' THEN 0 ELSE 1 END, rowid ASC`)
588
+ .all()
589
+ .map(rowToWarbleProfile);
590
+ }
591
+ deleteWarbleProfile(id) {
592
+ return this.db.prepare(`DELETE FROM warble_profiles WHERE id = ?`).run(id).changes > 0;
593
+ }
594
+ /** How many native sessions ever ran inside this profile; the registry refuses to remove a referenced one. */
595
+ countNativeSessionsByDispatchProfile(profileId) {
596
+ const row = this.db.prepare(`SELECT COUNT(*) AS n FROM native_sessions WHERE dispatch_profile = ?`).get(profileId);
597
+ return row ? num(row, "n") : 0;
598
+ }
542
599
  listNativeSessions() {
543
600
  return this.db.prepare(`SELECT * FROM native_sessions ORDER BY updated_at DESC, rowid DESC`).all().map(rowToNativeSession);
544
601
  }
@@ -853,7 +910,7 @@ export class Store {
853
910
  const createdAt = new Date().toISOString();
854
911
  const row = {
855
912
  id: params.id, sessionId: params.sessionId, name: params.name, kind: "dashboard", location: params.location,
856
- verified: true, createdAt, savedAt: createdAt, nativeSessionId: params.nativeSessionId,
913
+ verified: params.verified, createdAt, savedAt: createdAt, nativeSessionId: params.nativeSessionId,
857
914
  projectIdentity: params.projectIdentity, bindingGeneration: params.bindingGeneration,
858
915
  projectRevision: params.projectRevision, nativeVendor: params.vendor, nativeAgent: params.agent,
859
916
  contentDigest: params.digest, idempotencyKey: params.idempotencyKey,
@@ -865,7 +922,7 @@ export class Store {
865
922
  native_session_id, project_identity, binding_generation, project_revision,
866
923
  native_vendor, native_agent, content_digest, idempotency_key, source_answer_id
867
924
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
868
- .run(row.id, row.sessionId, row.name, row.kind, row.location, 1, row.createdAt, row.savedAt, row.nativeSessionId, row.projectIdentity, row.bindingGeneration, row.projectRevision, row.nativeVendor, row.nativeAgent, row.contentDigest, row.idempotencyKey, row.sourceAnswerId);
925
+ .run(row.id, row.sessionId, row.name, row.kind, row.location, boolToInt(row.verified), row.createdAt, row.savedAt, row.nativeSessionId, row.projectIdentity, row.bindingGeneration, row.projectRevision, row.nativeVendor, row.nativeAgent, row.contentDigest, row.idempotencyKey, row.sourceAnswerId);
869
926
  return { row, created: true };
870
927
  }
871
928
  catch (error) {
@@ -1724,8 +1781,9 @@ function rowToSession(row) {
1724
1781
  }
1725
1782
  function rowToNativeSession(row) {
1726
1783
  return {
1784
+ ...(row["transport"] === "conversation" ? { transport: "conversation" } : {}),
1727
1785
  id: str(row, "id"), purpose: str(row, "purpose"),
1728
- vendor: str(row, "vendor"), agent: str(row, "agent"),
1786
+ vendor: str(row, "vendor"), agent: str(row, "agent"), entryVerb: strOrNull(row, "entry_verb"),
1729
1787
  scopeKind: str(row, "scope_kind"), scopeId: str(row, "scope_id"),
1730
1788
  projectIdentity: strOrNull(row, "project_identity"),
1731
1789
  bindingGeneration: row["binding_generation"] === null ? null : num(row, "binding_generation"),
@@ -1739,6 +1797,26 @@ function rowToNativeSession(row) {
1739
1797
  exitCode: row["exit_code"] === null ? null : num(row, "exit_code"), failure: strOrNull(row, "failure"),
1740
1798
  };
1741
1799
  }
1800
+ function rowToWarbleProfile(row) {
1801
+ return {
1802
+ id: str(row, "id"),
1803
+ kind: str(row, "kind"),
1804
+ role: str(row, "role"),
1805
+ sourceDir: str(row, "source_dir"),
1806
+ profileHash: str(row, "profile_hash"),
1807
+ warbleIdentity: str(row, "warble_identity"),
1808
+ admissionStatus: str(row, "admission_status"),
1809
+ admissionReason: strOrNull(row, "admission_reason"),
1810
+ entryKind: strOrNull(row, "entry_kind"),
1811
+ entryVerb: strOrNull(row, "entry_verb"),
1812
+ irVersion: strOrNull(row, "ir_version"),
1813
+ componentsJson: str(row, "components_json"),
1814
+ rulesVersion: row["rules_version"] === null || row["rules_version"] === undefined ? 0 : num(row, "rules_version"),
1815
+ createdAt: str(row, "created_at"),
1816
+ updatedAt: str(row, "updated_at"),
1817
+ admittedAt: strOrNull(row, "admitted_at"),
1818
+ };
1819
+ }
1742
1820
  function rowToNativeSetupRecovery(row) {
1743
1821
  return {
1744
1822
  sessionId: str(row, "session_id"),
@@ -1761,6 +1839,7 @@ function rowToNativeStructuredAnswer(row) {
1761
1839
  envelopeJson: str(row, "envelope_json"),
1762
1840
  digest: str(row, "digest"),
1763
1841
  createdAt: str(row, "created_at"),
1842
+ grounded: intToBool(row["grounded"]),
1764
1843
  };
1765
1844
  }
1766
1845
  function rowToEvent(row) {
@@ -1,4 +1,10 @@
1
1
  import type { AdapterEnvStatus } from "./wire-types.js";
2
+ /**
3
+ * The one credential env var for `"gateway"` mode, whatever pi-ai provider it
4
+ * targets. The BFF reads it and passes it to the adapter per request; pi-ai's
5
+ * own provider env vars are never consulted.
6
+ */
7
+ export declare const GATEWAY_API_KEY_ENV = "GENBI_GATEWAY_API_KEY";
2
8
  /**
3
9
  * Reports which api-key adapters have their required credential env var present on the BFF
4
10
  * process — booleans only. The key values themselves (or any prefix/suffix/length derived
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The one credential env var for `"gateway"` mode, whatever pi-ai provider it
3
+ * targets. The BFF reads it and passes it to the adapter per request; pi-ai's
4
+ * own provider env vars are never consulted.
5
+ */
6
+ export const GATEWAY_API_KEY_ENV = "GENBI_GATEWAY_API_KEY";
1
7
  /** True iff the named env var is set to a non-empty (non-whitespace-only) value. */
2
8
  function isNonEmptyEnv(name) {
3
9
  const value = process.env[name];
@@ -13,5 +19,6 @@ export function detectAdapterEnv() {
13
19
  return {
14
20
  anthropic: isNonEmptyEnv("ANTHROPIC_API_KEY"),
15
21
  openaiCompatible: isNonEmptyEnv("OPENAI_API_KEY"),
22
+ gateway: isNonEmptyEnv(GATEWAY_API_KEY_ENV),
16
23
  };
17
24
  }
@@ -14,7 +14,7 @@
14
14
  * forwards whatever the runtime actually reports.
15
15
  */
16
16
  import type { AgentEvent, RouteResult, StepTrace } from "../harness/index.js";
17
- import type { AnswerEvent, ArtifactEvent, ArtifactKind, RefusalEvent, ToolStep } from "./wire-types.js";
17
+ import type { AnswerEvent, ArtifactEvent, ArtifactKind, RefusalEvent, RenderEnvelope, ToolStep } from "./wire-types.js";
18
18
  /**
19
19
  * Canned remediation text for a refusal. `RefusalAgentEvent`/`RefusalResult`
20
20
  * carry no remediation hint from the agent runtime — the UI's `fix` field is
@@ -37,9 +37,11 @@ export declare function extractTrace(result: RouteResult): StepTrace | undefined
37
37
  * on the floor and the raw text always went out as `form: "text"`,
38
38
  * `verified: false`). `extractEnvelopeFromText` recovers it — including
39
39
  * `answer_query`'s flat `{columns, rows}` tool-contract shape, normalized
40
- * into a `table` block — and `verified` is whatever the recovered envelope
41
- * actually carries, never hardcoded. Only when no envelope can be recovered
42
- * at all does the raw text fall back to `form: "text"`.
40
+ * into a `table` block. The recovered envelope's `verified` is model-written,
41
+ * so {@link withHostVerification} decides it from host evidence instead (see
42
+ * there). Only when
43
+ * no envelope can be recovered at all does the raw text fall back to
44
+ * `form: "text"`.
43
45
  *
44
46
  * That `form: "text"` fallback is the one case where "verified: false" is
45
47
  * ambiguous between two very different situations: a pure conversational
@@ -51,6 +53,23 @@ export declare function extractTrace(result: RouteResult): StepTrace | undefined
51
53
  * introspection and artifact-saving tool calls don't count as a data claim.
52
54
  */
53
55
  export declare function toAnswerOrRefusalEvent(id: string, result: RouteResult, worklog?: readonly ToolStep[]): AnswerEvent | RefusalEvent;
56
+ /**
57
+ * The one place a subscription answer's `verified` is decided; a `verified`
58
+ * field in model-written text is never kept.
59
+ *
60
+ * - `codex-local` and `agent-sdk`: the model's host query tool (codex:local's
61
+ * `run_sql`, the agent-sdk host MCP's `run_sql`) executes through this turn's
62
+ * host query service, so the answer is grounded against that turn's
63
+ * observations ({@link groundOnHostEvidence}): verified only when every
64
+ * query it cites is one the host ran this turn and that read a table, and
65
+ * then its tables and definitions are the observed ones. Otherwise it stays
66
+ * as written, badged "Unverified" — including any answer whose data came
67
+ * another way (agent-sdk's Bash `wren` stays available and yields no host
68
+ * evidence).
69
+ *
70
+ * Other backends pass through.
71
+ */
72
+ export declare function withHostVerification(result: RouteResult, envelope: RenderEnvelope): RenderEnvelope;
54
73
  /** Maps a `RunFinishEvent`'s companion `ArtifactAgentEvent` straight through — no BFF-side transformation needed. */
55
74
  export declare function toArtifactEvent(id: string, name: string, artifactKind: ArtifactKind, location: string, artifactId: string): ArtifactEvent;
56
75
  export declare const DECISION_ROUTE_ID = "decision-route";
@@ -1,4 +1,6 @@
1
1
  import { BUILD_DASHBOARD_TOOL_NAME, extractEnvelopeFromText, WREN_QUERY_TOOL_NAME } from "../harness/index.js";
2
+ import { extractJsonCandidateFromText } from "../harness/render/envelope.js";
3
+ import { groundOnHostEvidence } from "./host-evidence-grounding.js";
2
4
  /**
3
5
  * Canned remediation text for a refusal. `RefusalAgentEvent`/`RefusalResult`
4
6
  * carry no remediation hint from the agent runtime — the UI's `fix` field is
@@ -35,9 +37,11 @@ export function extractTrace(result) {
35
37
  * on the floor and the raw text always went out as `form: "text"`,
36
38
  * `verified: false`). `extractEnvelopeFromText` recovers it — including
37
39
  * `answer_query`'s flat `{columns, rows}` tool-contract shape, normalized
38
- * into a `table` block — and `verified` is whatever the recovered envelope
39
- * actually carries, never hardcoded. Only when no envelope can be recovered
40
- * at all does the raw text fall back to `form: "text"`.
40
+ * into a `table` block. The recovered envelope's `verified` is model-written,
41
+ * so {@link withHostVerification} decides it from host evidence instead (see
42
+ * there). Only when
43
+ * no envelope can be recovered at all does the raw text fall back to
44
+ * `form: "text"`.
41
45
  *
42
46
  * That `form: "text"` fallback is the one case where "verified: false" is
43
47
  * ambiguous between two very different situations: a pure conversational
@@ -49,10 +53,15 @@ export function extractTrace(result) {
49
53
  * introspection and artifact-saving tool calls don't count as a data claim.
50
54
  */
51
55
  export function toAnswerOrRefusalEvent(id, result, worklog = []) {
56
+ if (result.backend === "codex-app-server") {
57
+ return { id, kind: "answer", answer: result.envelope
58
+ ? { form: "rich", envelope: result.envelope }
59
+ : { form: "text", text: result.finalText, verified: false, dataAnswer: result.dataAttempted === true || attemptedDataAccess(worklog) } };
60
+ }
52
61
  if (result.backend === "agent-sdk" || result.backend === "codex-local") {
53
62
  const envelope = extractEnvelopeFromText(result.finalText);
54
63
  if (envelope !== undefined) {
55
- return { id, kind: "answer", answer: { form: "rich", envelope } };
64
+ return { id, kind: "answer", answer: { form: "rich", envelope: withHostVerification(result, envelope) } };
56
65
  }
57
66
  return {
58
67
  id,
@@ -65,11 +74,35 @@ export function toAnswerOrRefusalEvent(id, result, worklog = []) {
65
74
  }
66
75
  return { id, kind: "refusal", reason: result.reason, fix: REFUSAL_FIX_HINT };
67
76
  }
77
+ /**
78
+ * The one place a subscription answer's `verified` is decided; a `verified`
79
+ * field in model-written text is never kept.
80
+ *
81
+ * - `codex-local` and `agent-sdk`: the model's host query tool (codex:local's
82
+ * `run_sql`, the agent-sdk host MCP's `run_sql`) executes through this turn's
83
+ * host query service, so the answer is grounded against that turn's
84
+ * observations ({@link groundOnHostEvidence}): verified only when every
85
+ * query it cites is one the host ran this turn and that read a table, and
86
+ * then its tables and definitions are the observed ones. Otherwise it stays
87
+ * as written, badged "Unverified" — including any answer whose data came
88
+ * another way (agent-sdk's Bash `wren` stays available and yields no host
89
+ * evidence).
90
+ *
91
+ * Other backends pass through.
92
+ */
93
+ export function withHostVerification(result, envelope) {
94
+ if (result.backend === "codex-local" || result.backend === "agent-sdk") {
95
+ return groundOnHostEvidence(envelope, extractJsonCandidateFromText(result.finalText), result.hostObservations ?? []);
96
+ }
97
+ return envelope;
98
+ }
68
99
  /** In-process's native in-process tool names that constitute an actual data claim — schema browsing / artifact saving don't count. */
69
100
  const DATA_ACCESS_TOOL_NAMES = new Set([
70
101
  WREN_QUERY_TOOL_NAME,
71
102
  BUILD_DASHBOARD_TOOL_NAME,
72
103
  "wren.run_sql",
104
+ // The agent-sdk Ask's host MCP query tool (see `harness/route/dispatched.ts`).
105
+ "mcp__wren_host__run_sql",
73
106
  ]);
74
107
  /**
75
108
  * Dispatched never produces those native tool names — the dispatched agent's `answer_query`/
@@ -139,6 +172,10 @@ export function clarifyDecisionStep(detail) {
139
172
  * -> `undefined`, so no gate entry is appended.
140
173
  */
141
174
  export function gateDecisionStep(result) {
175
+ if (result.backend === "codex-app-server")
176
+ return result.envelope?.verified === true
177
+ ? { id: DECISION_GATE_ID, kind: "decision", label: "Verify gate", state: "done", detail: "Query rows and SQL captured from successful host execution; interpretation is not verified." }
178
+ : undefined;
142
179
  if (result.backend === "agent-sdk" || result.backend === "codex-local")
143
180
  return undefined;
144
181
  if (result.kind === "refusal") {
@@ -317,7 +354,7 @@ function truncate(text, max) {
317
354
  }
318
355
  /** Short text used for `turns.answer_summary` — feeds `server/compose.ts`'s D3 context composition. Never the full envelope. */
319
356
  export function summarizeResult(result) {
320
- if (result.backend === "agent-sdk" || result.backend === "codex-local") {
357
+ if (result.backend === "agent-sdk" || (result.backend === "codex-local" || result.backend === "codex-app-server")) {
321
358
  return truncate(result.finalText, SUMMARY_MAX_LENGTH);
322
359
  }
323
360
  if (result.kind === "answer") {
@@ -14,6 +14,6 @@ export declare function collectBundleTierNames(bundle: Bundle): string[];
14
14
  * Keep the compiled bundle's declared profile tied to the BFF-selected
15
15
  * purpose before exposing it through the read-only Harness DTO.
16
16
  */
17
- export declare function assertHarnessBundlePurpose(bundle: Bundle, purpose: NativePurpose): void;
18
- export declare function buildHarnessDto(bundle: Bundle, store: Store, baseRouteOptions: BaseRouteOptions, purpose?: NativePurpose, nativeReadiness?: NativeSessionReadiness, setupReadiness?: HarnessSetupReadiness): HarnessDto;
17
+ export declare function assertHarnessBundlePurpose(bundle: Bundle, purpose: NativePurpose, selectedProfile?: string): void;
18
+ export declare function buildHarnessDto(bundle: Bundle, store: Store, baseRouteOptions: BaseRouteOptions, purpose?: NativePurpose, nativeReadiness?: NativeSessionReadiness, setupReadiness?: HarnessSetupReadiness, selectedProfile?: string): HarnessDto;
19
19
  export {};
@@ -33,7 +33,7 @@ import { createHash } from "node:crypto";
33
33
  import path from "node:path";
34
34
  import { describeConnection, resolveConnectionSource } from "./conn-config.js";
35
35
  import { bundleFormatVersion, deriveAdapterSpec, findLockedGatedCheck } from "../harness/index.js";
36
- import { assertHarnessPurposeProfile, NATIVE_DISPATCH_REGISTRY } from "./native-dispatch-registry.js";
36
+ import { assertHarnessPurposeProfile, HARNESS_PROFILE_IDENTITY_ERROR, NATIVE_DISPATCH_REGISTRY } from "./native-dispatch-registry.js";
37
37
  import { collectBundleTierNames as collectCompiledBundleTierNames, effectiveTierModel } from "./runtime-binding.js";
38
38
  /** snake_case/kebab-case id -> Title Case display name, e.g. "answer_query" -> "Answer Query". */
39
39
  function humanize(id) {
@@ -146,8 +146,10 @@ function runtimeBackendAndLabel(authChoice) {
146
146
  return { backend: "api-key", label: `API key (${authChoice.adapter})` };
147
147
  case "local":
148
148
  return { backend: "local", label: authChoice.endpoint ? `Local (${authChoice.endpoint})` : "Local" };
149
- case "gateway":
150
- return { backend: "gateway", label: "Gateway" };
149
+ case "gateway": {
150
+ const provider = authChoice.config?.["provider"];
151
+ return { backend: "gateway", label: typeof provider === "string" && provider.trim() ? `Gateway (pi-ai: ${provider})` : "Gateway" };
152
+ }
151
153
  }
152
154
  }
153
155
  /**
@@ -354,7 +356,14 @@ export function collectBundleTierNames(bundle) {
354
356
  * Keep the compiled bundle's declared profile tied to the BFF-selected
355
357
  * purpose before exposing it through the read-only Harness DTO.
356
358
  */
357
- export function assertHarnessBundlePurpose(bundle, purpose) {
359
+ export function assertHarnessBundlePurpose(bundle, purpose, selectedProfile) {
360
+ // A selected conversation profile is described by its registry id; the compiled bundle must
361
+ // carry exactly that id. Without a selection the purpose's fixed profile applies as before.
362
+ if (selectedProfile !== undefined && purpose === "analysis") {
363
+ if (bundle.profile !== selectedProfile)
364
+ throw new Error(HARNESS_PROFILE_IDENTITY_ERROR);
365
+ return;
366
+ }
358
367
  assertHarnessPurposeProfile(purpose, bundle.profile);
359
368
  }
360
369
  function buildRuntime(bundle, baseRouteOptions, resolver) {
@@ -397,8 +406,25 @@ function setupRunnerTarget(authChoice) {
397
406
  case "in-process": return { target: "in-process:setup", targetLabel: "In-process Setup runner" };
398
407
  }
399
408
  }
400
- function buildHarnessPurpose(purpose, baseRouteOptions, nativeReadiness, setupReadiness) {
409
+ function buildHarnessPurpose(purpose, baseRouteOptions, nativeReadiness, setupReadiness, selectedProfile) {
401
410
  const definition = NATIVE_DISPATCH_REGISTRY[purpose];
411
+ // A selected conversation profile is reported with its own id and per-profile readiness.
412
+ if (selectedProfile !== undefined && purpose === "analysis" && selectedProfile !== definition.profile) {
413
+ // Only the per-profile entry speaks for a selected profile. Falling back to the purpose-level
414
+ // row would report the default's availability under another profile's name and could hide a
415
+ // vendor refusal that applies to that profile alone.
416
+ const readiness = nativeReadiness?.profiles?.[selectedProfile];
417
+ const available = readiness?.available ?? false;
418
+ return {
419
+ purpose,
420
+ profile: selectedProfile,
421
+ scopeKind: definition.scopeKind,
422
+ executionKind: "native_session",
423
+ ...(readiness?.target ? { target: readiness.target, targetLabel: readiness.targetLabel } : {}),
424
+ available,
425
+ ...(!available ? { reason: readiness?.reason ?? NATIVE_PURPOSE_UNAVAILABLE_REASON } : {}),
426
+ };
427
+ }
402
428
  if (purpose === "setup") {
403
429
  const available = setupReadiness?.available ?? false;
404
430
  return {
@@ -419,9 +445,9 @@ function buildHarnessPurpose(purpose, baseRouteOptions, nativeReadiness, setupRe
419
445
  ...(!available ? { reason: readiness?.reason ?? NATIVE_PURPOSE_UNAVAILABLE_REASON } : {}),
420
446
  };
421
447
  }
422
- export function buildHarnessDto(bundle, store, baseRouteOptions, purpose = "analysis", nativeReadiness, setupReadiness) {
448
+ export function buildHarnessDto(bundle, store, baseRouteOptions, purpose = "analysis", nativeReadiness, setupReadiness, selectedProfile) {
423
449
  const resolver = buildRealTierResolver(baseRouteOptions, store);
424
- const purposeInfo = buildHarnessPurpose(purpose, baseRouteOptions, nativeReadiness, setupReadiness);
450
+ const purposeInfo = buildHarnessPurpose(purpose, baseRouteOptions, nativeReadiness, setupReadiness, selectedProfile);
425
451
  return {
426
452
  purpose: purposeInfo,
427
453
  profile: buildProfile(bundle, baseRouteOptions, store, purpose),
@@ -0,0 +1,21 @@
1
+ import type { HostQueryObservation } from "../harness/route/host-query.js";
2
+ import type { RenderEnvelope } from "../harness/render/envelope.js";
3
+ /**
4
+ * Grounds an answer against the queries the host executed for its scope (one codex:local or
5
+ * agent-sdk Ask turn, or one native session), with the component path's predicate
6
+ * (`citedGroundedQuery`: a tool-proven definition that reads at least one table, cited by
7
+ * `query_id`, else by whitespace-insensitive SQL).
8
+ *
9
+ * Verified only when the answer cites at least one query and every citation grounds. The
10
+ * envelope is then rebuilt from the observations alone: one `table` and one `definition` block
11
+ * (carrying the observed `query_id`) per cited query, plus the model's `summary` text as written.
12
+ * A cited result the host saw truncated still grounds the rows it shows; the host then appends
13
+ * a note to `summary`, the only place the render contract carries such text. Anything else stays the model's answer with
14
+ * `verified: false`: no citation, a citation to a query this scope never ran (including one from
15
+ * another turn or session), a query that read no table, or chart / KPI blocks the host cannot rebuild.
16
+ *
17
+ * `raw` is the answer as written, before envelope normalization (see `citations`).
18
+ */
19
+ export declare function groundOnHostEvidence(envelope: RenderEnvelope, raw: unknown, observations: readonly HostQueryObservation[]): RenderEnvelope;
20
+ /** Host-written, never model-written: a cited table that is only the first rows of its result. */
21
+ export declare function truncationNote(shown: number): string;
@@ -0,0 +1,93 @@
1
+ import { citedGroundedQuery } from "../harness/components/normalize.js";
2
+ import { namedAnswerQueryId, namedAnswerSql, sqlKey } from "../harness/render/answering-query.js";
3
+ /** Blocks whose values the host cannot rebuild from one observed table; their presence leaves an answer unverified. */
4
+ const UNREBUILDABLE_DATA_BLOCKS = new Set(["chart", "kpi_card"]);
5
+ function record(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
7
+ }
8
+ /**
9
+ * The queries an answer names. A `{blocks}` answer cites through its `definition` blocks; a flat
10
+ * or query-result answer through its own `definition` / top-level fields, read from `raw` (the
11
+ * answer as the model wrote it) because envelope normalization drops a cited `query_id`. A host
12
+ * id wins over SQL, like the component path.
13
+ */
14
+ function citations(envelope, raw) {
15
+ if (!Array.isArray(record(raw)?.blocks)) {
16
+ const queryId = namedAnswerQueryId(raw);
17
+ if (queryId !== undefined)
18
+ return [{ queryId }];
19
+ const sql = namedAnswerSql(raw);
20
+ return sql !== undefined ? [{ sql }] : [];
21
+ }
22
+ const found = [];
23
+ const seen = new Set();
24
+ for (const block of Array.isArray(envelope.blocks) ? envelope.blocks : []) {
25
+ const fields = record(block);
26
+ if (fields?.type !== "definition")
27
+ continue;
28
+ const queryId = typeof fields.query_id === "string" && fields.query_id.length > 0 ? fields.query_id : undefined;
29
+ const sql = typeof fields.sql === "string" && fields.sql.trim() ? fields.sql : undefined;
30
+ const citation = queryId !== undefined ? { queryId } : sql !== undefined ? { sql } : undefined;
31
+ if (!citation)
32
+ continue;
33
+ const key = queryId !== undefined ? `id:${queryId}` : `sql:${sqlKey(sql)}`;
34
+ if (seen.has(key))
35
+ continue;
36
+ seen.add(key);
37
+ found.push(citation);
38
+ }
39
+ return found;
40
+ }
41
+ /**
42
+ * Grounds an answer against the queries the host executed for its scope (one codex:local or
43
+ * agent-sdk Ask turn, or one native session), with the component path's predicate
44
+ * (`citedGroundedQuery`: a tool-proven definition that reads at least one table, cited by
45
+ * `query_id`, else by whitespace-insensitive SQL).
46
+ *
47
+ * Verified only when the answer cites at least one query and every citation grounds. The
48
+ * envelope is then rebuilt from the observations alone: one `table` and one `definition` block
49
+ * (carrying the observed `query_id`) per cited query, plus the model's `summary` text as written.
50
+ * A cited result the host saw truncated still grounds the rows it shows; the host then appends
51
+ * a note to `summary`, the only place the render contract carries such text. Anything else stays the model's answer with
52
+ * `verified: false`: no citation, a citation to a query this scope never ran (including one from
53
+ * another turn or session), a query that read no table, or chart / KPI blocks the host cannot rebuild.
54
+ *
55
+ * `raw` is the answer as written, before envelope normalization (see `citations`).
56
+ */
57
+ export function groundOnHostEvidence(envelope, raw, observations) {
58
+ const unverified = { ...envelope, verified: false };
59
+ if (observations.length === 0)
60
+ return unverified;
61
+ const blocks = Array.isArray(envelope.blocks) ? envelope.blocks : [];
62
+ if (blocks.some((block) => UNREBUILDABLE_DATA_BLOCKS.has(String(record(block)?.type))))
63
+ return unverified;
64
+ const cited = citations(envelope, raw);
65
+ if (cited.length === 0)
66
+ return unverified;
67
+ const rebuilt = [];
68
+ const notes = [];
69
+ for (const citation of cited) {
70
+ const query = citedGroundedQuery(observations, citation);
71
+ if (!query)
72
+ return unverified;
73
+ const observation = observations.find((call) => call.output?.query_id === query.queryId);
74
+ if (observation?.output?.truncated === true)
75
+ notes.push(truncationNote(query.table.rows.length));
76
+ // The definition keeps the observed query's host id, so grounding the rebuilt envelope
77
+ // again (the artifact path does) resolves to the very same observation.
78
+ rebuilt.push({ type: "table", columns: query.table.columns, rows: query.table.rows }, { type: "definition", ...query.definition, ...(query.queryId !== undefined ? { query_id: query.queryId } : {}) });
79
+ }
80
+ const summary = withNotes(typeof envelope.summary === "string" ? envelope.summary : undefined, notes);
81
+ return { blocks: rebuilt, ...(summary !== undefined ? { summary } : {}), verified: true };
82
+ }
83
+ /** Host-written, never model-written: a cited table that is only the first rows of its result. */
84
+ export function truncationNote(shown) {
85
+ return `Host note: this query returned more than ${shown} rows; the table shows only the first ${shown}.`;
86
+ }
87
+ /** Appends each host note once, so grounding an already-grounded envelope again changes nothing. */
88
+ function withNotes(summary, notes) {
89
+ const missing = [...new Set(notes)].filter((note) => !summary?.includes(note));
90
+ if (missing.length === 0)
91
+ return summary;
92
+ return [summary, ...missing].filter((part) => part !== undefined && part.length > 0).join("\n\n");
93
+ }