@wrenai/genbi 0.0.1
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/LICENSE +201 -0
- package/README.md +141 -0
- package/bin/genbi.mjs +31 -0
- package/dist/assets/echarts-LTFC-ysd.js +40 -0
- package/dist/assets/index-Bo--QMUN.js +441 -0
- package/dist/assets/index-Xs4q7pNo.css +1 -0
- package/dist/index.html +14 -0
- package/dist/wrenai_logo.png +0 -0
- package/dist/wrenai_logo_white.png +0 -0
- package/dist-server/harness/auth/detect.d.ts +18 -0
- package/dist-server/harness/auth/detect.js +37 -0
- package/dist-server/harness/auth/index.d.ts +4 -0
- package/dist-server/harness/auth/index.js +2 -0
- package/dist-server/harness/auth/probe.d.ts +18 -0
- package/dist-server/harness/auth/probe.js +52 -0
- package/dist-server/harness/auth/types.d.ts +42 -0
- package/dist-server/harness/auth/types.js +9 -0
- package/dist-server/harness/bundle/loader.d.ts +32 -0
- package/dist-server/harness/bundle/loader.js +43 -0
- package/dist-server/harness/bundle/schema.d.ts +199 -0
- package/dist-server/harness/bundle/schema.js +127 -0
- package/dist-server/harness/bundle/version.d.ts +10 -0
- package/dist-server/harness/bundle/version.js +43 -0
- package/dist-server/harness/capability/gate.d.ts +6 -0
- package/dist-server/harness/capability/gate.js +29 -0
- package/dist-server/harness/capability/registry.d.ts +6 -0
- package/dist-server/harness/capability/registry.js +17 -0
- package/dist-server/harness/cli-args.d.ts +123 -0
- package/dist-server/harness/cli-args.js +247 -0
- package/dist-server/harness/cli.d.ts +2 -0
- package/dist-server/harness/cli.js +151 -0
- package/dist-server/harness/compile/cache.d.ts +23 -0
- package/dist-server/harness/compile/cache.js +109 -0
- package/dist-server/harness/compile/compose-profile.d.ts +34 -0
- package/dist-server/harness/compile/compose-profile.js +95 -0
- package/dist-server/harness/compile/errors.d.ts +22 -0
- package/dist-server/harness/compile/errors.js +41 -0
- package/dist-server/harness/compile/fingerprint.d.ts +22 -0
- package/dist-server/harness/compile/fingerprint.js +84 -0
- package/dist-server/harness/compile/index.d.ts +8 -0
- package/dist-server/harness/compile/index.js +7 -0
- package/dist-server/harness/compile/pipeline.d.ts +66 -0
- package/dist-server/harness/compile/pipeline.js +176 -0
- package/dist-server/harness/compile/resolve-binary.d.ts +55 -0
- package/dist-server/harness/compile/resolve-binary.js +133 -0
- package/dist-server/harness/compile/types.d.ts +107 -0
- package/dist-server/harness/compile/types.js +1 -0
- package/dist-server/harness/compile/warble-identity.d.ts +12 -0
- package/dist-server/harness/compile/warble-identity.js +33 -0
- package/dist-server/harness/compliance/gate.d.ts +35 -0
- package/dist-server/harness/compliance/gate.js +48 -0
- package/dist-server/harness/compliance/index.d.ts +2 -0
- package/dist-server/harness/compliance/index.js +1 -0
- package/dist-server/harness/compliance/types.d.ts +16 -0
- package/dist-server/harness/compliance/types.js +1 -0
- package/dist-server/harness/events/emitter.d.ts +34 -0
- package/dist-server/harness/events/emitter.js +25 -0
- package/dist-server/harness/events/index.d.ts +4 -0
- package/dist-server/harness/events/index.js +2 -0
- package/dist-server/harness/events/sse.d.ts +20 -0
- package/dist-server/harness/events/sse.js +48 -0
- package/dist-server/harness/events/types.d.ts +131 -0
- package/dist-server/harness/events/types.js +1 -0
- package/dist-server/harness/exec/errors.d.ts +16 -0
- package/dist-server/harness/exec/errors.js +29 -0
- package/dist-server/harness/exec/index.d.ts +5 -0
- package/dist-server/harness/exec/index.js +2 -0
- package/dist-server/harness/exec/local.d.ts +32 -0
- package/dist-server/harness/exec/local.js +134 -0
- package/dist-server/harness/exec/policy.d.ts +12 -0
- package/dist-server/harness/exec/policy.js +1 -0
- package/dist-server/harness/exec/types.d.ts +63 -0
- package/dist-server/harness/exec/types.js +1 -0
- package/dist-server/harness/guardrails/index.d.ts +2 -0
- package/dist-server/harness/guardrails/index.js +1 -0
- package/dist-server/harness/guardrails/policy.d.ts +45 -0
- package/dist-server/harness/guardrails/policy.js +57 -0
- package/dist-server/harness/index.d.ts +37 -0
- package/dist-server/harness/index.js +19 -0
- package/dist-server/harness/loop/errors.d.ts +53 -0
- package/dist-server/harness/loop/errors.js +72 -0
- package/dist-server/harness/loop/executor.d.ts +123 -0
- package/dist-server/harness/loop/executor.js +380 -0
- package/dist-server/harness/loop/guard.d.ts +16 -0
- package/dist-server/harness/loop/guard.js +1 -0
- package/dist-server/harness/loop/index.d.ts +5 -0
- package/dist-server/harness/loop/index.js +3 -0
- package/dist-server/harness/native-producer/index.d.ts +109 -0
- package/dist-server/harness/native-producer/index.js +302 -0
- package/dist-server/harness/npm-package-resolve.d.ts +28 -0
- package/dist-server/harness/npm-package-resolve.js +47 -0
- package/dist-server/harness/providers/adapters/anthropic.d.ts +7 -0
- package/dist-server/harness/providers/adapters/anthropic.js +6 -0
- package/dist-server/harness/providers/adapters/mock.d.ts +4 -0
- package/dist-server/harness/providers/adapters/mock.js +5 -0
- package/dist-server/harness/providers/adapters/openai-compatible.d.ts +10 -0
- package/dist-server/harness/providers/adapters/openai-compatible.js +10 -0
- package/dist-server/harness/providers/binding.d.ts +23 -0
- package/dist-server/harness/providers/binding.js +13 -0
- package/dist-server/harness/providers/errors.d.ts +6 -0
- package/dist-server/harness/providers/errors.js +12 -0
- package/dist-server/harness/providers/index.d.ts +18 -0
- package/dist-server/harness/providers/index.js +22 -0
- package/dist-server/harness/providers/registry.d.ts +16 -0
- package/dist-server/harness/providers/registry.js +24 -0
- package/dist-server/harness/render/envelope.d.ts +145 -0
- package/dist-server/harness/render/envelope.js +428 -0
- package/dist-server/harness/render/errors.d.ts +33 -0
- package/dist-server/harness/render/errors.js +46 -0
- package/dist-server/harness/render/index.d.ts +5 -0
- package/dist-server/harness/render/index.js +3 -0
- package/dist-server/harness/render/validate.d.ts +13 -0
- package/dist-server/harness/render/validate.js +88 -0
- package/dist-server/harness/route/adapter-spec.d.ts +40 -0
- package/dist-server/harness/route/adapter-spec.js +61 -0
- package/dist-server/harness/route/agent-sdk-cli.d.ts +30 -0
- package/dist-server/harness/route/agent-sdk-cli.js +86 -0
- package/dist-server/harness/route/agent-sdk-manifest.d.ts +31 -0
- package/dist-server/harness/route/agent-sdk-manifest.js +56 -0
- package/dist-server/harness/route/artifact-content.d.ts +53 -0
- package/dist-server/harness/route/artifact-content.js +83 -0
- package/dist-server/harness/route/chat-event-mapper.d.ts +95 -0
- package/dist-server/harness/route/chat-event-mapper.js +164 -0
- package/dist-server/harness/route/codex-ask-events.d.ts +25 -0
- package/dist-server/harness/route/codex-ask-events.js +395 -0
- package/dist-server/harness/route/codex-ask.d.ts +15 -0
- package/dist-server/harness/route/codex-ask.js +242 -0
- package/dist-server/harness/route/codex-local-cli.d.ts +20 -0
- package/dist-server/harness/route/codex-local-cli.js +78 -0
- package/dist-server/harness/route/codex-local-manifest.d.ts +22 -0
- package/dist-server/harness/route/codex-local-manifest.js +180 -0
- package/dist-server/harness/route/describe.d.ts +54 -0
- package/dist-server/harness/route/describe.js +84 -0
- package/dist-server/harness/route/dispatched.d.ts +138 -0
- package/dist-server/harness/route/dispatched.js +339 -0
- package/dist-server/harness/route/in-process.d.ts +43 -0
- package/dist-server/harness/route/in-process.js +106 -0
- package/dist-server/harness/route/index.d.ts +19 -0
- package/dist-server/harness/route/index.js +12 -0
- package/dist-server/harness/route/profile-source.d.ts +37 -0
- package/dist-server/harness/route/profile-source.js +76 -0
- package/dist-server/harness/route/route.d.ts +33 -0
- package/dist-server/harness/route/route.js +114 -0
- package/dist-server/harness/route/tier-binding.d.ts +36 -0
- package/dist-server/harness/route/tier-binding.js +63 -0
- package/dist-server/harness/route/types.d.ts +286 -0
- package/dist-server/harness/route/types.js +1 -0
- package/dist-server/harness/session/errors.d.ts +4 -0
- package/dist-server/harness/session/errors.js +7 -0
- package/dist-server/harness/session/gate.d.ts +11 -0
- package/dist-server/harness/session/gate.js +13 -0
- package/dist-server/harness/session/index.d.ts +5 -0
- package/dist-server/harness/session/index.js +3 -0
- package/dist-server/harness/session/run.d.ts +60 -0
- package/dist-server/harness/session/run.js +333 -0
- package/dist-server/harness/session/types.d.ts +29 -0
- package/dist-server/harness/session/types.js +1 -0
- package/dist-server/harness/setup/codex-events.d.ts +12 -0
- package/dist-server/harness/setup/codex-events.js +108 -0
- package/dist-server/harness/setup/runner.d.ts +497 -0
- package/dist-server/harness/setup/runner.js +1314 -0
- package/dist-server/harness/tools/errors.d.ts +107 -0
- package/dist-server/harness/tools/errors.js +141 -0
- package/dist-server/harness/tools/index.d.ts +13 -0
- package/dist-server/harness/tools/index.js +8 -0
- package/dist-server/harness/tools/mcp.d.ts +26 -0
- package/dist-server/harness/tools/mcp.js +15 -0
- package/dist-server/harness/tools/native.d.ts +179 -0
- package/dist-server/harness/tools/native.js +322 -0
- package/dist-server/harness/tools/parse.d.ts +14 -0
- package/dist-server/harness/tools/parse.js +18 -0
- package/dist-server/harness/tools/resolve-wren-binary.d.ts +12 -0
- package/dist-server/harness/tools/resolve-wren-binary.js +21 -0
- package/dist-server/harness/tools/resolve.d.ts +51 -0
- package/dist-server/harness/tools/resolve.js +79 -0
- package/dist-server/harness/tools/setup-native.d.ts +84 -0
- package/dist-server/harness/tools/setup-native.js +353 -0
- package/dist-server/harness/tools/wiring.d.ts +10 -0
- package/dist-server/harness/tools/wiring.js +16 -0
- package/dist-server/providers/setup.provider.yaml +31 -0
- package/dist-server/providers/wren.provider.yaml +33 -0
- package/dist-server/server/adopt.d.ts +87 -0
- package/dist-server/server/adopt.js +263 -0
- package/dist-server/server/app.d.ts +6 -0
- package/dist-server/server/app.js +2521 -0
- package/dist-server/server/auth-choice.d.ts +16 -0
- package/dist-server/server/auth-choice.js +36 -0
- package/dist-server/server/bin.d.ts +2 -0
- package/dist-server/server/bin.js +601 -0
- package/dist-server/server/bootstrap-project-binding.d.ts +31 -0
- package/dist-server/server/bootstrap-project-binding.js +71 -0
- package/dist-server/server/clarify.d.ts +10 -0
- package/dist-server/server/clarify.js +17 -0
- package/dist-server/server/codex-setup-mcp.d.ts +2 -0
- package/dist-server/server/codex-setup-mcp.js +147 -0
- package/dist-server/server/compose.d.ts +89 -0
- package/dist-server/server/compose.js +271 -0
- package/dist-server/server/conn-config.d.ts +30 -0
- package/dist-server/server/conn-config.js +253 -0
- package/dist-server/server/context-files.d.ts +12 -0
- package/dist-server/server/context-files.js +117 -0
- package/dist-server/server/context-map.d.ts +30 -0
- package/dist-server/server/context-map.js +95 -0
- package/dist-server/server/context-source.d.ts +68 -0
- package/dist-server/server/context-source.js +85 -0
- package/dist-server/server/db.d.ts +862 -0
- package/dist-server/server/db.js +1839 -0
- package/dist-server/server/enrichment-runner.d.ts +184 -0
- package/dist-server/server/enrichment-runner.js +213 -0
- package/dist-server/server/enrichment-verify.d.ts +73 -0
- package/dist-server/server/enrichment-verify.js +290 -0
- package/dist-server/server/enrichment.d.ts +232 -0
- package/dist-server/server/enrichment.js +199 -0
- package/dist-server/server/env-detect.d.ts +8 -0
- package/dist-server/server/env-detect.js +17 -0
- package/dist-server/server/env-field-hints.d.ts +46 -0
- package/dist-server/server/env-field-hints.js +48 -0
- package/dist-server/server/fold.d.ts +133 -0
- package/dist-server/server/fold.js +407 -0
- package/dist-server/server/harness-profile-sources.d.ts +3 -0
- package/dist-server/server/harness-profile-sources.js +24 -0
- package/dist-server/server/harness.d.ts +19 -0
- package/dist-server/server/harness.js +439 -0
- package/dist-server/server/impact.d.ts +18 -0
- package/dist-server/server/impact.js +85 -0
- package/dist-server/server/interactive-terminal.d.ts +175 -0
- package/dist-server/server/interactive-terminal.js +336 -0
- package/dist-server/server/native-artifacts.d.ts +1004 -0
- package/dist-server/server/native-artifacts.js +564 -0
- package/dist-server/server/native-dispatch-registry.d.ts +42 -0
- package/dist-server/server/native-dispatch-registry.js +45 -0
- package/dist-server/server/native-producer-fence.d.ts +14 -0
- package/dist-server/server/native-producer-fence.js +48 -0
- package/dist-server/server/native-session-resume.d.ts +6 -0
- package/dist-server/server/native-session-resume.js +103 -0
- package/dist-server/server/native-session-workspace.d.ts +40 -0
- package/dist-server/server/native-session-workspace.js +175 -0
- package/dist-server/server/native-sessions.d.ts +374 -0
- package/dist-server/server/native-sessions.js +1517 -0
- package/dist-server/server/native-wren-runtime.d.ts +20 -0
- package/dist-server/server/native-wren-runtime.js +259 -0
- package/dist-server/server/node-pty-host.d.ts +31 -0
- package/dist-server/server/node-pty-host.js +100 -0
- package/dist-server/server/route-intent.d.ts +40 -0
- package/dist-server/server/route-intent.js +23 -0
- package/dist-server/server/runtime-binding.d.ts +52 -0
- package/dist-server/server/runtime-binding.js +258 -0
- package/dist-server/server/source-catalog.d.ts +84 -0
- package/dist-server/server/source-catalog.js +221 -0
- package/dist-server/server/source-driver.d.ts +35 -0
- package/dist-server/server/source-driver.js +124 -0
- package/dist-server/server/spa.d.ts +2 -0
- package/dist-server/server/spa.js +51 -0
- package/dist-server/server/subscription-model-catalog.d.ts +27 -0
- package/dist-server/server/subscription-model-catalog.js +203 -0
- package/dist-server/server/turn.d.ts +249 -0
- package/dist-server/server/turn.js +1074 -0
- package/dist-server/server/wire-types.d.ts +799 -0
- package/dist-server/server/wire-types.js +1 -0
- package/dist-server/server/wren-home.d.ts +19 -0
- package/dist-server/server/wren-home.js +33 -0
- package/dist-server/server/wren-profiles.d.ts +43 -0
- package/dist-server/server/wren-profiles.js +123 -0
- package/package.json +93 -0
- package/profiles/genbi-default/context/binding.yml +9 -0
- package/profiles/genbi-default/ir.golden.json +506 -0
- package/profiles/genbi-default/profile.yml +16 -0
- package/profiles/genbi-enrich-context/components/apply_enrichment/component.yml +42 -0
- package/profiles/genbi-enrich-context/components/draft_enrichment/component.yml +30 -0
- package/profiles/genbi-enrich-context/components/draft_enrichment/steps/draft.md +34 -0
- package/profiles/genbi-enrich-context/components/inspect_context/component.yml +32 -0
- package/profiles/genbi-enrich-context/components/inspect_context/steps/inspect.md +13 -0
- package/profiles/genbi-enrich-context/context/binding.yml +4 -0
- package/profiles/genbi-enrich-context/ir.golden.json +302 -0
- package/profiles/genbi-enrich-context/profile.yml +15 -0
- package/profiles/genbi-monitor/context/binding.yml +7 -0
- package/profiles/genbi-monitor/ir.golden.json +188 -0
- package/profiles/genbi-monitor/profile.yml +27 -0
- package/profiles/genbi-setup/components/build_context/component.yml +44 -0
- package/profiles/genbi-setup/components/build_context/steps/build.md +14 -0
- package/profiles/genbi-setup/components/connect_source/component.yml +51 -0
- package/profiles/genbi-setup/components/connect_source/steps/connect.md +23 -0
- package/profiles/genbi-setup/context/binding.yml +8 -0
- package/profiles/genbi-setup/ir.golden.json +140 -0
- package/profiles/genbi-setup/profile.yml +18 -0
- package/profiles/genbi-setup/raw/schema.json +5 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Agent } from "../bundle/schema.js";
|
|
2
|
+
import type { DataflowArtifacts } from "../loop/index.js";
|
|
3
|
+
import type { ProviderRegistry, TierBinding } from "../providers/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* The structured answer synthesized from an agent's finished dataflow
|
|
6
|
+
* artifacts, shaped by `agent.output_schema`. The golden `answer_query`
|
|
7
|
+
* schema requires `blocks` and allows `summary`/`verified` to be present —
|
|
8
|
+
* this type only pins down the fields the harness itself inspects
|
|
9
|
+
* (`verified`, for `gated_check` enforcement); everything else the schema
|
|
10
|
+
* defines flows through as-is.
|
|
11
|
+
*/
|
|
12
|
+
export interface RenderEnvelope {
|
|
13
|
+
readonly blocks: unknown;
|
|
14
|
+
readonly summary?: string | null;
|
|
15
|
+
readonly verified?: boolean | null;
|
|
16
|
+
readonly [key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A flat table payload captured from a data-access tool's real, successful
|
|
20
|
+
* result (the `query` tool's `{columns, rows}` reshaped to positional rows,
|
|
21
|
+
* plus the SQL that produced it). When present, `renderEnvelope` builds the
|
|
22
|
+
* envelope's table/definition blocks directly from this — deterministic, and
|
|
23
|
+
* carrying the agent's actual rows — instead of routing through the render
|
|
24
|
+
* model, which is lossy for structured data.
|
|
25
|
+
*/
|
|
26
|
+
export interface ToolTableSeed {
|
|
27
|
+
readonly columns: readonly string[];
|
|
28
|
+
readonly rows: readonly unknown[][];
|
|
29
|
+
readonly sql?: string;
|
|
30
|
+
readonly summary?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A full `{blocks}` payload captured from a `build_dashboard` tool's real,
|
|
34
|
+
* successful result (see `harness/tools/native.ts`'s `assembleDashboardBlocks`).
|
|
35
|
+
* Unlike {@link ToolTableSeed} (a flat table reshaped into table/definition
|
|
36
|
+
* blocks), this is already a complete, ordered dashboard envelope — the
|
|
37
|
+
* deterministic assembly tool did all the reshaping. When present and it
|
|
38
|
+
* validates against `output_schema`, `renderEnvelope` returns it directly,
|
|
39
|
+
* ahead of every other path (see `renderEnvelope`'s seed-precedence order).
|
|
40
|
+
*/
|
|
41
|
+
export interface DashboardSeed {
|
|
42
|
+
readonly blocks: readonly unknown[];
|
|
43
|
+
readonly summary?: string;
|
|
44
|
+
readonly verified?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface RenderEnvelopeContext {
|
|
47
|
+
readonly binding: TierBinding;
|
|
48
|
+
readonly registry: ProviderRegistry;
|
|
49
|
+
/** The user's original question, threaded into the synthesis prompt. */
|
|
50
|
+
readonly userInput: string;
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic render seed: a data-access tool's real result. When set
|
|
53
|
+
* and it validates against `output_schema`, the envelope is built straight
|
|
54
|
+
* from it and the render model is skipped entirely. See {@link ToolTableSeed}.
|
|
55
|
+
*/
|
|
56
|
+
readonly toolTable?: ToolTableSeed;
|
|
57
|
+
/**
|
|
58
|
+
* Deterministic render seed: a `build_dashboard` tool's real, already-
|
|
59
|
+
* assembled `{blocks}` result (see {@link DashboardSeed}). Takes priority
|
|
60
|
+
* over every other seed/path — see `renderEnvelope`'s seed-precedence order.
|
|
61
|
+
*/
|
|
62
|
+
readonly dashboardSeed?: DashboardSeed;
|
|
63
|
+
/**
|
|
64
|
+
* Tier to run the envelope-synthesis `generateText` call on. Defaults
|
|
65
|
+
* to the tier of the agent's *last* `independent` step (in `steps[]`
|
|
66
|
+
* array order) — the reasoning behind this default: the last independent
|
|
67
|
+
* step is the one that did the heaviest lifting toward the final answer
|
|
68
|
+
* (e.g. `generate_sql` in the golden `answer_query` shape), so synthesis
|
|
69
|
+
* warrants at least that much model strength. Callers with a dedicated
|
|
70
|
+
* "render" tier in their bundle can override this explicitly.
|
|
71
|
+
*/
|
|
72
|
+
readonly tier?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The two-stage structured-output envelope. Deliberately kept OUT of the
|
|
76
|
+
* `ToolLoopAgent` construction in `executeAgent` — attaching `Output.object`
|
|
77
|
+
* to a tool-bearing loop constrains every turn's output to the final
|
|
78
|
+
* schema, including turns that are just making tool calls. Instead, this
|
|
79
|
+
* runs as its own separate call, once, after the loop's dataflow artifacts
|
|
80
|
+
* have all been produced.
|
|
81
|
+
*
|
|
82
|
+
* This deliberately uses `generateText`, not `generateObject` — the schema
|
|
83
|
+
* is enforced in prompt text plus `collectJsonSchemaErrors` below, not via
|
|
84
|
+
* an SDK-level `responseFormat: json_schema`/structured-output request.
|
|
85
|
+
* `generateObject`'s `json_schema` response format isn't supported by every
|
|
86
|
+
* OpenAI-compatible endpoint (e.g. Ollama's `/v1` endpoint 400s on it), so a
|
|
87
|
+
* provider-agnostic envelope stage can't rely on it; `generateText` has no
|
|
88
|
+
* such requirement and works identically against mock, cloud, and local
|
|
89
|
+
* providers. Enforcement doesn't get any weaker: `jsonSchema()`'s own
|
|
90
|
+
* `validate` callback was already the only thing giving `output_schema`
|
|
91
|
+
* real teeth (it performs no runtime validation on its own without one), so
|
|
92
|
+
* running `collectJsonSchemaErrors` directly against the parsed JSON here
|
|
93
|
+
* is the same enforcement, just invoked by hand instead of through the SDK.
|
|
94
|
+
*/
|
|
95
|
+
export declare function renderEnvelope(agent: Agent, artifacts: DataflowArtifacts, ctx: RenderEnvelopeContext): Promise<RenderEnvelope>;
|
|
96
|
+
/**
|
|
97
|
+
* Extracts the set of block `type` consts an `output_schema` permits, by
|
|
98
|
+
* walking `properties.blocks.items` — either a single item schema (e.g.
|
|
99
|
+
* `explain_change`'s narrative-only shape) or an `anyOf` of several (e.g.
|
|
100
|
+
* `answer_query`'s table/definition shape, or `generate_dashboard`'s
|
|
101
|
+
* kpi_card/table/chart/definition shape). Each variant contributes its
|
|
102
|
+
* `properties.type.const`, if it declares one.
|
|
103
|
+
*
|
|
104
|
+
* Returns `undefined` when the schema declares no `type` consts at all (e.g.
|
|
105
|
+
* `explore_model`'s bare `{type: "object"}` item schema, which places no
|
|
106
|
+
* constraint on block shape) — an unconstrained schema is deliberately never
|
|
107
|
+
* treated as "table-only" by {@link isTableOnlySchema}, since there's no
|
|
108
|
+
* actual evidence it only permits table/definition blocks.
|
|
109
|
+
*/
|
|
110
|
+
export declare function getAllowedBlockTypes(outputSchema: Record<string, unknown>): Set<string> | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* True when `output_schema`'s `blocks.items` permits ONLY `table`/
|
|
113
|
+
* `definition` block types — the `answer_query` shape, and the exact
|
|
114
|
+
* precondition for applying `ToolTableSeed` (see `renderEnvelope`). An
|
|
115
|
+
* unconstrained schema (`getAllowedBlockTypes` returns `undefined`) is never
|
|
116
|
+
* table-only.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isTableOnlySchema(outputSchema: Record<string, unknown>): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Best-effort extraction of a {@link RenderEnvelope} from dispatched's raw
|
|
121
|
+
* `finalText` — a CLI stdout string (`DispatchedResult.finalText`), not a
|
|
122
|
+
* structured artifact map the way in-process's `executeAgent` output is. Dispatched
|
|
123
|
+
* has no dataflow-artifact scan or tool-outcome capture of its own (see
|
|
124
|
+
* `runDispatchedDefault` — it only reads the dispatcher's terminal stdout line),
|
|
125
|
+
* so this is the only signal available to build a `form: "rich"` answer from.
|
|
126
|
+
*
|
|
127
|
+
* The agent is expected to emit its final structured answer as a JSON
|
|
128
|
+
* object, plain or ```json-fenced (the same contract the render model
|
|
129
|
+
* itself follows — see `extractJsonObjectText`), in one of the shapes
|
|
130
|
+
* {@link normalizeToEnvelopeShape} already reconciles:
|
|
131
|
+
* - already `{blocks: [...]}` (e.g. `generate_dashboard`) -> used as-is.
|
|
132
|
+
* - flat `{columns, rows, verified?, summary?, definition?}` (`answer_query`'s
|
|
133
|
+
* actual tool contract) -> reshaped into a `table` (+ `definition`) block.
|
|
134
|
+
* - an MCP `CallToolResult` (`{content: [{type:"text", text}], ...}` or
|
|
135
|
+
* `{structuredContent}`) -> unwrapped one level and the same rules
|
|
136
|
+
* re-applied to what's inside. This is a known trap (see the
|
|
137
|
+
* harness-tooltable-seed-dead follow-up): the real `query` MCP tool's raw
|
|
138
|
+
* result is this wrapper shape, not the flat one above, so an extractor
|
|
139
|
+
* that only checked for `{columns, rows}` would silently miss it.
|
|
140
|
+
*
|
|
141
|
+
* Returns `undefined` when nothing recognizable is found (not JSON at all,
|
|
142
|
+
* or JSON that matches none of the above), so the caller can fall back to
|
|
143
|
+
* `form: "text"` instead of fabricating an envelope.
|
|
144
|
+
*/
|
|
145
|
+
export declare function extractEnvelopeFromText(text: string): RenderEnvelope | undefined;
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { generateText } from "ai";
|
|
2
|
+
import { resolveTierModel } from "../providers/index.js";
|
|
3
|
+
import { EnvelopeParseError, EnvelopeSchemaError, NoRenderTierError } from "./errors.js";
|
|
4
|
+
import { collectJsonSchemaErrors } from "./validate.js";
|
|
5
|
+
/** Bounded: at most one reformat attempt if the model's first response doesn't parse. */
|
|
6
|
+
const MAX_PARSE_ATTEMPTS = 2;
|
|
7
|
+
/**
|
|
8
|
+
* The two-stage structured-output envelope. Deliberately kept OUT of the
|
|
9
|
+
* `ToolLoopAgent` construction in `executeAgent` — attaching `Output.object`
|
|
10
|
+
* to a tool-bearing loop constrains every turn's output to the final
|
|
11
|
+
* schema, including turns that are just making tool calls. Instead, this
|
|
12
|
+
* runs as its own separate call, once, after the loop's dataflow artifacts
|
|
13
|
+
* have all been produced.
|
|
14
|
+
*
|
|
15
|
+
* This deliberately uses `generateText`, not `generateObject` — the schema
|
|
16
|
+
* is enforced in prompt text plus `collectJsonSchemaErrors` below, not via
|
|
17
|
+
* an SDK-level `responseFormat: json_schema`/structured-output request.
|
|
18
|
+
* `generateObject`'s `json_schema` response format isn't supported by every
|
|
19
|
+
* OpenAI-compatible endpoint (e.g. Ollama's `/v1` endpoint 400s on it), so a
|
|
20
|
+
* provider-agnostic envelope stage can't rely on it; `generateText` has no
|
|
21
|
+
* such requirement and works identically against mock, cloud, and local
|
|
22
|
+
* providers. Enforcement doesn't get any weaker: `jsonSchema()`'s own
|
|
23
|
+
* `validate` callback was already the only thing giving `output_schema`
|
|
24
|
+
* real teeth (it performs no runtime validation on its own without one), so
|
|
25
|
+
* running `collectJsonSchemaErrors` directly against the parsed JSON here
|
|
26
|
+
* is the same enforcement, just invoked by hand instead of through the SDK.
|
|
27
|
+
*/
|
|
28
|
+
export async function renderEnvelope(agent, artifacts, ctx) {
|
|
29
|
+
const outputSchema = agent.output_schema;
|
|
30
|
+
// Deterministic render seed (STRONGEST path, checked first): a
|
|
31
|
+
// `build_dashboard` tool's real, already-assembled `{blocks}` result
|
|
32
|
+
// (`ctx.dashboardSeed`). This is a complete dashboard envelope built
|
|
33
|
+
// deterministically by the native tool (see `assembleDashboardBlocks`), so
|
|
34
|
+
// there is nothing left to reconcile — it wins over the table-only
|
|
35
|
+
// `toolTable` seed below (whose gate below would skip a dashboard schema
|
|
36
|
+
// anyway) and over the artifact scan / render LLM. Only taken when it
|
|
37
|
+
// validates against `output_schema`; otherwise fall through (e.g.
|
|
38
|
+
// `build_dashboard` was never called for this run).
|
|
39
|
+
if (ctx.dashboardSeed !== undefined) {
|
|
40
|
+
const seeded = {
|
|
41
|
+
blocks: ctx.dashboardSeed.blocks,
|
|
42
|
+
...(ctx.dashboardSeed.summary !== undefined ? { summary: ctx.dashboardSeed.summary } : {}),
|
|
43
|
+
...(ctx.dashboardSeed.verified !== undefined ? { verified: ctx.dashboardSeed.verified } : {}),
|
|
44
|
+
};
|
|
45
|
+
if (collectJsonSchemaErrors(outputSchema, seeded).length === 0) {
|
|
46
|
+
return seeded;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Deterministic render seed: if the caller captured a data-access tool's
|
|
50
|
+
// real result (`ctx.toolTable` — e.g. the `query` tool's rows), build the
|
|
51
|
+
// envelope straight from it. This preserves the agent's actual data
|
|
52
|
+
// regardless of whether the step model chose to re-emit that structure as
|
|
53
|
+
// text (many models summarize it to prose, which the artifact scan below
|
|
54
|
+
// then can't recover). `verified: true` is warranted here specifically
|
|
55
|
+
// because the payload IS a successful, non-erroring execution result — the
|
|
56
|
+
// exact evidence `runAgent`'s deterministic `gated_check` requires.
|
|
57
|
+
//
|
|
58
|
+
// Gated on `isTableOnlySchema`: this seed only ever produces `table`/
|
|
59
|
+
// `definition` blocks, so it must only be applied when `output_schema`
|
|
60
|
+
// permits *just* those block types (the `answer_query` shape). Without
|
|
61
|
+
// this gate, a table-only seed captured from an incidental `query` call
|
|
62
|
+
// (e.g. inside `generate_dashboard`'s multi-panel flow) would fire FIRST
|
|
63
|
+
// and collapse a whole dashboard down to a single table — the schema
|
|
64
|
+
// validation below would eventually reject it (a dashboard schema also
|
|
65
|
+
// permits `kpi_card`/`chart`, so a table-only payload can actually still
|
|
66
|
+
// validate against it, since every block in the array only needs to match
|
|
67
|
+
// ONE `anyOf` branch), silently losing every other panel. Gating on the
|
|
68
|
+
// schema's allowed types catches this before validation, not after.
|
|
69
|
+
// Only taken when it also validates against `output_schema`; otherwise
|
|
70
|
+
// fall through.
|
|
71
|
+
if (ctx.toolTable !== undefined && isTableOnlySchema(outputSchema)) {
|
|
72
|
+
const seeded = normalizeToEnvelopeShape({
|
|
73
|
+
columns: ctx.toolTable.columns,
|
|
74
|
+
rows: ctx.toolTable.rows,
|
|
75
|
+
verified: true,
|
|
76
|
+
...(ctx.toolTable.summary !== undefined ? { summary: ctx.toolTable.summary } : {}),
|
|
77
|
+
...(ctx.toolTable.sql !== undefined
|
|
78
|
+
? { definition: { sql: ctx.toolTable.sql, source_tables: [], filters: [] } }
|
|
79
|
+
: {}),
|
|
80
|
+
});
|
|
81
|
+
if (collectJsonSchemaErrors(outputSchema, seeded).length === 0) {
|
|
82
|
+
return seeded;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Deterministic fast-path: if a terminal artifact is ALREADY a renderable
|
|
86
|
+
// payload (answer_query emits the flat {columns,rows,verified,definition};
|
|
87
|
+
// other agents may already emit a {blocks} envelope), build the envelope
|
|
88
|
+
// straight from it — no render LLM. This preserves the agent's real data
|
|
89
|
+
// (the table rows) instead of asking a render model to re-synthesize it,
|
|
90
|
+
// which is lossy (it tends to summarize the table away, leaving a bare
|
|
91
|
+
// `{type:"table"}`) and an extra call. Only taken when the result validates;
|
|
92
|
+
// otherwise fall through to the render-LLM path below.
|
|
93
|
+
const direct = tryDirectEnvelope(artifacts);
|
|
94
|
+
if (direct !== undefined && collectJsonSchemaErrors(outputSchema, direct).length === 0) {
|
|
95
|
+
return direct;
|
|
96
|
+
}
|
|
97
|
+
const tier = ctx.tier ?? defaultRenderTier(agent);
|
|
98
|
+
const model = resolveTierModel(ctx.binding, tier, ctx.registry);
|
|
99
|
+
const prompt = buildEnvelopePrompt(agent, artifacts, ctx.userInput, outputSchema);
|
|
100
|
+
let lastParseError;
|
|
101
|
+
for (let attempt = 1; attempt <= MAX_PARSE_ATTEMPTS; attempt += 1) {
|
|
102
|
+
const { text } = await generateText({ model, prompt: attempt === 1 ? prompt : reformatPrompt(prompt) });
|
|
103
|
+
let value;
|
|
104
|
+
try {
|
|
105
|
+
value = JSON.parse(extractJsonObjectText(text));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
lastParseError = error;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const normalized = normalizeToEnvelopeShape(value);
|
|
112
|
+
const errors = collectJsonSchemaErrors(outputSchema, normalized);
|
|
113
|
+
if (errors.length > 0) {
|
|
114
|
+
throw new EnvelopeSchemaError(agent.id, errors);
|
|
115
|
+
}
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
throw new EnvelopeParseError(agent.id, lastParseError);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `answer_query`'s ACTUAL tool contract (see `answer_tool.ts`/warble's own
|
|
122
|
+
* eval, which this must never change) is a FLAT table shape —
|
|
123
|
+
* `{columns, rows, verified, definition}` — not the `{blocks}`
|
|
124
|
+
* RenderEnvelope this module otherwise expects. Left unreconciled, that flat
|
|
125
|
+
* shape fails `output_schema` validation below and the run degrades to an
|
|
126
|
+
* unverified raw-JSON text blob (see `runAgent`'s `gated_check`, which only
|
|
127
|
+
* trusts `envelope.verified === true`).
|
|
128
|
+
*
|
|
129
|
+
* This is a compatibility/normalization shim, applied to every parsed
|
|
130
|
+
* candidate before schema validation:
|
|
131
|
+
* - Already has a `blocks` array (e.g. `generate_dashboard`, or any future
|
|
132
|
+
* agent that already speaks the block contract) → returned unchanged.
|
|
133
|
+
* - Otherwise, if it structurally matches the flat table shape (`columns`
|
|
134
|
+
* and `rows` are both arrays — a shape a real `{blocks: [...]}` envelope
|
|
135
|
+
* never has at its top level, so this can't misfire against one) →
|
|
136
|
+
* synthesize `blocks = [{type: "table", columns, rows}, ...(definition
|
|
137
|
+
* block, if present)]` and carry `verified`/`summary` onto the result,
|
|
138
|
+
* so the flat payload renders as a verified table instead of failing
|
|
139
|
+
* validation.
|
|
140
|
+
* - Anything else (unrecognized shape) → returned unchanged, so it still
|
|
141
|
+
* fails `output_schema` validation below with a useful error rather than
|
|
142
|
+
* being silently coerced into something misleading.
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* Scans the loop's dataflow artifacts for one that is ALREADY a renderable
|
|
146
|
+
* payload and builds the envelope from it deterministically (via
|
|
147
|
+
* `normalizeToEnvelopeShape`), skipping the render LLM. Artifacts are checked
|
|
148
|
+
* in reverse insertion order so the agent's terminal output (the last step's
|
|
149
|
+
* result) wins over any intermediate one. String artifacts are parsed the same
|
|
150
|
+
* way a model response would be. Returns `undefined` when nothing is directly
|
|
151
|
+
* renderable, so the caller falls back to the render-LLM path.
|
|
152
|
+
*/
|
|
153
|
+
function tryDirectEnvelope(artifacts) {
|
|
154
|
+
for (const raw of [...artifacts.values()].reverse()) {
|
|
155
|
+
const candidate = coerceToObject(raw);
|
|
156
|
+
if (candidate === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
if (hasBlocksArray(candidate) || isFlatTablePayload(candidate)) {
|
|
159
|
+
return normalizeToEnvelopeShape(candidate);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
/** An artifact value as a parsed object: objects pass through; strings are
|
|
165
|
+
* parsed like a model response (fence-stripped); anything else is skipped. */
|
|
166
|
+
function coerceToObject(raw) {
|
|
167
|
+
if (typeof raw === "object" && raw !== null)
|
|
168
|
+
return raw;
|
|
169
|
+
if (typeof raw === "string") {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(extractJsonObjectText(raw));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
function normalizeToEnvelopeShape(value) {
|
|
180
|
+
if (hasBlocksArray(value))
|
|
181
|
+
return value;
|
|
182
|
+
const candidate = isCodexQueryResultPayload(value)
|
|
183
|
+
? codexQueryResultToFlatTable(value)
|
|
184
|
+
: value;
|
|
185
|
+
if (!isFlatTablePayload(candidate))
|
|
186
|
+
return value;
|
|
187
|
+
const { columns, rows, verified, summary, definition } = candidate;
|
|
188
|
+
const blocks = [{ type: "table", columns, rows }];
|
|
189
|
+
if (definition && typeof definition === "object") {
|
|
190
|
+
const { sql, source_tables, filters } = definition;
|
|
191
|
+
blocks.push({ type: "definition", sql, source_tables, filters });
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
blocks,
|
|
195
|
+
...(summary !== undefined ? { summary } : {}),
|
|
196
|
+
...(verified !== undefined ? { verified } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const TABLE_ONLY_BLOCK_TYPES = new Set(["table", "definition"]);
|
|
200
|
+
/**
|
|
201
|
+
* Extracts the set of block `type` consts an `output_schema` permits, by
|
|
202
|
+
* walking `properties.blocks.items` — either a single item schema (e.g.
|
|
203
|
+
* `explain_change`'s narrative-only shape) or an `anyOf` of several (e.g.
|
|
204
|
+
* `answer_query`'s table/definition shape, or `generate_dashboard`'s
|
|
205
|
+
* kpi_card/table/chart/definition shape). Each variant contributes its
|
|
206
|
+
* `properties.type.const`, if it declares one.
|
|
207
|
+
*
|
|
208
|
+
* Returns `undefined` when the schema declares no `type` consts at all (e.g.
|
|
209
|
+
* `explore_model`'s bare `{type: "object"}` item schema, which places no
|
|
210
|
+
* constraint on block shape) — an unconstrained schema is deliberately never
|
|
211
|
+
* treated as "table-only" by {@link isTableOnlySchema}, since there's no
|
|
212
|
+
* actual evidence it only permits table/definition blocks.
|
|
213
|
+
*/
|
|
214
|
+
export function getAllowedBlockTypes(outputSchema) {
|
|
215
|
+
const items = extractBlocksItemsSchema(outputSchema);
|
|
216
|
+
if (items === undefined)
|
|
217
|
+
return undefined;
|
|
218
|
+
const variants = Array.isArray(items.anyOf) ? items.anyOf : [items];
|
|
219
|
+
const types = new Set();
|
|
220
|
+
for (const variant of variants) {
|
|
221
|
+
if (!isPlainRecord(variant))
|
|
222
|
+
continue;
|
|
223
|
+
const properties = variant.properties;
|
|
224
|
+
if (!isPlainRecord(properties))
|
|
225
|
+
continue;
|
|
226
|
+
const typeSchema = properties.type;
|
|
227
|
+
if (isPlainRecord(typeSchema) && typeof typeSchema.const === "string") {
|
|
228
|
+
types.add(typeSchema.const);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return types.size > 0 ? types : undefined;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* True when `output_schema`'s `blocks.items` permits ONLY `table`/
|
|
235
|
+
* `definition` block types — the `answer_query` shape, and the exact
|
|
236
|
+
* precondition for applying `ToolTableSeed` (see `renderEnvelope`). An
|
|
237
|
+
* unconstrained schema (`getAllowedBlockTypes` returns `undefined`) is never
|
|
238
|
+
* table-only.
|
|
239
|
+
*/
|
|
240
|
+
export function isTableOnlySchema(outputSchema) {
|
|
241
|
+
const allowed = getAllowedBlockTypes(outputSchema);
|
|
242
|
+
if (allowed === undefined)
|
|
243
|
+
return false;
|
|
244
|
+
for (const type of allowed) {
|
|
245
|
+
if (!TABLE_ONLY_BLOCK_TYPES.has(type))
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
function extractBlocksItemsSchema(outputSchema) {
|
|
251
|
+
const properties = outputSchema.properties;
|
|
252
|
+
if (!isPlainRecord(properties))
|
|
253
|
+
return undefined;
|
|
254
|
+
const blocks = properties.blocks;
|
|
255
|
+
if (!isPlainRecord(blocks))
|
|
256
|
+
return undefined;
|
|
257
|
+
const items = blocks.items;
|
|
258
|
+
return isPlainRecord(items) ? items : undefined;
|
|
259
|
+
}
|
|
260
|
+
function isPlainRecord(value) {
|
|
261
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
262
|
+
}
|
|
263
|
+
function hasBlocksArray(value) {
|
|
264
|
+
return typeof value === "object" && value !== null && Array.isArray(value.blocks);
|
|
265
|
+
}
|
|
266
|
+
function isFlatTablePayload(value) {
|
|
267
|
+
if (typeof value !== "object" || value === null)
|
|
268
|
+
return false;
|
|
269
|
+
const record = value;
|
|
270
|
+
return Array.isArray(record.columns) && Array.isArray(record.rows);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* codex:local's terminal child value is deliberately an execution-evidence
|
|
274
|
+
* object rather than the legacy flat answer_query payload. Recognize only
|
|
275
|
+
* the complete shape: a partial or model-invented lookalike must fall back
|
|
276
|
+
* to unverified text instead of being promoted into a verified table.
|
|
277
|
+
*/
|
|
278
|
+
function isCodexQueryResultPayload(value) {
|
|
279
|
+
if (!isPlainRecord(value) || !isPlainRecord(value.result_set))
|
|
280
|
+
return false;
|
|
281
|
+
const resultSet = value.result_set;
|
|
282
|
+
return (Array.isArray(resultSet.columns) &&
|
|
283
|
+
resultSet.columns.every((column) => typeof column === "string") &&
|
|
284
|
+
Array.isArray(resultSet.rows) &&
|
|
285
|
+
typeof value.sql === "string" &&
|
|
286
|
+
Array.isArray(value.source_tables) &&
|
|
287
|
+
value.source_tables.every((table) => typeof table === "string") &&
|
|
288
|
+
Array.isArray(value.filters) &&
|
|
289
|
+
typeof value.execution_passed === "boolean" &&
|
|
290
|
+
typeof value.validation_passed === "boolean" &&
|
|
291
|
+
(typeof value.validation_error === "string" || value.validation_error === null));
|
|
292
|
+
}
|
|
293
|
+
function codexQueryResultToFlatTable(value) {
|
|
294
|
+
const { columns, rows } = value.result_set;
|
|
295
|
+
const positionalRows = rows.map((row) => isPlainRecord(row) ? columns.map((column) => row[column]) : row);
|
|
296
|
+
return {
|
|
297
|
+
columns,
|
|
298
|
+
rows: positionalRows,
|
|
299
|
+
verified: value.execution_passed &&
|
|
300
|
+
value.validation_passed &&
|
|
301
|
+
value.validation_error === null,
|
|
302
|
+
definition: {
|
|
303
|
+
sql: value.sql,
|
|
304
|
+
source_tables: value.source_tables,
|
|
305
|
+
filters: value.filters,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Extracts a JSON object from a model's raw text response: strips
|
|
311
|
+
* ```json …``` / ``` fences, trims surrounding prose, and locates the outer
|
|
312
|
+
* `{ … }` object by its first `{` and matching last `}`. Throws (falls
|
|
313
|
+
* through to `JSON.parse`'s own error) if no object-shaped substring is
|
|
314
|
+
* found, so callers can treat "couldn't extract" and "extracted but invalid
|
|
315
|
+
* JSON" the same way.
|
|
316
|
+
*/
|
|
317
|
+
function extractJsonObjectText(text) {
|
|
318
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
319
|
+
const candidate = (fenced?.[1] ?? text).trim();
|
|
320
|
+
const start = candidate.indexOf("{");
|
|
321
|
+
const end = candidate.lastIndexOf("}");
|
|
322
|
+
if (start === -1 || end === -1 || end < start) {
|
|
323
|
+
throw new SyntaxError(`no JSON object found in model response: ${JSON.stringify(text.slice(0, 200))}`);
|
|
324
|
+
}
|
|
325
|
+
return candidate.slice(start, end + 1);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Best-effort extraction of a {@link RenderEnvelope} from dispatched's raw
|
|
329
|
+
* `finalText` — a CLI stdout string (`DispatchedResult.finalText`), not a
|
|
330
|
+
* structured artifact map the way in-process's `executeAgent` output is. Dispatched
|
|
331
|
+
* has no dataflow-artifact scan or tool-outcome capture of its own (see
|
|
332
|
+
* `runDispatchedDefault` — it only reads the dispatcher's terminal stdout line),
|
|
333
|
+
* so this is the only signal available to build a `form: "rich"` answer from.
|
|
334
|
+
*
|
|
335
|
+
* The agent is expected to emit its final structured answer as a JSON
|
|
336
|
+
* object, plain or ```json-fenced (the same contract the render model
|
|
337
|
+
* itself follows — see `extractJsonObjectText`), in one of the shapes
|
|
338
|
+
* {@link normalizeToEnvelopeShape} already reconciles:
|
|
339
|
+
* - already `{blocks: [...]}` (e.g. `generate_dashboard`) -> used as-is.
|
|
340
|
+
* - flat `{columns, rows, verified?, summary?, definition?}` (`answer_query`'s
|
|
341
|
+
* actual tool contract) -> reshaped into a `table` (+ `definition`) block.
|
|
342
|
+
* - an MCP `CallToolResult` (`{content: [{type:"text", text}], ...}` or
|
|
343
|
+
* `{structuredContent}`) -> unwrapped one level and the same rules
|
|
344
|
+
* re-applied to what's inside. This is a known trap (see the
|
|
345
|
+
* harness-tooltable-seed-dead follow-up): the real `query` MCP tool's raw
|
|
346
|
+
* result is this wrapper shape, not the flat one above, so an extractor
|
|
347
|
+
* that only checked for `{columns, rows}` would silently miss it.
|
|
348
|
+
*
|
|
349
|
+
* Returns `undefined` when nothing recognizable is found (not JSON at all,
|
|
350
|
+
* or JSON that matches none of the above), so the caller can fall back to
|
|
351
|
+
* `form: "text"` instead of fabricating an envelope.
|
|
352
|
+
*/
|
|
353
|
+
export function extractEnvelopeFromText(text) {
|
|
354
|
+
let candidate;
|
|
355
|
+
try {
|
|
356
|
+
candidate = JSON.parse(extractJsonObjectText(text));
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
return normalizeExtractedCandidate(candidate);
|
|
362
|
+
}
|
|
363
|
+
/** Bounded: at most one MCP `CallToolResult` unwrap, so a pathological/self-referential payload can't recurse forever. */
|
|
364
|
+
const MAX_UNWRAP_DEPTH = 2;
|
|
365
|
+
function normalizeExtractedCandidate(value, depth = 0) {
|
|
366
|
+
const normalized = normalizeToEnvelopeShape(value);
|
|
367
|
+
if (hasBlocksArray(normalized))
|
|
368
|
+
return normalized;
|
|
369
|
+
if (depth >= MAX_UNWRAP_DEPTH)
|
|
370
|
+
return undefined;
|
|
371
|
+
const unwrapped = unwrapCallToolResult(value);
|
|
372
|
+
return unwrapped === undefined ? undefined : normalizeExtractedCandidate(unwrapped, depth + 1);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Unwraps one layer of an MCP `CallToolResult` (`{content, structuredContent,
|
|
376
|
+
* isError}`) to the value inside — `structuredContent` when present,
|
|
377
|
+
* otherwise the first `type: "text"` content item's own JSON (fence-stripped
|
|
378
|
+
* the same way a model response is). Returns `undefined` for anything that
|
|
379
|
+
* isn't recognizably this shape.
|
|
380
|
+
*/
|
|
381
|
+
function unwrapCallToolResult(value) {
|
|
382
|
+
if (!isPlainRecord(value))
|
|
383
|
+
return undefined;
|
|
384
|
+
if (value.structuredContent !== undefined)
|
|
385
|
+
return value.structuredContent;
|
|
386
|
+
if (!Array.isArray(value.content))
|
|
387
|
+
return undefined;
|
|
388
|
+
const textItem = value.content.find((item) => isPlainRecord(item) && item.type === "text" && typeof item.text === "string");
|
|
389
|
+
if (textItem === undefined)
|
|
390
|
+
return undefined;
|
|
391
|
+
try {
|
|
392
|
+
return JSON.parse(extractJsonObjectText(textItem.text));
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/** Wraps the original prompt with a stricter instruction for the one bounded retry. */
|
|
399
|
+
function reformatPrompt(prompt) {
|
|
400
|
+
return (`${prompt}\n\nYour previous response could not be parsed as a single JSON object. ` +
|
|
401
|
+
`Respond again with ONLY the JSON object — no prose, no markdown code fences.`);
|
|
402
|
+
}
|
|
403
|
+
function defaultRenderTier(agent) {
|
|
404
|
+
const independentSteps = agent.steps.filter((step) => step.realization.kind === "independent");
|
|
405
|
+
const lastIndependent = independentSteps[independentSteps.length - 1];
|
|
406
|
+
if (!lastIndependent) {
|
|
407
|
+
throw new NoRenderTierError(agent.id);
|
|
408
|
+
}
|
|
409
|
+
return lastIndependent.tier;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Renders every accumulated dataflow artifact plus `output_schema` into the
|
|
413
|
+
* synthesis prompt. Since there's no SDK-level `responseFormat` doing the
|
|
414
|
+
* asking for us (see `renderEnvelope`), the instruction to emit bare JSON
|
|
415
|
+
* matching the schema — and nothing else — has to live in the prompt text.
|
|
416
|
+
*/
|
|
417
|
+
function buildEnvelopePrompt(agent, artifacts, userInput, outputSchema) {
|
|
418
|
+
const sections = [
|
|
419
|
+
`User's question: ${userInput}`,
|
|
420
|
+
`Synthesize the final structured answer for agent "${agent.id}" from the artifacts below.`,
|
|
421
|
+
];
|
|
422
|
+
for (const [name, value] of artifacts) {
|
|
423
|
+
sections.push(`${name}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
424
|
+
}
|
|
425
|
+
sections.push(`Respond with ONLY a single JSON object conforming to this JSON Schema — no prose, ` +
|
|
426
|
+
`no markdown code fences, no explanation before or after it:\n${JSON.stringify(outputSchema)}`);
|
|
427
|
+
return sections.join("\n\n");
|
|
428
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when the render envelope stage (the two-stage structured-output
|
|
3
|
+
* synthesis) needs a tier to run on but the caller didn't supply one
|
|
4
|
+
* explicitly and the agent has no `independent` step to fall back to —
|
|
5
|
+
* e.g. an agent made up entirely of `repair_fold` steps. This should never
|
|
6
|
+
* happen for a v1-scoped agent (`executeAgent` already requires at least
|
|
7
|
+
* one independent step to produce anything), but the envelope stage
|
|
8
|
+
* doesn't assume that invariant on its own.
|
|
9
|
+
*/
|
|
10
|
+
export declare class NoRenderTierError extends Error {
|
|
11
|
+
constructor(agentId: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Thrown when the model's envelope response parses as JSON but doesn't
|
|
15
|
+
* structurally match `agent.output_schema` — checked by hand via
|
|
16
|
+
* `collectJsonSchemaErrors` against the `generateText` output (see
|
|
17
|
+
* `renderEnvelope`; the envelope stage doesn't use `generateObject`'s
|
|
18
|
+
* SDK-level schema enforcement, so this replaces the errors that used to
|
|
19
|
+
* surface as a `NoObjectGeneratedError`'s cause).
|
|
20
|
+
*/
|
|
21
|
+
export declare class EnvelopeSchemaError extends Error {
|
|
22
|
+
constructor(agentId: string, violations: readonly string[]);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Thrown when the model's envelope response still doesn't extract into
|
|
26
|
+
* parseable JSON after `renderEnvelope`'s one bounded reformat retry (see
|
|
27
|
+
* `extractJsonObjectText`). Distinct from `EnvelopeSchemaError`, which is
|
|
28
|
+
* for JSON that parses but doesn't match `output_schema` — this is for
|
|
29
|
+
* responses that never became valid JSON at all.
|
|
30
|
+
*/
|
|
31
|
+
export declare class EnvelopeParseError extends Error {
|
|
32
|
+
constructor(agentId: string, cause: unknown);
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when the render envelope stage (the two-stage structured-output
|
|
3
|
+
* synthesis) needs a tier to run on but the caller didn't supply one
|
|
4
|
+
* explicitly and the agent has no `independent` step to fall back to —
|
|
5
|
+
* e.g. an agent made up entirely of `repair_fold` steps. This should never
|
|
6
|
+
* happen for a v1-scoped agent (`executeAgent` already requires at least
|
|
7
|
+
* one independent step to produce anything), but the envelope stage
|
|
8
|
+
* doesn't assume that invariant on its own.
|
|
9
|
+
*/
|
|
10
|
+
export class NoRenderTierError extends Error {
|
|
11
|
+
constructor(agentId) {
|
|
12
|
+
super(`agent "${agentId}" has no "independent" step to default the render envelope's ` +
|
|
13
|
+
`tier from, and no explicit tier was supplied to renderEnvelope()`);
|
|
14
|
+
this.name = "NoRenderTierError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when the model's envelope response parses as JSON but doesn't
|
|
19
|
+
* structurally match `agent.output_schema` — checked by hand via
|
|
20
|
+
* `collectJsonSchemaErrors` against the `generateText` output (see
|
|
21
|
+
* `renderEnvelope`; the envelope stage doesn't use `generateObject`'s
|
|
22
|
+
* SDK-level schema enforcement, so this replaces the errors that used to
|
|
23
|
+
* surface as a `NoObjectGeneratedError`'s cause).
|
|
24
|
+
*/
|
|
25
|
+
export class EnvelopeSchemaError extends Error {
|
|
26
|
+
constructor(agentId, violations) {
|
|
27
|
+
super(`agent "${agentId}"'s render envelope does not match its output_schema:\n` +
|
|
28
|
+
violations.map((violation) => ` - ${violation}`).join("\n"));
|
|
29
|
+
this.name = "EnvelopeSchemaError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when the model's envelope response still doesn't extract into
|
|
34
|
+
* parseable JSON after `renderEnvelope`'s one bounded reformat retry (see
|
|
35
|
+
* `extractJsonObjectText`). Distinct from `EnvelopeSchemaError`, which is
|
|
36
|
+
* for JSON that parses but doesn't match `output_schema` — this is for
|
|
37
|
+
* responses that never became valid JSON at all.
|
|
38
|
+
*/
|
|
39
|
+
export class EnvelopeParseError extends Error {
|
|
40
|
+
constructor(agentId, cause) {
|
|
41
|
+
super(`agent "${agentId}"'s render envelope response could not be parsed as JSON after retrying: ` +
|
|
42
|
+
`${cause instanceof Error ? cause.message : String(cause)}`);
|
|
43
|
+
this.name = "EnvelopeParseError";
|
|
44
|
+
this.cause = cause;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { extractEnvelopeFromText, getAllowedBlockTypes, isTableOnlySchema, renderEnvelope } from "./envelope.js";
|
|
2
|
+
export type { DashboardSeed, RenderEnvelope, RenderEnvelopeContext, ToolTableSeed } from "./envelope.js";
|
|
3
|
+
export { EnvelopeParseError, EnvelopeSchemaError, NoRenderTierError } from "./errors.js";
|
|
4
|
+
export { collectJsonSchemaErrors } from "./validate.js";
|
|
5
|
+
export type { JsonSchemaDocument } from "./validate.js";
|