@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,84 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { loadBundleWithProvenance } from "../bundle/loader.js";
|
|
3
|
+
import { compileProfile, compileRawProfile } from "../compile/pipeline.js";
|
|
4
|
+
import { buildAgentSdkManifestArgs, runAgentSdkManifest } from "./agent-sdk-manifest.js";
|
|
5
|
+
import { resolveAgentSdkCli } from "./agent-sdk-cli.js";
|
|
6
|
+
import { resolveCodexLocalCli } from "./codex-local-cli.js";
|
|
7
|
+
import { describeCodexAskManifest, describeCodexBootstrapManifest, describeCodexEnrichmentManifest } from "./codex-local-manifest.js";
|
|
8
|
+
/**
|
|
9
|
+
* Describes whichever back-end will ACTUALLY run a turn for
|
|
10
|
+
* `options.authChoice` — the invariant this maintains is: Harness-shown
|
|
11
|
+
* target == `runtimeDispatcher`'s prediction (`server/harness.ts`)
|
|
12
|
+
* == what `route()` really dispatches to. The IR itself stays target-neutral
|
|
13
|
+
* either way; only the DISPLAY artifact this function loads differs:
|
|
14
|
+
*
|
|
15
|
+
* - `authChoice.mode === "subscription"` (dispatched): compiles IR only
|
|
16
|
+
* (`mode: "native"`, reusing `compileProfile`'s cache) — no vercel bundle
|
|
17
|
+
* is produced on this path at all — then sources the display from the
|
|
18
|
+
* claude-agent-sdk dispatcher's OWN `manifest` subcommand, which reads
|
|
19
|
+
* that same IR and emits a structurally-identical-to-the-vercel-bundle
|
|
20
|
+
* JSON (`target: "claude-agent-sdk:local"`) to stdout. This is genuinely
|
|
21
|
+
* what dispatched runs — `runDispatchedDefault` (`./dispatched.js`) shells this same
|
|
22
|
+
* dispatcher's `chat` subcommand against the same IR.
|
|
23
|
+
* - every other `authChoice.mode` (in-process: api-key/local/gateway): compiles
|
|
24
|
+
* to a vercel bundle (`mode: "agnostic"`, `warble dispatch --target
|
|
25
|
+
* vercel`) — unchanged pre-existing behavior; `runInProcessDefault`
|
|
26
|
+
* (`./in-process.js`) calls this too rather than keeping its own copy.
|
|
27
|
+
*/
|
|
28
|
+
export async function describeBundle(options) {
|
|
29
|
+
if (options.authChoice.mode === "subscription") {
|
|
30
|
+
if (options.authChoice.provider === "codex")
|
|
31
|
+
return describeCodexManifest(options);
|
|
32
|
+
return describeAgentSdkManifest(options);
|
|
33
|
+
}
|
|
34
|
+
return describeVercelBundle(options);
|
|
35
|
+
}
|
|
36
|
+
async function describeCodexManifest(options) {
|
|
37
|
+
const compiled = await compileForDescription(options, "native");
|
|
38
|
+
const purpose = options.codexManifestPurpose ?? (options.context === "bootstrap" ? "setup" : "analysis");
|
|
39
|
+
if (purpose === "setup") {
|
|
40
|
+
const cli = await resolveCodexLocalCli(options.codexLocalBin);
|
|
41
|
+
return describeCodexBootstrapManifest(cli, compiled.irPath);
|
|
42
|
+
}
|
|
43
|
+
const models = options.codexModels;
|
|
44
|
+
if (!models || !models.orchestrator.trim() || !models.cheap.trim() || !models.strong.trim()) {
|
|
45
|
+
throw new Error("Codex manifest requires orchestrator, cheap, and strong model bindings");
|
|
46
|
+
}
|
|
47
|
+
const cli = await resolveCodexLocalCli(options.codexLocalBin);
|
|
48
|
+
return purpose === "context_enrichment"
|
|
49
|
+
? describeCodexEnrichmentManifest(cli, compiled.irPath, models)
|
|
50
|
+
: describeCodexAskManifest(cli, compiled.irPath, models);
|
|
51
|
+
}
|
|
52
|
+
async function describeVercelBundle(options) {
|
|
53
|
+
const compiled = await compileForDescription(options, "agnostic");
|
|
54
|
+
// `mode: "agnostic"` always produces a bundle — see `compileProfile`'s doc comment.
|
|
55
|
+
const bundleJson = JSON.parse(await readFile(compiled.bundlePath, "utf-8"));
|
|
56
|
+
return loadBundleWithProvenance(bundleJson, {
|
|
57
|
+
...(compiled.warbleBin !== undefined ? { warbleBin: compiled.warbleBin } : {}),
|
|
58
|
+
profileSource: options.profileSource,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function describeAgentSdkManifest(options) {
|
|
62
|
+
const compiled = await compileForDescription(options, "native");
|
|
63
|
+
const cli = await resolveAgentSdkCli(options.agentSdkBin);
|
|
64
|
+
const { command, args } = buildAgentSdkManifestArgs(cli, {
|
|
65
|
+
irPath: compiled.irPath,
|
|
66
|
+
...(options.context !== "bootstrap" ? { userProject: options.userProject } : {}),
|
|
67
|
+
});
|
|
68
|
+
const stdout = await runAgentSdkManifest(command, args);
|
|
69
|
+
return loadBundleWithProvenance(JSON.parse(stdout), {
|
|
70
|
+
...(compiled.warbleBin !== undefined ? { warbleBin: compiled.warbleBin } : {}),
|
|
71
|
+
profileSource: options.profileSource,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function compileForDescription(options, mode) {
|
|
75
|
+
const shared = {
|
|
76
|
+
profileSource: options.profileSource,
|
|
77
|
+
mode,
|
|
78
|
+
...(options.warbleBin !== undefined ? { warbleBin: options.warbleBin } : {}),
|
|
79
|
+
...(options.workDir !== undefined ? { workDir: options.workDir } : {}),
|
|
80
|
+
};
|
|
81
|
+
return options.context === "bootstrap"
|
|
82
|
+
? compileRawProfile(shared)
|
|
83
|
+
: compileProfile({ ...shared, userProject: options.userProject });
|
|
84
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ResolvedCli } from "./agent-sdk-cli.js";
|
|
2
|
+
import type { DispatchedOptions, DispatchedResult } from "./types.js";
|
|
3
|
+
export interface AgentSdkChatCommand {
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly args: readonly string[];
|
|
6
|
+
/** Fed to the child process over stdin, never interpolated into `args`. */
|
|
7
|
+
readonly input: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BuildAgentSdkChatArgsOptions {
|
|
10
|
+
readonly irPath: string;
|
|
11
|
+
readonly userProject: string;
|
|
12
|
+
readonly question: string;
|
|
13
|
+
readonly outDir: string;
|
|
14
|
+
readonly warbleBin: string;
|
|
15
|
+
/**
|
|
16
|
+
* Which warble component `chat` dispatches, forwarded
|
|
17
|
+
* verbatim as `--component <agentId>`. Defaults to `MODE_B_AGENT_ID`
|
|
18
|
+
* (`"answer_query"`) when unset — the original default from before this
|
|
19
|
+
* option existed. Named `agentId`
|
|
20
|
+
* to match `InProcessOptions.agentId` (intent routing), even though
|
|
21
|
+
* Dispatched has no per-agent bundle concept of its own — it's the same
|
|
22
|
+
* "which compiled thing to run" knob, just a component id on the warble
|
|
23
|
+
* IR side rather than an agent id on a compiled vercel bundle.
|
|
24
|
+
*/
|
|
25
|
+
readonly agentId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The "hybrid" passthrough: forwarded verbatim as `--models-config
|
|
28
|
+
* <modelsConfig>` when set, omitted entirely otherwise (existing callers
|
|
29
|
+
* that don't hybrid-route are byte-for-byte unaffected). This lets the
|
|
30
|
+
* subscription main loop (dispatched) route one step onto a non-Anthropic
|
|
31
|
+
* model per warble's own `ModelConfig.fromYaml` mechanism — see
|
|
32
|
+
* `runDispatchedDefault`'s doc comment for the known limitations this inherits.
|
|
33
|
+
*/
|
|
34
|
+
readonly modelsConfig?: string;
|
|
35
|
+
/** Forwarded as `--max-turns <n>` when set; omitted otherwise so warble's default applies. */
|
|
36
|
+
readonly maxTurns?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Session resume: forwarded verbatim as `--resume <id>` when set, so the dispatcher resumes
|
|
39
|
+
* the SAME agent-sdk conversation instead of starting a fresh one. See
|
|
40
|
+
* `DispatchedOptions.resumeSessionId`.
|
|
41
|
+
*/
|
|
42
|
+
readonly resume?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Builds the argv (and stdin input) for a `warble-agent-sdk chat` invocation
|
|
46
|
+
* without spawning anything — the validated shape from the earlier spike:
|
|
47
|
+
* `echo "<question>" | <agent-sdk-cli> chat <ir.json> --project <userProject>
|
|
48
|
+
* --component <agentId, default "answer_query"> --out <dir> --warble-bin
|
|
49
|
+
* <warble> [--models-config <path>] --stream-json`. `--stream-json` is always
|
|
50
|
+
* passed so `spawnChat` can incrementally parse per-step/per-tool NDJSON
|
|
51
|
+
* events off stdout instead of only getting the plain final-answer line. The
|
|
52
|
+
* question is always routed through `input` (stdin), never interpolated into
|
|
53
|
+
* `args`, so it can never break argv parsing — there is no shell involved
|
|
54
|
+
* either (`spawn`, not `exec`).
|
|
55
|
+
*
|
|
56
|
+
* `warble-agent-sdk chat` is a line-per-turn REPL over stdin — one
|
|
57
|
+
* line is one turn. A question containing a newline (e.g. the BFF's
|
|
58
|
+
* multi-turn Ask composing the last ~5 turns of prior context ahead of the
|
|
59
|
+
* follow-up) would silently become *multiple* turns if sent as-is — only the
|
|
60
|
+
* last turn's answer would ever be returned. The dispatcher can't be changed
|
|
61
|
+
* (it's a separate, soon-public repo), so instead of rejecting a multi-line
|
|
62
|
+
* question, `encodeQuestionForStdin` collapses its interior newlines into a
|
|
63
|
+
* literal `\n` escape so the whole thing still crosses stdin as exactly one
|
|
64
|
+
* line/turn; an LLM reads a literal `\n` in prose as a line break just fine.
|
|
65
|
+
* A single-line question has nothing to collapse and passes through
|
|
66
|
+
* unchanged. An empty/whitespace-only question is never a meaningful turn at
|
|
67
|
+
* all and is still rejected here, before anything is spawned.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildAgentSdkChatArgs(cli: ResolvedCli, options: BuildAgentSdkChatArgsOptions): AgentSdkChatCommand;
|
|
70
|
+
/**
|
|
71
|
+
* Dispatched ("subscription"): compile the profile to IR only (`mode: "native"` —
|
|
72
|
+
* no vercel bundle; the claude-agent-sdk dispatcher reads IR directly and
|
|
73
|
+
* drives its own Claude Agent SDK `query()` loop at runtime, so no adapter/
|
|
74
|
+
* tier binding is assembled here), then shell the warble-agent-sdk CLI's
|
|
75
|
+
* `chat` subcommand, feeding the question over stdin and returning its
|
|
76
|
+
* finishing text.
|
|
77
|
+
*
|
|
78
|
+
* The "hybrid" passthrough: `options.modelsConfig`, when set, becomes
|
|
79
|
+
* `--models-config <path>` on the `chat` invocation (see
|
|
80
|
+
* `buildAgentSdkChatArgs`), letting the subscription main loop route one
|
|
81
|
+
* step (e.g. `resolve_intent`'s `cheap` tier) onto a non-Anthropic model via
|
|
82
|
+
* warble's own `ModelConfig.fromYaml` binding
|
|
83
|
+
* (`dispatcher/claude-agent-sdk/src/models.ts`). This harness only forwards
|
|
84
|
+
* the path — it never parses the YAML or the resulting per-tier routing
|
|
85
|
+
* itself, and inherits warble's own hybrid-mode holes as of this writing
|
|
86
|
+
* (tracked upstream, not fixed here): the render stage wall-hits on any
|
|
87
|
+
* non-`render: none` component under a non-Anthropic tier, the
|
|
88
|
+
* `openai_compat` local client has no streaming/retry, and net cost savings
|
|
89
|
+
* are unproven. Weak local models are also known to call tools unreliably —
|
|
90
|
+
* a separate, model-quality issue rather than a wiring bug. See the opt-in
|
|
91
|
+
* live hybrid tests (`test/hybrid-live.test.ts`) for how to exercise this
|
|
92
|
+
* against a real endpoint.
|
|
93
|
+
*
|
|
94
|
+
* As a belt-and-suspenders measure, `route()` already runs the compliance gate
|
|
95
|
+
* (`enforceCompliance`) before ever calling this function, but
|
|
96
|
+
* `runDispatchedDefault` is also exported from the package root — an embedder
|
|
97
|
+
* that imports it directly bypasses `route()`, and with it that gate,
|
|
98
|
+
* entirely. Re-running `enforceCompliance` here makes the executor safe
|
|
99
|
+
* regardless of entry point: `route()`'s call and this one are the same
|
|
100
|
+
* pure, idempotent decision (subscription + hosted always throws,
|
|
101
|
+
* subscription + personal always allows), so double-gating through
|
|
102
|
+
* `route()` costs nothing and closes the direct-import hole.
|
|
103
|
+
*
|
|
104
|
+
* Session resume (Plan A): `options.resumeSessionId`, when set, is forwarded as `--resume <id>` so
|
|
105
|
+
* the dispatcher resumes the SAME agent-sdk conversation for this turn instead of starting a fresh
|
|
106
|
+
* one — the agent keeps whatever it already read/listed/fetched, rather than re-orienting from
|
|
107
|
+
* scratch on every turn. The turn's own session id (for resuming a LATER turn) comes back on
|
|
108
|
+
* `DispatchedResult.sessionId` on success, and — since a resumable session id from a failed turn is just
|
|
109
|
+
* as useful as one from a successful turn (an `error_max_turns` failure is exactly the case this
|
|
110
|
+
* exists for) — is still recoverable from a thrown `DispatchedSessionError`'s `sessionId` field when this
|
|
111
|
+
* function rejects. This is deliberately independent of `irPath`/`resumeFromDisk`-style setup-state
|
|
112
|
+
* reconstruction elsewhere in the harness (`server/compose.ts`): this option resumes the actual SDK
|
|
113
|
+
* conversation, not a disk-state prompt rebuild.
|
|
114
|
+
*/
|
|
115
|
+
export declare function runDispatchedDefault(options: DispatchedOptions): Promise<DispatchedResult>;
|
|
116
|
+
/**
|
|
117
|
+
* A `spawnChat` turn that failed (non-zero exit, timeout, or spawn error) but still surfaced a
|
|
118
|
+
* resumable SDK session id along the way (the dispatcher's `{t:"session",id}` NDJSON line — see
|
|
119
|
+
* `chat-event-mapper.ts` and warble's own `DispatchSessionError`, which is what makes this possible
|
|
120
|
+
* on a failed turn like `error_max_turns` in the first place, not just a successful one).
|
|
121
|
+
* `sessionId` is `null` when the dispatcher emitted the session line but the SDK itself never
|
|
122
|
+
* produced an id; it is simply absent as a property when `error` is a plain `Error` (e.g. a spawn
|
|
123
|
+
* failure before any NDJSON was ever read) — callers should treat "not a `DispatchedSessionError`" the
|
|
124
|
+
* same as "no session id available", never as a hard failure of their own.
|
|
125
|
+
*/
|
|
126
|
+
export declare class DispatchedSessionError extends Error {
|
|
127
|
+
readonly sessionId: string | null;
|
|
128
|
+
constructor(message: string, sessionId: string | null);
|
|
129
|
+
}
|
|
130
|
+
export interface SpawnChatResult {
|
|
131
|
+
readonly finalText: string;
|
|
132
|
+
/**
|
|
133
|
+
* The SDK session id this turn ran under (the dispatcher's `{t:"session",id}` line), or
|
|
134
|
+
* `undefined` if no such line was ever read (e.g. an older dispatcher build, or the process
|
|
135
|
+
* died before emitting one).
|
|
136
|
+
*/
|
|
137
|
+
readonly sessionId: string | null | undefined;
|
|
138
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { enforceCompliance } from "../compliance/index.js";
|
|
7
|
+
import { compileProfile } from "../compile/pipeline.js";
|
|
8
|
+
import { resolveWarbleBinary } from "../compile/resolve-binary.js";
|
|
9
|
+
import { createAgentEventEmitter } from "../events/index.js";
|
|
10
|
+
import { createChatEventMapperState, mapChatEventToAgentEvent, parseWarbleChatEventLine, } from "./chat-event-mapper.js";
|
|
11
|
+
import { resolveAgentSdkCli } from "./agent-sdk-cli.js";
|
|
12
|
+
const MODE_B_AGENT_ID = "answer_query";
|
|
13
|
+
/**
|
|
14
|
+
* Builds the argv (and stdin input) for a `warble-agent-sdk chat` invocation
|
|
15
|
+
* without spawning anything — the validated shape from the earlier spike:
|
|
16
|
+
* `echo "<question>" | <agent-sdk-cli> chat <ir.json> --project <userProject>
|
|
17
|
+
* --component <agentId, default "answer_query"> --out <dir> --warble-bin
|
|
18
|
+
* <warble> [--models-config <path>] --stream-json`. `--stream-json` is always
|
|
19
|
+
* passed so `spawnChat` can incrementally parse per-step/per-tool NDJSON
|
|
20
|
+
* events off stdout instead of only getting the plain final-answer line. The
|
|
21
|
+
* question is always routed through `input` (stdin), never interpolated into
|
|
22
|
+
* `args`, so it can never break argv parsing — there is no shell involved
|
|
23
|
+
* either (`spawn`, not `exec`).
|
|
24
|
+
*
|
|
25
|
+
* `warble-agent-sdk chat` is a line-per-turn REPL over stdin — one
|
|
26
|
+
* line is one turn. A question containing a newline (e.g. the BFF's
|
|
27
|
+
* multi-turn Ask composing the last ~5 turns of prior context ahead of the
|
|
28
|
+
* follow-up) would silently become *multiple* turns if sent as-is — only the
|
|
29
|
+
* last turn's answer would ever be returned. The dispatcher can't be changed
|
|
30
|
+
* (it's a separate, soon-public repo), so instead of rejecting a multi-line
|
|
31
|
+
* question, `encodeQuestionForStdin` collapses its interior newlines into a
|
|
32
|
+
* literal `\n` escape so the whole thing still crosses stdin as exactly one
|
|
33
|
+
* line/turn; an LLM reads a literal `\n` in prose as a line break just fine.
|
|
34
|
+
* A single-line question has nothing to collapse and passes through
|
|
35
|
+
* unchanged. An empty/whitespace-only question is never a meaningful turn at
|
|
36
|
+
* all and is still rejected here, before anything is spawned.
|
|
37
|
+
*/
|
|
38
|
+
export function buildAgentSdkChatArgs(cli, options) {
|
|
39
|
+
if (options.question.trim().length === 0) {
|
|
40
|
+
throw new Error("dispatched question must not be empty or whitespace-only");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
command: cli.command,
|
|
44
|
+
args: [
|
|
45
|
+
...cli.prefixArgs,
|
|
46
|
+
"chat",
|
|
47
|
+
options.irPath,
|
|
48
|
+
"--project",
|
|
49
|
+
options.userProject,
|
|
50
|
+
"--component",
|
|
51
|
+
options.agentId ?? MODE_B_AGENT_ID,
|
|
52
|
+
"--out",
|
|
53
|
+
options.outDir,
|
|
54
|
+
"--warble-bin",
|
|
55
|
+
options.warbleBin,
|
|
56
|
+
...(options.modelsConfig !== undefined ? ["--models-config", options.modelsConfig] : []),
|
|
57
|
+
...(options.maxTurns !== undefined ? ["--max-turns", String(options.maxTurns)] : []),
|
|
58
|
+
...(options.resume !== undefined ? ["--resume", options.resume] : []),
|
|
59
|
+
"--stream-json",
|
|
60
|
+
],
|
|
61
|
+
input: `${encodeQuestionForStdin(options.question)}\n`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Collapses every newline variant inside `question` into a literal
|
|
66
|
+
* two-character `\n` escape so the result always fits on a single stdin
|
|
67
|
+
* line/turn, no matter how many lines the caller composed into it. CRLF and
|
|
68
|
+
* lone CR are normalized to LF first so every newline style escapes
|
|
69
|
+
* uniformly. A question with no interior newline has nothing to normalize or
|
|
70
|
+
* escape and is returned byte-for-byte unchanged.
|
|
71
|
+
*/
|
|
72
|
+
function encodeQuestionForStdin(question) {
|
|
73
|
+
return question.replace(/\r\n|\r/g, "\n").replace(/\n/g, "\\n");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Dispatched ("subscription"): compile the profile to IR only (`mode: "native"` —
|
|
77
|
+
* no vercel bundle; the claude-agent-sdk dispatcher reads IR directly and
|
|
78
|
+
* drives its own Claude Agent SDK `query()` loop at runtime, so no adapter/
|
|
79
|
+
* tier binding is assembled here), then shell the warble-agent-sdk CLI's
|
|
80
|
+
* `chat` subcommand, feeding the question over stdin and returning its
|
|
81
|
+
* finishing text.
|
|
82
|
+
*
|
|
83
|
+
* The "hybrid" passthrough: `options.modelsConfig`, when set, becomes
|
|
84
|
+
* `--models-config <path>` on the `chat` invocation (see
|
|
85
|
+
* `buildAgentSdkChatArgs`), letting the subscription main loop route one
|
|
86
|
+
* step (e.g. `resolve_intent`'s `cheap` tier) onto a non-Anthropic model via
|
|
87
|
+
* warble's own `ModelConfig.fromYaml` binding
|
|
88
|
+
* (`dispatcher/claude-agent-sdk/src/models.ts`). This harness only forwards
|
|
89
|
+
* the path — it never parses the YAML or the resulting per-tier routing
|
|
90
|
+
* itself, and inherits warble's own hybrid-mode holes as of this writing
|
|
91
|
+
* (tracked upstream, not fixed here): the render stage wall-hits on any
|
|
92
|
+
* non-`render: none` component under a non-Anthropic tier, the
|
|
93
|
+
* `openai_compat` local client has no streaming/retry, and net cost savings
|
|
94
|
+
* are unproven. Weak local models are also known to call tools unreliably —
|
|
95
|
+
* a separate, model-quality issue rather than a wiring bug. See the opt-in
|
|
96
|
+
* live hybrid tests (`test/hybrid-live.test.ts`) for how to exercise this
|
|
97
|
+
* against a real endpoint.
|
|
98
|
+
*
|
|
99
|
+
* As a belt-and-suspenders measure, `route()` already runs the compliance gate
|
|
100
|
+
* (`enforceCompliance`) before ever calling this function, but
|
|
101
|
+
* `runDispatchedDefault` is also exported from the package root — an embedder
|
|
102
|
+
* that imports it directly bypasses `route()`, and with it that gate,
|
|
103
|
+
* entirely. Re-running `enforceCompliance` here makes the executor safe
|
|
104
|
+
* regardless of entry point: `route()`'s call and this one are the same
|
|
105
|
+
* pure, idempotent decision (subscription + hosted always throws,
|
|
106
|
+
* subscription + personal always allows), so double-gating through
|
|
107
|
+
* `route()` costs nothing and closes the direct-import hole.
|
|
108
|
+
*
|
|
109
|
+
* Session resume (Plan A): `options.resumeSessionId`, when set, is forwarded as `--resume <id>` so
|
|
110
|
+
* the dispatcher resumes the SAME agent-sdk conversation for this turn instead of starting a fresh
|
|
111
|
+
* one — the agent keeps whatever it already read/listed/fetched, rather than re-orienting from
|
|
112
|
+
* scratch on every turn. The turn's own session id (for resuming a LATER turn) comes back on
|
|
113
|
+
* `DispatchedResult.sessionId` on success, and — since a resumable session id from a failed turn is just
|
|
114
|
+
* as useful as one from a successful turn (an `error_max_turns` failure is exactly the case this
|
|
115
|
+
* exists for) — is still recoverable from a thrown `DispatchedSessionError`'s `sessionId` field when this
|
|
116
|
+
* function rejects. This is deliberately independent of `irPath`/`resumeFromDisk`-style setup-state
|
|
117
|
+
* reconstruction elsewhere in the harness (`server/compose.ts`): this option resumes the actual SDK
|
|
118
|
+
* conversation, not a disk-state prompt rebuild.
|
|
119
|
+
*/
|
|
120
|
+
export async function runDispatchedDefault(options) {
|
|
121
|
+
// Live-event layer: emits run.start/answer/run.finish/error on
|
|
122
|
+
// options.onEvent directly, plus — now that `chat --stream-json` streams
|
|
123
|
+
// per-step/per-tool NDJSON over stdout — step.*/tool.* events mapped in
|
|
124
|
+
// from that stream via `chat-event-mapper.ts` as `spawnChat` reads each
|
|
125
|
+
// line (see there for the mapping). Mapped events are best-effort: a
|
|
126
|
+
// malformed/unrecognized NDJSON line is dropped, not fatal to the turn.
|
|
127
|
+
enforceCompliance(options.authChoice, { deployment: options.deployment ?? "personal" });
|
|
128
|
+
// `SubscriptionAuthChoice.provider` is "claude" | "codex", but the
|
|
129
|
+
// only Ask back-end this function drives is the Claude Agent SDK
|
|
130
|
+
// dispatcher (`warble-agent-sdk chat`). Codex is selected by route() and
|
|
131
|
+
// runs through the separate codex:local adapter.
|
|
132
|
+
// Without this check, a `provider: "codex"` choice would silently run the
|
|
133
|
+
// Claude dispatcher anyway (misrouting the request to the wrong model
|
|
134
|
+
// family) instead of failing loudly. Fail before compiling or spawning
|
|
135
|
+
// anything.
|
|
136
|
+
if (options.authChoice.provider !== "claude") {
|
|
137
|
+
throw new Error(`runDispatchedDefault only supports the Claude subscription provider. ` +
|
|
138
|
+
`Codex Ask must be dispatched through the codex:local route and will not fall back.`);
|
|
139
|
+
}
|
|
140
|
+
if (options.signal?.aborted)
|
|
141
|
+
throw new Error("warble-agent-sdk chat was cancelled before start");
|
|
142
|
+
// Which warble component `chat` dispatches; defaults to
|
|
143
|
+
// MODE_B_AGENT_ID ("answer_query") — the original default from before this
|
|
144
|
+
// option existed. See `DispatchedOptions.agentId`.
|
|
145
|
+
const agentId = options.agentId ?? MODE_B_AGENT_ID;
|
|
146
|
+
const emitter = createAgentEventEmitter(options.onEvent);
|
|
147
|
+
emitter.emit({ kind: "run.start", mode: "B", agentId });
|
|
148
|
+
try {
|
|
149
|
+
// Setup-flow bypass (`DispatchedOptions.irPath`): skip compileProfile entirely
|
|
150
|
+
// and dispatch the caller-supplied IR directly. Used by the setup
|
|
151
|
+
// wizard's `DispatchedSetupRunner`, which has no bound wren project to
|
|
152
|
+
// compile a profile against — it dispatches the fixed, warble-committed
|
|
153
|
+
// genbi-setup IR instead.
|
|
154
|
+
const irPath = options.irPath ?? (await compileProfile({
|
|
155
|
+
profileSource: options.profileSource,
|
|
156
|
+
userProject: options.userProject,
|
|
157
|
+
mode: "native",
|
|
158
|
+
...(options.warbleBin !== undefined ? { warbleBin: options.warbleBin } : {}),
|
|
159
|
+
...(options.workDir !== undefined ? { workDir: options.workDir } : {}),
|
|
160
|
+
})).irPath;
|
|
161
|
+
if (options.signal?.aborted)
|
|
162
|
+
throw new Error("warble-agent-sdk chat was cancelled during preparation");
|
|
163
|
+
const warbleBin = options.warbleBin ?? (await resolveWarbleBinary());
|
|
164
|
+
if (options.signal?.aborted)
|
|
165
|
+
throw new Error("warble-agent-sdk chat was cancelled during preparation");
|
|
166
|
+
const cli = await resolveAgentSdkCli(options.agentSdkBin);
|
|
167
|
+
if (options.signal?.aborted)
|
|
168
|
+
throw new Error("warble-agent-sdk chat was cancelled during preparation");
|
|
169
|
+
const outDir = options.outDir ?? (await mkdtemp(path.join(os.tmpdir(), "wren-harness-agent-sdk-")));
|
|
170
|
+
if (options.signal?.aborted)
|
|
171
|
+
throw new Error("warble-agent-sdk chat was cancelled during preparation");
|
|
172
|
+
const command = buildAgentSdkChatArgs(cli, {
|
|
173
|
+
irPath,
|
|
174
|
+
userProject: options.userProject,
|
|
175
|
+
question: options.question,
|
|
176
|
+
outDir,
|
|
177
|
+
warbleBin,
|
|
178
|
+
agentId,
|
|
179
|
+
...(options.modelsConfig !== undefined ? { modelsConfig: options.modelsConfig } : {}),
|
|
180
|
+
...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}),
|
|
181
|
+
...(options.resumeSessionId !== undefined ? { resume: options.resumeSessionId } : {}),
|
|
182
|
+
});
|
|
183
|
+
const { finalText, sessionId } = await spawnChat(command, (event) => emitter.emit(event), options.chatTimeoutMs ?? CHAT_TIMEOUT_MS, options.signal);
|
|
184
|
+
// `spawnChat` resolves with whatever trimmed final-answer text
|
|
185
|
+
// the process produced, even if that's empty — e.g. the CLI exits 0 but
|
|
186
|
+
// never printed a terminal answer line (a dispatcher bug, an unexpected
|
|
187
|
+
// output-format change, or a turn that produced no text). Treat that as
|
|
188
|
+
// a failure rather than a silent empty "answer".
|
|
189
|
+
if (finalText.length === 0) {
|
|
190
|
+
throw new Error("warble-agent-sdk chat exited successfully but produced no stdout — expected the turn's " +
|
|
191
|
+
"final answer text; got empty output");
|
|
192
|
+
}
|
|
193
|
+
emitter.emit({ kind: "answer", text: finalText });
|
|
194
|
+
emitter.emit({ kind: "run.finish", status: "answer" });
|
|
195
|
+
return { finalText, ...(sessionId !== undefined ? { sessionId } : {}) };
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
emitter.emit({ kind: "error", message: describeDispatchedError(error) });
|
|
199
|
+
emitter.emit({ kind: "run.finish", status: "error" });
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function describeDispatchedError(error) {
|
|
204
|
+
if (error instanceof Error)
|
|
205
|
+
return error.message;
|
|
206
|
+
return typeof error === "string" ? error : JSON.stringify(error);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* A `spawnChat` turn that failed (non-zero exit, timeout, or spawn error) but still surfaced a
|
|
210
|
+
* resumable SDK session id along the way (the dispatcher's `{t:"session",id}` NDJSON line — see
|
|
211
|
+
* `chat-event-mapper.ts` and warble's own `DispatchSessionError`, which is what makes this possible
|
|
212
|
+
* on a failed turn like `error_max_turns` in the first place, not just a successful one).
|
|
213
|
+
* `sessionId` is `null` when the dispatcher emitted the session line but the SDK itself never
|
|
214
|
+
* produced an id; it is simply absent as a property when `error` is a plain `Error` (e.g. a spawn
|
|
215
|
+
* failure before any NDJSON was ever read) — callers should treat "not a `DispatchedSessionError`" the
|
|
216
|
+
* same as "no session id available", never as a hard failure of their own.
|
|
217
|
+
*/
|
|
218
|
+
export class DispatchedSessionError extends Error {
|
|
219
|
+
sessionId;
|
|
220
|
+
constructor(message, sessionId) {
|
|
221
|
+
super(message);
|
|
222
|
+
this.sessionId = sessionId;
|
|
223
|
+
this.name = "DispatchedSessionError";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// guardrail-enforcement fix: a hung `warble-agent-sdk chat` invocation
|
|
227
|
+
// previously blocked forever. This is a hang-guard CEILING (turns finish when
|
|
228
|
+
// done; it only fires on a genuine hang), so it's set generously: a cold Ask
|
|
229
|
+
// right after setup — a fresh, un-warmed project where answer_query explores
|
|
230
|
+
// before it queries — was observed to legitimately need >5 min, so 10 minutes
|
|
231
|
+
// is the default. Override via `DispatchedOptions.chatTimeoutMs`
|
|
232
|
+
// (`--chat-timeout-ms` / `WREN_HARNESS_CHAT_TIMEOUT_MS`).
|
|
233
|
+
const CHAT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
234
|
+
/**
|
|
235
|
+
* Runs one `warble-agent-sdk chat --stream-json` turn, reading stdout INCREMENTALLY
|
|
236
|
+
* (line-by-line, via `readline`) rather than buffering the whole process output like the
|
|
237
|
+
* previous `execFile`-based version did — that's what lets `onEvent` fire live, per line, as the
|
|
238
|
+
* turn runs, instead of only after the process exits. Each line is parsed with
|
|
239
|
+
* `parseWarbleChatEventLine` and either captured as the turn's final answer (`t: "answer"`), its
|
|
240
|
+
* resumable session id (`t: "session"` — tracked so it can be surfaced on BOTH a successful
|
|
241
|
+
* resolve and a failed reject, since a resumable session id is just as useful after an
|
|
242
|
+
* `error_max_turns` failure as after a success; see `DispatchedSessionError`), or mapped to this
|
|
243
|
+
* harness's `AgentEvent` vocabulary via `mapChatEventToAgentEvent` and forwarded to `onEvent`. A
|
|
244
|
+
* line that fails to parse is silently dropped (never fatal to the turn — see that module's doc
|
|
245
|
+
* comment). `onEvent` is always called unconditionally here; it is `runDispatchedDefault`'s
|
|
246
|
+
* `emitter.emit`, which is already a documented no-op when the caller supplied no sink.
|
|
247
|
+
*/
|
|
248
|
+
function spawnChat(command, onEvent, timeoutMs = CHAT_TIMEOUT_MS, signal) {
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
if (signal?.aborted) {
|
|
251
|
+
reject(new DispatchedSessionError("warble-agent-sdk chat was cancelled before spawn", null));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const child = spawn(command.command, [...command.args], {
|
|
255
|
+
detached: process.platform !== "win32",
|
|
256
|
+
});
|
|
257
|
+
let settled = false;
|
|
258
|
+
let stopReason;
|
|
259
|
+
let killTimer;
|
|
260
|
+
let answerText;
|
|
261
|
+
let sessionId;
|
|
262
|
+
let stderrText = "";
|
|
263
|
+
const mapperState = createChatEventMapperState();
|
|
264
|
+
const terminate = (childSignal) => {
|
|
265
|
+
if (child.pid !== undefined && process.platform !== "win32") {
|
|
266
|
+
try {
|
|
267
|
+
process.kill(-child.pid, childSignal);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Fall through to the direct child when it did not form a group.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
child.kill(childSignal);
|
|
275
|
+
};
|
|
276
|
+
const stop = (reason) => {
|
|
277
|
+
if (settled || stopReason !== undefined)
|
|
278
|
+
return;
|
|
279
|
+
stopReason = reason;
|
|
280
|
+
terminate("SIGTERM");
|
|
281
|
+
killTimer = setTimeout(() => terminate("SIGKILL"), 1_000);
|
|
282
|
+
};
|
|
283
|
+
const timer = setTimeout(() => stop("timeout"), timeoutMs);
|
|
284
|
+
const cancel = () => stop("cancelled");
|
|
285
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
286
|
+
const settle = (fn) => {
|
|
287
|
+
if (settled)
|
|
288
|
+
return;
|
|
289
|
+
settled = true;
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
if (killTimer)
|
|
292
|
+
clearTimeout(killTimer);
|
|
293
|
+
signal?.removeEventListener("abort", cancel);
|
|
294
|
+
fn();
|
|
295
|
+
};
|
|
296
|
+
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
297
|
+
rl.on("line", (line) => {
|
|
298
|
+
const raw = parseWarbleChatEventLine(line);
|
|
299
|
+
if (raw === undefined)
|
|
300
|
+
return;
|
|
301
|
+
if (raw.t === "answer") {
|
|
302
|
+
answerText = raw.text;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (raw.t === "session") {
|
|
306
|
+
sessionId = raw.id;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const mapped = mapChatEventToAgentEvent(raw, mapperState);
|
|
310
|
+
if (mapped !== undefined)
|
|
311
|
+
onEvent(mapped);
|
|
312
|
+
});
|
|
313
|
+
child.stderr.on("data", (chunk) => {
|
|
314
|
+
stderrText += chunk.toString("utf8");
|
|
315
|
+
});
|
|
316
|
+
child.on("error", (error) => {
|
|
317
|
+
settle(() => reject(new DispatchedSessionError(`warble-agent-sdk chat failed to start: ${error.message}`, sessionId ?? null)));
|
|
318
|
+
});
|
|
319
|
+
child.on("close", (code, signal) => {
|
|
320
|
+
settle(() => {
|
|
321
|
+
if (stopReason === "timeout") {
|
|
322
|
+
reject(new DispatchedSessionError(`warble-agent-sdk chat failed (timed out after ${timeoutMs}ms): ${stderrText}`, sessionId ?? null));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (stopReason === "cancelled") {
|
|
326
|
+
reject(new DispatchedSessionError("warble-agent-sdk chat was cancelled", sessionId ?? null));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (code !== 0) {
|
|
330
|
+
const exitCode = code ?? signal ?? "unknown";
|
|
331
|
+
reject(new DispatchedSessionError(`warble-agent-sdk chat failed (exit ${exitCode}): ${stderrText || "(no stderr output)"}`, sessionId ?? null));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
resolve({ finalText: (answerText ?? "").trim(), sessionId });
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
child.stdin?.end(command.input);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RunAgentResult } from "../session/index.js";
|
|
2
|
+
import type { InProcessOptions } from "./types.js";
|
|
3
|
+
/** Projects a bundle-wide validated map onto the component selected for one turn. */
|
|
4
|
+
export declare function filterTierBindingForAgent(agent: {
|
|
5
|
+
readonly steps: readonly {
|
|
6
|
+
readonly tier: string;
|
|
7
|
+
}[];
|
|
8
|
+
}, tierBinding: Readonly<Record<string, import("../providers/index.js").AdapterSpec>>): Readonly<Record<string, import("../providers/index.js").AdapterSpec>>;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the `rootDir` the native `write_artifact` tool's
|
|
11
|
+
* `createLocalExecutionEnv` scope is rooted at. Precedence: an explicit
|
|
12
|
+
* `outDir` (per-call override, e.g. `InProcessOptions.outDir` /
|
|
13
|
+
* `WREN_HARNESS_OUT`) > the `WREN_HARNESS_ARTIFACTS_DIR` env var
|
|
14
|
+
* (operator-wide override) > a fixed `os.tmpdir()` subdirectory.
|
|
15
|
+
* Deliberately NEVER `process.cwd()` — that was the bug this fixes:
|
|
16
|
+
* `runInProcessDefault` used to call `createLocalExecutionEnv()` with no
|
|
17
|
+
* `rootDir` at all, which defaults to `process.cwd()` (`harness/exec/local.ts`)
|
|
18
|
+
* — for a live BFF process that's wherever this package's server process
|
|
19
|
+
* happens to be launched from, so every `write_artifact` call (from
|
|
20
|
+
* `explain_change`/`generate_dashboard`, both scoped-write-gated) dropped its
|
|
21
|
+
* output straight into whatever directory started the process.
|
|
22
|
+
*
|
|
23
|
+
* This is entirely independent of `projectDir` (`options.userProject`),
|
|
24
|
+
* which is the ONLY thing that governs the `query` tool's exec cwd (see
|
|
25
|
+
* `createWrenQueryTool` in `harness/tools/native.ts` — it takes `projectDir` as
|
|
26
|
+
* its `cwd` directly, never touching `env`'s own `rootDir`). Changing this
|
|
27
|
+
* default never changes where `query` runs.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveArtifactsDir(outDir: string | undefined): string;
|
|
30
|
+
/**
|
|
31
|
+
* In-process ("api-key" | "local" | "gateway"): compile the profile to a vercel
|
|
32
|
+
* bundle (unless `options.bundle` overrides it, for tests), resolve which
|
|
33
|
+
* compiled agent to run (`options.agentId`, defaulting to
|
|
34
|
+
* `ANSWER_QUERY_AGENT_ID` — intent routing picks this upstream in
|
|
35
|
+
* `server/turn.ts`, not here), assemble a `RunAgentContext` bound either to a
|
|
36
|
+
* single adapter covering every tier that agent uses (the default), or —
|
|
37
|
+
* when `options.tierBinding` is set ("hybrid" mode) — to that non-uniform
|
|
38
|
+
* per-tier map, validated via `buildHybridTierBinding`; wire its `query` tool
|
|
39
|
+
* to the real `wren` CLI against `options.userProject` (unless
|
|
40
|
+
* `options.mcpServers` overrides it, for tests), and run it in-process via
|
|
41
|
+
* `runAgent`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runInProcessDefault(options: InProcessOptions): Promise<RunAgentResult>;
|