@vidge/dsh-agent-hub 0.1.0-rc2 → 0.1.0-rc4
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/lib/index.js
CHANGED
|
@@ -61,10 +61,10 @@ import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
|
61
61
|
import { SessionPreparation } from "@deepseek-ai/dsh-session";
|
|
62
62
|
|
|
63
63
|
// src/engine-claude/agent.ts
|
|
64
|
-
import { Inbox, agentEvents } from "@deepseek-ai/dsh-agent";
|
|
64
|
+
import { Inbox, agentEvents, assembleContextFor } from "@deepseek-ai/dsh-agent";
|
|
65
65
|
import { LlmError, createAssistantMessage, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
|
|
66
66
|
import { createScope } from "@deepseek-ai/dsh-scope";
|
|
67
|
-
import { canonicalHeader } from "@deepseek-ai/dsh-session";
|
|
67
|
+
import { canonicalHeader, headerEquals } from "@deepseek-ai/dsh-session";
|
|
68
68
|
|
|
69
69
|
// src/engine-claude/mapping.ts
|
|
70
70
|
import {
|
|
@@ -486,15 +486,21 @@ var SHARED_ENV_KEYS = [
|
|
|
486
486
|
];
|
|
487
487
|
function inheritedLlmCredentials(backend) {
|
|
488
488
|
const creds = {};
|
|
489
|
-
const
|
|
489
|
+
const chosen = backend === "auto" ? BACKEND_ENV_GROUPS.find((group) => process.env[group.selector] !== void 0) : BACKEND_ENV_GROUPS.find((group) => group.id === backend);
|
|
490
|
+
for (const key of chosen?.keys ?? []) {
|
|
490
491
|
const value = process.env[key];
|
|
491
492
|
if (value !== void 0) creds[key] = value;
|
|
492
|
-
}
|
|
493
|
-
const chosen = backend === "auto" ? BACKEND_ENV_GROUPS.find((group) => process.env[group.selector] !== void 0) : BACKEND_ENV_GROUPS.find((group) => group.id === backend);
|
|
494
|
-
for (const key of chosen?.keys ?? []) take(key);
|
|
495
|
-
for (const key of SHARED_ENV_KEYS) take(key);
|
|
493
|
+
}
|
|
496
494
|
return creds;
|
|
497
495
|
}
|
|
496
|
+
function inheritedSharedEnv() {
|
|
497
|
+
const shared = {};
|
|
498
|
+
for (const key of SHARED_ENV_KEYS) {
|
|
499
|
+
const value = process.env[key];
|
|
500
|
+
if (value !== void 0) shared[key] = value;
|
|
501
|
+
}
|
|
502
|
+
return shared;
|
|
503
|
+
}
|
|
498
504
|
function claudeChildEnv(spec) {
|
|
499
505
|
const derived = spec.providerEnv;
|
|
500
506
|
const selected = derived ?? inheritedLlmCredentials(spec.backend ?? "auto");
|
|
@@ -506,6 +512,7 @@ function claudeChildEnv(spec) {
|
|
|
506
512
|
}
|
|
507
513
|
return {
|
|
508
514
|
...env,
|
|
515
|
+
...inheritedSharedEnv(),
|
|
509
516
|
...selected,
|
|
510
517
|
...spec.model === void 0 ? {} : { ANTHROPIC_MODEL: spec.model },
|
|
511
518
|
...spec.env
|
|
@@ -606,6 +613,7 @@ function readProviderProfile(ctx, provider) {
|
|
|
606
613
|
}
|
|
607
614
|
function mapRouteToEnv(provider, profile, key) {
|
|
608
615
|
const headers = profile.headers ?? {};
|
|
616
|
+
const extra = profile.childEnv ?? {};
|
|
609
617
|
if (BEDROCK_PROVIDER_IDS.has(provider)) {
|
|
610
618
|
return {
|
|
611
619
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
@@ -615,13 +623,15 @@ function mapRouteToEnv(provider, profile, key) {
|
|
|
615
623
|
// explicit and the gateway ignores it; without one the client refuses to
|
|
616
624
|
// construct. A route pointing at a corporate gateway has no real region,
|
|
617
625
|
// so this is a placeholder the endpoint override makes irrelevant.
|
|
618
|
-
..."AWS_REGION" in headers ? {} : { AWS_REGION: "us-east-1" }
|
|
626
|
+
..."AWS_REGION" in headers ? {} : { AWS_REGION: "us-east-1" },
|
|
627
|
+
...extra
|
|
619
628
|
};
|
|
620
629
|
}
|
|
621
630
|
if (ANTHROPIC_PROVIDER_IDS.has(provider) || profile.api === "anthropic-messages") {
|
|
622
631
|
return {
|
|
623
632
|
ANTHROPIC_AUTH_TOKEN: key,
|
|
624
|
-
...profile.baseURL === void 0 ? {} : { ANTHROPIC_BASE_URL: profile.baseURL }
|
|
633
|
+
...profile.baseURL === void 0 ? {} : { ANTHROPIC_BASE_URL: profile.baseURL },
|
|
634
|
+
...extra
|
|
625
635
|
};
|
|
626
636
|
}
|
|
627
637
|
return void 0;
|
|
@@ -644,9 +654,10 @@ async function deriveProviderEnv(ctx, provider) {
|
|
|
644
654
|
if (key === void 0 || key === "") return void 0;
|
|
645
655
|
const env = mapRouteToEnv(provider, profile, key);
|
|
646
656
|
if (env === void 0) return void 0;
|
|
657
|
+
const extra = Object.keys(profile.childEnv ?? {});
|
|
647
658
|
return {
|
|
648
659
|
env,
|
|
649
|
-
diagnostic: `claude-code: routing to dsh provider "${provider}"${profile.baseURL === void 0 ? "" : ` at ${profile.baseURL}`} (credential ${ref})`
|
|
660
|
+
diagnostic: `claude-code: routing to dsh provider "${provider}"${profile.baseURL === void 0 ? "" : ` at ${profile.baseURL}`} (credential ${ref})${extra.length === 0 ? "" : `; route env ${extra.join(", ")}`}`
|
|
650
661
|
};
|
|
651
662
|
}
|
|
652
663
|
|
|
@@ -706,6 +717,7 @@ async function loadClaudeQuery() {
|
|
|
706
717
|
return claudeQueryPromise;
|
|
707
718
|
}
|
|
708
719
|
var NATIVE_MODEL_LABEL = "claude-code-native";
|
|
720
|
+
var NATIVE_PROVIDER_LABEL = PROVIDER;
|
|
709
721
|
function failureCode(subtype) {
|
|
710
722
|
switch (subtype) {
|
|
711
723
|
case "error_during_execution":
|
|
@@ -1036,19 +1048,35 @@ var ClaudeCodeAgent = class {
|
|
|
1036
1048
|
return true;
|
|
1037
1049
|
}
|
|
1038
1050
|
/**
|
|
1039
|
-
* Resolve the model one query runs on, session
|
|
1051
|
+
* Resolve the model one query runs on, per-session selection first.
|
|
1052
|
+
*
|
|
1053
|
+
* The layers, in precedence order:
|
|
1054
|
+
*
|
|
1055
|
+
* 1. **The `agent/request` waterfall.** This is the seam the dsh model
|
|
1056
|
+
* picker actually drives. api-proxy installs `installModelSelection` on
|
|
1057
|
+
* every agent's own context, which listens on `system-prompt/assemble`
|
|
1058
|
+
* to *snapshot* the session's selection and on `agent/request` to *apply*
|
|
1059
|
+
* the snapshot — two stages, so a mid-turn switch lands on a later step
|
|
1060
|
+
* rather than splitting the prompt from the route. Both must be
|
|
1061
|
+
* dispatched, and in that order: the request listener reads
|
|
1062
|
+
* `selection.assembled`, which only the assemble listener writes, so
|
|
1063
|
+
* dispatching the request waterfall alone yields nothing.
|
|
1064
|
+
* 2. `AgentOptions.model` — the create-time seed.
|
|
1065
|
+
* 3. `agentDefaultModel` — the global default.
|
|
1066
|
+
* 4. The deployment's pinned `config.model`.
|
|
1067
|
+
* 5. Nothing, leaving the CLI on its own model.
|
|
1040
1068
|
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
* optional — a minimal profile may not mount it — so it is resolved through
|
|
1045
|
-
* `ctx.get` rather than `inject`, and a faulting provider degrades to the
|
|
1046
|
-
* next layer instead of failing the turn.
|
|
1069
|
+
* Every layer is optional and every failure degrades to the next one: a
|
|
1070
|
+
* minimal profile mounts neither service, and a listener that throws must
|
|
1071
|
+
* cost this session its turn no more than a missing service does.
|
|
1047
1072
|
*
|
|
1073
|
+
* @param signal - the step's cancellation signal, forwarded to prompt assembly.
|
|
1048
1074
|
* @returns the chosen id (undefined leaves the CLI on its own default),
|
|
1049
1075
|
* its provider route, and the layer that chose it.
|
|
1050
1076
|
*/
|
|
1051
|
-
resolveModel() {
|
|
1077
|
+
async resolveModel(signal) {
|
|
1078
|
+
const selected = await this.selectionFromWaterfall(signal);
|
|
1079
|
+
if (selected !== void 0) return selected;
|
|
1052
1080
|
if (this.options.model !== void 0) {
|
|
1053
1081
|
return { model: this.options.model, provider: this.options.provider, source: "session" };
|
|
1054
1082
|
}
|
|
@@ -1068,22 +1096,88 @@ var ClaudeCodeAgent = class {
|
|
|
1068
1096
|
}
|
|
1069
1097
|
return { model: void 0, provider: void 0, source: "native" };
|
|
1070
1098
|
}
|
|
1071
|
-
/**
|
|
1072
|
-
|
|
1073
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* Ask the host what this session is routed to, through the two waterfalls
|
|
1101
|
+
* that carry a per-session selection.
|
|
1102
|
+
*
|
|
1103
|
+
* The seed handed to `agent/request` is the same one the in-process loop
|
|
1104
|
+
* seeds with — the agent's own options — so a host that installs no listener
|
|
1105
|
+
* gets its own answer back and this returns undefined, leaving the layers
|
|
1106
|
+
* below untouched. A listener that replaces it wins.
|
|
1107
|
+
*
|
|
1108
|
+
* The assemble pass is dispatched for its *side effect* on the selection
|
|
1109
|
+
* state; its returned prompt is discarded, because Claude Code builds its own
|
|
1110
|
+
* prompt and dsh's assembly never reaches the child. That makes this a real
|
|
1111
|
+
* (if small) cost per step: the host's prompt providers run and their output
|
|
1112
|
+
* is dropped. It is the price of reaching a selection whose only publisher is
|
|
1113
|
+
* that listener pair.
|
|
1114
|
+
*
|
|
1115
|
+
* @param signal - the step's cancellation signal.
|
|
1116
|
+
* @returns the selection when a listener supplied one, else undefined.
|
|
1117
|
+
*/
|
|
1118
|
+
async selectionFromWaterfall(signal) {
|
|
1119
|
+
const phase = this.phase;
|
|
1120
|
+
if (phase.kind !== "running") return void 0;
|
|
1121
|
+
const { turn, step } = phase;
|
|
1122
|
+
const seed = {
|
|
1123
|
+
provider: this.options.provider ?? "",
|
|
1124
|
+
model: this.options.model ?? ""
|
|
1125
|
+
};
|
|
1126
|
+
try {
|
|
1127
|
+
const systemPrompt = this.loopCtx.get("systemPrompt");
|
|
1128
|
+
if (systemPrompt !== void 0) {
|
|
1129
|
+
await systemPrompt.assemble(assembleContextFor(this, signal));
|
|
1130
|
+
}
|
|
1131
|
+
const proposed = await this.dispatch.waterfall(
|
|
1132
|
+
"agent/request",
|
|
1133
|
+
{ turn, step, signal },
|
|
1134
|
+
() => Promise.resolve(seed)
|
|
1135
|
+
);
|
|
1136
|
+
if (proposed.model === "" || proposed.provider === "") return void 0;
|
|
1137
|
+
if (proposed.provider === seed.provider && proposed.model === seed.model) return void 0;
|
|
1138
|
+
return { model: proposed.model, provider: proposed.provider, source: "selection" };
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
this.ctx.logger.warn("claude-code: per-session model selection unavailable: %s", error);
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1074
1143
|
}
|
|
1075
|
-
/**
|
|
1076
|
-
|
|
1077
|
-
|
|
1144
|
+
/**
|
|
1145
|
+
* Append the request header, and re-append it whenever the route changes.
|
|
1146
|
+
*
|
|
1147
|
+
* The provider written here is the **real** dsh route (`copilot-proxy`,
|
|
1148
|
+
* `amazon-bedrock`, …), not this engine's name. That is not cosmetic:
|
|
1149
|
+
* api-proxy re-reads this field on every read as "the model this session is
|
|
1150
|
+
* on", resolves it against `ctx.llm.listProviders()`, and locks the composer
|
|
1151
|
+
* when the name is not a registered provider — so writing the engine name
|
|
1152
|
+
* here made every session demand a fresh model pick after each turn. The
|
|
1153
|
+
* engine that ran the turn is recorded in the `*.loop-engine.json` sidecar,
|
|
1154
|
+
* which is where per-session engine provenance already lives.
|
|
1155
|
+
*
|
|
1156
|
+
* Re-logging on change mirrors the in-process loop: the header is the log's
|
|
1157
|
+
* record of what each request ran under, so a mid-session model switch has to
|
|
1158
|
+
* produce a new snapshot or the log misattributes every later turn.
|
|
1159
|
+
*
|
|
1160
|
+
* @param selected - the model resolved for the step about to run.
|
|
1161
|
+
*/
|
|
1162
|
+
noteRequestHeader(selected) {
|
|
1078
1163
|
const header = canonicalHeader({
|
|
1079
|
-
config: {
|
|
1164
|
+
config: {
|
|
1165
|
+
provider: selected.provider ?? NATIVE_PROVIDER_LABEL,
|
|
1166
|
+
model: selected.model ?? NATIVE_MODEL_LABEL
|
|
1167
|
+
}
|
|
1080
1168
|
});
|
|
1081
1169
|
const baseline = this.session.requestHeader();
|
|
1082
|
-
this.
|
|
1083
|
-
header,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1170
|
+
if (!this.requestHeaderLogged) {
|
|
1171
|
+
this.session.append("request/header", {
|
|
1172
|
+
header,
|
|
1173
|
+
reason: baseline === void 0 ? "initial" : "resume"
|
|
1174
|
+
});
|
|
1175
|
+
this.requestHeaderLogged = true;
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (baseline === void 0 || !headerEquals(baseline, header)) {
|
|
1179
|
+
this.session.append("request/header", { header, reason: "change" });
|
|
1180
|
+
}
|
|
1087
1181
|
}
|
|
1088
1182
|
/** Run one Claude Code query for the current step and map its transcript into the session log. */
|
|
1089
1183
|
async step() {
|
|
@@ -1099,7 +1193,9 @@ var ClaudeCodeAgent = class {
|
|
|
1099
1193
|
if (prompt.length === 0) {
|
|
1100
1194
|
throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
|
|
1101
1195
|
}
|
|
1102
|
-
this.
|
|
1196
|
+
const selected = await this.resolveModel(signal);
|
|
1197
|
+
signal.throwIfAborted();
|
|
1198
|
+
this.noteRequestHeader(selected);
|
|
1103
1199
|
signal.throwIfAborted();
|
|
1104
1200
|
const controller = new AbortController();
|
|
1105
1201
|
const cancel = () => {
|
|
@@ -1110,7 +1206,6 @@ var ClaudeCodeAgent = class {
|
|
|
1110
1206
|
signal.addEventListener("abort", cancel, { once: true });
|
|
1111
1207
|
const diagnostics = [];
|
|
1112
1208
|
try {
|
|
1113
|
-
const selected = this.resolveModel();
|
|
1114
1209
|
const derived = await deriveProviderEnv(this.loopCtx, selected.provider);
|
|
1115
1210
|
if (derived !== void 0) diagnostics.push(derived.diagnostic);
|
|
1116
1211
|
const options = claudeQueryOptions({
|
|
@@ -93,23 +93,72 @@ export declare class ClaudeCodeAgent implements Agent {
|
|
|
93
93
|
/** Open one turn before claiming its first proposed step. */
|
|
94
94
|
private turn;
|
|
95
95
|
/**
|
|
96
|
-
* Resolve the model one query runs on, session
|
|
96
|
+
* Resolve the model one query runs on, per-session selection first.
|
|
97
97
|
*
|
|
98
|
-
* The
|
|
99
|
-
* `agentDefaultModel` holds the global default; reading both is what makes
|
|
100
|
-
* the dsh model picker mean something for this engine. The service is
|
|
101
|
-
* optional — a minimal profile may not mount it — so it is resolved through
|
|
102
|
-
* `ctx.get` rather than `inject`, and a faulting provider degrades to the
|
|
103
|
-
* next layer instead of failing the turn.
|
|
98
|
+
* The layers, in precedence order:
|
|
104
99
|
*
|
|
100
|
+
* 1. **The `agent/request` waterfall.** This is the seam the dsh model
|
|
101
|
+
* picker actually drives. api-proxy installs `installModelSelection` on
|
|
102
|
+
* every agent's own context, which listens on `system-prompt/assemble`
|
|
103
|
+
* to *snapshot* the session's selection and on `agent/request` to *apply*
|
|
104
|
+
* the snapshot — two stages, so a mid-turn switch lands on a later step
|
|
105
|
+
* rather than splitting the prompt from the route. Both must be
|
|
106
|
+
* dispatched, and in that order: the request listener reads
|
|
107
|
+
* `selection.assembled`, which only the assemble listener writes, so
|
|
108
|
+
* dispatching the request waterfall alone yields nothing.
|
|
109
|
+
* 2. `AgentOptions.model` — the create-time seed.
|
|
110
|
+
* 3. `agentDefaultModel` — the global default.
|
|
111
|
+
* 4. The deployment's pinned `config.model`.
|
|
112
|
+
* 5. Nothing, leaving the CLI on its own model.
|
|
113
|
+
*
|
|
114
|
+
* Every layer is optional and every failure degrades to the next one: a
|
|
115
|
+
* minimal profile mounts neither service, and a listener that throws must
|
|
116
|
+
* cost this session its turn no more than a missing service does.
|
|
117
|
+
*
|
|
118
|
+
* @param signal - the step's cancellation signal, forwarded to prompt assembly.
|
|
105
119
|
* @returns the chosen id (undefined leaves the CLI on its own default),
|
|
106
120
|
* its provider route, and the layer that chose it.
|
|
107
121
|
*/
|
|
108
122
|
private resolveModel;
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Ask the host what this session is routed to, through the two waterfalls
|
|
125
|
+
* that carry a per-session selection.
|
|
126
|
+
*
|
|
127
|
+
* The seed handed to `agent/request` is the same one the in-process loop
|
|
128
|
+
* seeds with — the agent's own options — so a host that installs no listener
|
|
129
|
+
* gets its own answer back and this returns undefined, leaving the layers
|
|
130
|
+
* below untouched. A listener that replaces it wins.
|
|
131
|
+
*
|
|
132
|
+
* The assemble pass is dispatched for its *side effect* on the selection
|
|
133
|
+
* state; its returned prompt is discarded, because Claude Code builds its own
|
|
134
|
+
* prompt and dsh's assembly never reaches the child. That makes this a real
|
|
135
|
+
* (if small) cost per step: the host's prompt providers run and their output
|
|
136
|
+
* is dropped. It is the price of reaching a selection whose only publisher is
|
|
137
|
+
* that listener pair.
|
|
138
|
+
*
|
|
139
|
+
* @param signal - the step's cancellation signal.
|
|
140
|
+
* @returns the selection when a listener supplied one, else undefined.
|
|
141
|
+
*/
|
|
142
|
+
private selectionFromWaterfall;
|
|
143
|
+
/**
|
|
144
|
+
* Append the request header, and re-append it whenever the route changes.
|
|
145
|
+
*
|
|
146
|
+
* The provider written here is the **real** dsh route (`copilot-proxy`,
|
|
147
|
+
* `amazon-bedrock`, …), not this engine's name. That is not cosmetic:
|
|
148
|
+
* api-proxy re-reads this field on every read as "the model this session is
|
|
149
|
+
* on", resolves it against `ctx.llm.listProviders()`, and locks the composer
|
|
150
|
+
* when the name is not a registered provider — so writing the engine name
|
|
151
|
+
* here made every session demand a fresh model pick after each turn. The
|
|
152
|
+
* engine that ran the turn is recorded in the `*.loop-engine.json` sidecar,
|
|
153
|
+
* which is where per-session engine provenance already lives.
|
|
154
|
+
*
|
|
155
|
+
* Re-logging on change mirrors the in-process loop: the header is the log's
|
|
156
|
+
* record of what each request ran under, so a mid-session model switch has to
|
|
157
|
+
* produce a new snapshot or the log misattributes every later turn.
|
|
158
|
+
*
|
|
159
|
+
* @param selected - the model resolved for the step about to run.
|
|
160
|
+
*/
|
|
161
|
+
private noteRequestHeader;
|
|
113
162
|
/** Run one Claude Code query for the current step and map its transcript into the session log. */
|
|
114
163
|
private step;
|
|
115
164
|
}
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Only the route's *transport* is derived. Which model runs is settled earlier
|
|
15
15
|
* by the agent's own resolution (session choice, then `agentDefaultModel`), and
|
|
16
|
-
* is passed in rather than re-read here.
|
|
16
|
+
* is passed in rather than re-read here. A route may also carry a `childEnv`
|
|
17
|
+
* map, which is not derived at all but forwarded verbatim — the escape hatch
|
|
18
|
+
* for what a protocol mapping cannot express.
|
|
17
19
|
*
|
|
18
20
|
* Everything is resolved per call. `credentials.resolve` is contractually a
|
|
19
21
|
* per-call read — the store layers process env over `~/.dsh/.credentials.yaml`
|