@vidge/dsh-agent-hub 0.1.0-rc2 → 0.1.0-rc3
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 +108 -24
- package/lib/types/engine-claude/agent.d.ts +60 -11
- package/package.json +1 -1
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 {
|
|
@@ -706,6 +706,7 @@ async function loadClaudeQuery() {
|
|
|
706
706
|
return claudeQueryPromise;
|
|
707
707
|
}
|
|
708
708
|
var NATIVE_MODEL_LABEL = "claude-code-native";
|
|
709
|
+
var NATIVE_PROVIDER_LABEL = PROVIDER;
|
|
709
710
|
function failureCode(subtype) {
|
|
710
711
|
switch (subtype) {
|
|
711
712
|
case "error_during_execution":
|
|
@@ -1036,19 +1037,35 @@ var ClaudeCodeAgent = class {
|
|
|
1036
1037
|
return true;
|
|
1037
1038
|
}
|
|
1038
1039
|
/**
|
|
1039
|
-
* Resolve the model one query runs on, session
|
|
1040
|
+
* Resolve the model one query runs on, per-session selection first.
|
|
1040
1041
|
*
|
|
1041
|
-
* The
|
|
1042
|
-
* `agentDefaultModel` holds the global default; reading both is what makes
|
|
1043
|
-
* the dsh model picker mean something for this engine. The service is
|
|
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.
|
|
1042
|
+
* The layers, in precedence order:
|
|
1047
1043
|
*
|
|
1044
|
+
* 1. **The `agent/request` waterfall.** This is the seam the dsh model
|
|
1045
|
+
* picker actually drives. api-proxy installs `installModelSelection` on
|
|
1046
|
+
* every agent's own context, which listens on `system-prompt/assemble`
|
|
1047
|
+
* to *snapshot* the session's selection and on `agent/request` to *apply*
|
|
1048
|
+
* the snapshot — two stages, so a mid-turn switch lands on a later step
|
|
1049
|
+
* rather than splitting the prompt from the route. Both must be
|
|
1050
|
+
* dispatched, and in that order: the request listener reads
|
|
1051
|
+
* `selection.assembled`, which only the assemble listener writes, so
|
|
1052
|
+
* dispatching the request waterfall alone yields nothing.
|
|
1053
|
+
* 2. `AgentOptions.model` — the create-time seed.
|
|
1054
|
+
* 3. `agentDefaultModel` — the global default.
|
|
1055
|
+
* 4. The deployment's pinned `config.model`.
|
|
1056
|
+
* 5. Nothing, leaving the CLI on its own model.
|
|
1057
|
+
*
|
|
1058
|
+
* Every layer is optional and every failure degrades to the next one: a
|
|
1059
|
+
* minimal profile mounts neither service, and a listener that throws must
|
|
1060
|
+
* cost this session its turn no more than a missing service does.
|
|
1061
|
+
*
|
|
1062
|
+
* @param signal - the step's cancellation signal, forwarded to prompt assembly.
|
|
1048
1063
|
* @returns the chosen id (undefined leaves the CLI on its own default),
|
|
1049
1064
|
* its provider route, and the layer that chose it.
|
|
1050
1065
|
*/
|
|
1051
|
-
resolveModel() {
|
|
1066
|
+
async resolveModel(signal) {
|
|
1067
|
+
const selected = await this.selectionFromWaterfall(signal);
|
|
1068
|
+
if (selected !== void 0) return selected;
|
|
1052
1069
|
if (this.options.model !== void 0) {
|
|
1053
1070
|
return { model: this.options.model, provider: this.options.provider, source: "session" };
|
|
1054
1071
|
}
|
|
@@ -1068,22 +1085,88 @@ var ClaudeCodeAgent = class {
|
|
|
1068
1085
|
}
|
|
1069
1086
|
return { model: void 0, provider: void 0, source: "native" };
|
|
1070
1087
|
}
|
|
1071
|
-
/**
|
|
1072
|
-
|
|
1073
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* Ask the host what this session is routed to, through the two waterfalls
|
|
1090
|
+
* that carry a per-session selection.
|
|
1091
|
+
*
|
|
1092
|
+
* The seed handed to `agent/request` is the same one the in-process loop
|
|
1093
|
+
* seeds with — the agent's own options — so a host that installs no listener
|
|
1094
|
+
* gets its own answer back and this returns undefined, leaving the layers
|
|
1095
|
+
* below untouched. A listener that replaces it wins.
|
|
1096
|
+
*
|
|
1097
|
+
* The assemble pass is dispatched for its *side effect* on the selection
|
|
1098
|
+
* state; its returned prompt is discarded, because Claude Code builds its own
|
|
1099
|
+
* prompt and dsh's assembly never reaches the child. That makes this a real
|
|
1100
|
+
* (if small) cost per step: the host's prompt providers run and their output
|
|
1101
|
+
* is dropped. It is the price of reaching a selection whose only publisher is
|
|
1102
|
+
* that listener pair.
|
|
1103
|
+
*
|
|
1104
|
+
* @param signal - the step's cancellation signal.
|
|
1105
|
+
* @returns the selection when a listener supplied one, else undefined.
|
|
1106
|
+
*/
|
|
1107
|
+
async selectionFromWaterfall(signal) {
|
|
1108
|
+
const phase = this.phase;
|
|
1109
|
+
if (phase.kind !== "running") return void 0;
|
|
1110
|
+
const { turn, step } = phase;
|
|
1111
|
+
const seed = {
|
|
1112
|
+
provider: this.options.provider ?? "",
|
|
1113
|
+
model: this.options.model ?? ""
|
|
1114
|
+
};
|
|
1115
|
+
try {
|
|
1116
|
+
const systemPrompt = this.loopCtx.get("systemPrompt");
|
|
1117
|
+
if (systemPrompt !== void 0) {
|
|
1118
|
+
await systemPrompt.assemble(assembleContextFor(this, signal));
|
|
1119
|
+
}
|
|
1120
|
+
const proposed = await this.dispatch.waterfall(
|
|
1121
|
+
"agent/request",
|
|
1122
|
+
{ turn, step, signal },
|
|
1123
|
+
() => Promise.resolve(seed)
|
|
1124
|
+
);
|
|
1125
|
+
if (proposed.model === "" || proposed.provider === "") return void 0;
|
|
1126
|
+
if (proposed.provider === seed.provider && proposed.model === seed.model) return void 0;
|
|
1127
|
+
return { model: proposed.model, provider: proposed.provider, source: "selection" };
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
this.ctx.logger.warn("claude-code: per-session model selection unavailable: %s", error);
|
|
1130
|
+
return void 0;
|
|
1131
|
+
}
|
|
1074
1132
|
}
|
|
1075
|
-
/**
|
|
1076
|
-
|
|
1077
|
-
|
|
1133
|
+
/**
|
|
1134
|
+
* Append the request header, and re-append it whenever the route changes.
|
|
1135
|
+
*
|
|
1136
|
+
* The provider written here is the **real** dsh route (`copilot-proxy`,
|
|
1137
|
+
* `amazon-bedrock`, …), not this engine's name. That is not cosmetic:
|
|
1138
|
+
* api-proxy re-reads this field on every read as "the model this session is
|
|
1139
|
+
* on", resolves it against `ctx.llm.listProviders()`, and locks the composer
|
|
1140
|
+
* when the name is not a registered provider — so writing the engine name
|
|
1141
|
+
* here made every session demand a fresh model pick after each turn. The
|
|
1142
|
+
* engine that ran the turn is recorded in the `*.loop-engine.json` sidecar,
|
|
1143
|
+
* which is where per-session engine provenance already lives.
|
|
1144
|
+
*
|
|
1145
|
+
* Re-logging on change mirrors the in-process loop: the header is the log's
|
|
1146
|
+
* record of what each request ran under, so a mid-session model switch has to
|
|
1147
|
+
* produce a new snapshot or the log misattributes every later turn.
|
|
1148
|
+
*
|
|
1149
|
+
* @param selected - the model resolved for the step about to run.
|
|
1150
|
+
*/
|
|
1151
|
+
noteRequestHeader(selected) {
|
|
1078
1152
|
const header = canonicalHeader({
|
|
1079
|
-
config: {
|
|
1153
|
+
config: {
|
|
1154
|
+
provider: selected.provider ?? NATIVE_PROVIDER_LABEL,
|
|
1155
|
+
model: selected.model ?? NATIVE_MODEL_LABEL
|
|
1156
|
+
}
|
|
1080
1157
|
});
|
|
1081
1158
|
const baseline = this.session.requestHeader();
|
|
1082
|
-
this.
|
|
1083
|
-
header,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1159
|
+
if (!this.requestHeaderLogged) {
|
|
1160
|
+
this.session.append("request/header", {
|
|
1161
|
+
header,
|
|
1162
|
+
reason: baseline === void 0 ? "initial" : "resume"
|
|
1163
|
+
});
|
|
1164
|
+
this.requestHeaderLogged = true;
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (baseline === void 0 || !headerEquals(baseline, header)) {
|
|
1168
|
+
this.session.append("request/header", { header, reason: "change" });
|
|
1169
|
+
}
|
|
1087
1170
|
}
|
|
1088
1171
|
/** Run one Claude Code query for the current step and map its transcript into the session log. */
|
|
1089
1172
|
async step() {
|
|
@@ -1099,7 +1182,9 @@ var ClaudeCodeAgent = class {
|
|
|
1099
1182
|
if (prompt.length === 0) {
|
|
1100
1183
|
throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
|
|
1101
1184
|
}
|
|
1102
|
-
this.
|
|
1185
|
+
const selected = await this.resolveModel(signal);
|
|
1186
|
+
signal.throwIfAborted();
|
|
1187
|
+
this.noteRequestHeader(selected);
|
|
1103
1188
|
signal.throwIfAborted();
|
|
1104
1189
|
const controller = new AbortController();
|
|
1105
1190
|
const cancel = () => {
|
|
@@ -1110,7 +1195,6 @@ var ClaudeCodeAgent = class {
|
|
|
1110
1195
|
signal.addEventListener("abort", cancel, { once: true });
|
|
1111
1196
|
const diagnostics = [];
|
|
1112
1197
|
try {
|
|
1113
|
-
const selected = this.resolveModel();
|
|
1114
1198
|
const derived = await deriveProviderEnv(this.loopCtx, selected.provider);
|
|
1115
1199
|
if (derived !== void 0) diagnostics.push(derived.diagnostic);
|
|
1116
1200
|
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
|
}
|