dsh-workbuddy-connect 0.3.2 → 0.4.0
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/README.en.md +17 -5
- package/README.md +18 -7
- package/lib/bin.js +1 -1
- package/lib/client.js +866 -31
- package/lib/{host-heartbeat-BSsKUoB8.js → host-heartbeat-0UAWqWac.js} +232 -3
- package/lib/index.d.ts +224 -1
- package/lib/index.js +563 -43
- package/package.json +11 -5
|
@@ -3,6 +3,7 @@ import { homedir, release } from "node:os";
|
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
5
5
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { execFileSync } from "node:child_process";
|
|
7
8
|
//#region src/auth.ts
|
|
8
9
|
/**
|
|
@@ -572,7 +573,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
572
573
|
supportedEfforts: [
|
|
573
574
|
"low",
|
|
574
575
|
"high",
|
|
575
|
-
"
|
|
576
|
+
"max"
|
|
576
577
|
],
|
|
577
578
|
defaultEffort: "high",
|
|
578
579
|
canDisableThinking: true
|
|
@@ -618,6 +619,137 @@ var WorkBuddyCatalog = class {
|
|
|
618
619
|
}
|
|
619
620
|
};
|
|
620
621
|
//#endregion
|
|
622
|
+
//#region src/probe.ts
|
|
623
|
+
/**
|
|
624
|
+
* The reasoning-effort probe: decide whether a model's `reasoning_effort`
|
|
625
|
+
* parameter is actually validated, and if so which canonical values it accepts.
|
|
626
|
+
*
|
|
627
|
+
* Implements `docs/reasoning-effort-probe-plan.md` §4. The order matters and is
|
|
628
|
+
* not an optimization:
|
|
629
|
+
*
|
|
630
|
+
* 1. **Baseline** (no `reasoning_effort`) proves the model, credential, and
|
|
631
|
+
* request shape work at all, so a later rejection can be attributed.
|
|
632
|
+
* 2. **Sentinel** (a fresh random, impossible-to-collide value) answers the one
|
|
633
|
+
* question a per-level sweep cannot: does the upstream validate the field?
|
|
634
|
+
* A model that accepts the sentinel answers 200 to *everything*, so its
|
|
635
|
+
* per-level results would be uniformly false positives.
|
|
636
|
+
* 3. **Levels**, only after the sentinel was refused.
|
|
637
|
+
*
|
|
638
|
+
* The result is an observation, never a capability claim. Even a fully
|
|
639
|
+
* successful sweep means "the upstream accepted these spellings", not "these
|
|
640
|
+
* spellings change how the model thinks".
|
|
641
|
+
*
|
|
642
|
+
* @module dsh-workbuddy-connect/probe
|
|
643
|
+
*/
|
|
644
|
+
/**
|
|
645
|
+
* The canonical values a probe tests, in a fixed order.
|
|
646
|
+
*
|
|
647
|
+
* `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
|
|
648
|
+
* by policy — disabling thinking is a separate capability the upstream must
|
|
649
|
+
* declare through `canDisableThinking`, never something probing may infer.
|
|
650
|
+
*/
|
|
651
|
+
const PROBE_EFFORT_CANDIDATES = [
|
|
652
|
+
"low",
|
|
653
|
+
"medium",
|
|
654
|
+
"high",
|
|
655
|
+
"xhigh",
|
|
656
|
+
"max"
|
|
657
|
+
];
|
|
658
|
+
/** Prompt body used by every probe request; carries nothing user-specific. */
|
|
659
|
+
const PROBE_PROMPT = "ping";
|
|
660
|
+
/** Default sentinel: unmistakably non-canonical, different on every call. */
|
|
661
|
+
function randomSentinel() {
|
|
662
|
+
return `probe_sentinel_${randomBytes(12).toString("hex")}`;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* The upstream's "this effort value is not supported" code, measured
|
|
666
|
+
* 2026-09-11 (plan §4.2). It is *not* treated as a permanent protocol promise:
|
|
667
|
+
* anything unrecognized degrades to `unknown` rather than to a capability
|
|
668
|
+
* conclusion.
|
|
669
|
+
*/
|
|
670
|
+
const INVALID_EFFORT_CODE = "invalid_reasoning_effort";
|
|
671
|
+
/** Whether an attempt is an attributable rejection of the effort value. */
|
|
672
|
+
function isEffortRejection(attempt) {
|
|
673
|
+
return attempt.status === 400 && attempt.errorCode === INVALID_EFFORT_CODE;
|
|
674
|
+
}
|
|
675
|
+
/** Whether an attempt shows the upstream accepted the request and streamed. */
|
|
676
|
+
function isAcceptance(attempt) {
|
|
677
|
+
return attempt.status === 200 && attempt.streamed;
|
|
678
|
+
}
|
|
679
|
+
/** Why an attempt ended in `unknown`, phrased for a log line. */
|
|
680
|
+
function unknownReason(stage, attempt) {
|
|
681
|
+
const code = attempt.errorCode === void 0 ? "" : ` (${attempt.errorCode})`;
|
|
682
|
+
const detail = attempt.detail === void 0 ? "" : `: ${attempt.detail}`;
|
|
683
|
+
return `${stage} status ${attempt.status}${code}${detail}`;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Probe one model.
|
|
687
|
+
*
|
|
688
|
+
* `options.candidates` exists so tests can shorten the sweep; production always
|
|
689
|
+
* uses {@link PROBE_EFFORT_CANDIDATES}.
|
|
690
|
+
*/
|
|
691
|
+
async function probeModel(options) {
|
|
692
|
+
const sentinel = options.sentinel ?? randomSentinel;
|
|
693
|
+
const candidates = options.candidates ?? PROBE_EFFORT_CANDIDATES;
|
|
694
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
695
|
+
let requests = 0;
|
|
696
|
+
const attempt = async (effort) => {
|
|
697
|
+
requests += 1;
|
|
698
|
+
const controller = new AbortController();
|
|
699
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
700
|
+
try {
|
|
701
|
+
return await options.send(effort, controller.signal);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
return {
|
|
704
|
+
status: 0,
|
|
705
|
+
streamed: false,
|
|
706
|
+
detail: `transport error: ${String(error)}`
|
|
707
|
+
};
|
|
708
|
+
} finally {
|
|
709
|
+
clearTimeout(timer);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
const baseline = await attempt(void 0);
|
|
713
|
+
if (!isAcceptance(baseline)) return {
|
|
714
|
+
validation: "unknown",
|
|
715
|
+
efforts: [],
|
|
716
|
+
requests,
|
|
717
|
+
reason: unknownReason("baseline", baseline)
|
|
718
|
+
};
|
|
719
|
+
const sentinelAttempt = await attempt(sentinel());
|
|
720
|
+
if (isAcceptance(sentinelAttempt)) return {
|
|
721
|
+
validation: "non-validating",
|
|
722
|
+
efforts: [],
|
|
723
|
+
requests
|
|
724
|
+
};
|
|
725
|
+
if (!isEffortRejection(sentinelAttempt)) return {
|
|
726
|
+
validation: "unknown",
|
|
727
|
+
efforts: [],
|
|
728
|
+
requests,
|
|
729
|
+
reason: unknownReason("sentinel", sentinelAttempt)
|
|
730
|
+
};
|
|
731
|
+
const accepted = [];
|
|
732
|
+
for (const effort of candidates) {
|
|
733
|
+
const levelAttempt = await attempt(effort);
|
|
734
|
+
if (isAcceptance(levelAttempt)) {
|
|
735
|
+
accepted.push(effort);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (isEffortRejection(levelAttempt)) continue;
|
|
739
|
+
return {
|
|
740
|
+
validation: "unknown",
|
|
741
|
+
efforts: [],
|
|
742
|
+
requests,
|
|
743
|
+
reason: unknownReason(`level ${effort}`, levelAttempt)
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
validation: "validating",
|
|
748
|
+
efforts: accepted,
|
|
749
|
+
requests
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
//#endregion
|
|
621
753
|
//#region src/upstream.ts
|
|
622
754
|
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
623
755
|
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
@@ -1049,10 +1181,107 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1049
1181
|
accounts
|
|
1050
1182
|
};
|
|
1051
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* One probe request: a real streaming chat call carrying the effort under
|
|
1186
|
+
* test.
|
|
1187
|
+
*
|
|
1188
|
+
* Shares `chatHeaders` with the normal chat path on purpose — the plan
|
|
1189
|
+
* forbids probing through anything but the plugin's own credential handling,
|
|
1190
|
+
* so a result describes what a real message would experience.
|
|
1191
|
+
*
|
|
1192
|
+
* The caller aborts as soon as a parseable event arrives; the body is never
|
|
1193
|
+
* assembled into an answer. `reasoning_effort` is omitted entirely (rather
|
|
1194
|
+
* than sent empty) when `effort` is undefined, so the baseline case is a
|
|
1195
|
+
* genuinely bare request.
|
|
1196
|
+
*/
|
|
1197
|
+
async probeEffort(credential, model, effort, signal) {
|
|
1198
|
+
const payload = {
|
|
1199
|
+
model,
|
|
1200
|
+
stream: true,
|
|
1201
|
+
messages: [{
|
|
1202
|
+
role: "user",
|
|
1203
|
+
content: PROBE_PROMPT
|
|
1204
|
+
}],
|
|
1205
|
+
max_tokens: 1
|
|
1206
|
+
};
|
|
1207
|
+
if (effort !== void 0) payload["reasoning_effort"] = effort;
|
|
1208
|
+
let response;
|
|
1209
|
+
try {
|
|
1210
|
+
response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
|
|
1211
|
+
method: "POST",
|
|
1212
|
+
headers: {
|
|
1213
|
+
...chatHeaders(credential),
|
|
1214
|
+
"Authorization": `Bearer ${credential.accessToken}`
|
|
1215
|
+
},
|
|
1216
|
+
body: JSON.stringify(payload),
|
|
1217
|
+
signal
|
|
1218
|
+
});
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
return {
|
|
1221
|
+
status: 0,
|
|
1222
|
+
streamed: false,
|
|
1223
|
+
detail: `transport error: ${String(error)}`
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
if (!response.ok) {
|
|
1227
|
+
const text = (await response.text()).slice(0, ERROR_BODY_LIMIT);
|
|
1228
|
+
return {
|
|
1229
|
+
status: response.status,
|
|
1230
|
+
streamed: false,
|
|
1231
|
+
...errorCodeOf(text)
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const streamed = await readFirstEvent(response);
|
|
1235
|
+
return {
|
|
1236
|
+
status: response.status,
|
|
1237
|
+
streamed
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1052
1240
|
};
|
|
1241
|
+
/** Pull `extError.code` out of an upstream error body, if it is shaped that way. */
|
|
1242
|
+
function errorCodeOf(text) {
|
|
1243
|
+
try {
|
|
1244
|
+
const parsed = JSON.parse(text);
|
|
1245
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1246
|
+
const extError = parsed["extError"];
|
|
1247
|
+
if (typeof extError === "object" && extError !== null && !Array.isArray(extError)) {
|
|
1248
|
+
const code = extError["code"];
|
|
1249
|
+
if (typeof code === "string") return {
|
|
1250
|
+
errorCode: code,
|
|
1251
|
+
detail: code
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
} catch {}
|
|
1256
|
+
return { detail: text.slice(0, 200) };
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Consume just enough of a streaming response to know it really streams.
|
|
1260
|
+
*
|
|
1261
|
+
* Returns true on the first chunk containing a data line. Cancels the body
|
|
1262
|
+
* afterwards; a stream that ends or errors before that counts as not streamed,
|
|
1263
|
+
* because an empty 200 is not evidence the effort was accepted.
|
|
1264
|
+
*/
|
|
1265
|
+
async function readFirstEvent(response) {
|
|
1266
|
+
const body = response.body;
|
|
1267
|
+
if (body === null) return false;
|
|
1268
|
+
const reader = body.getReader();
|
|
1269
|
+
const decoder = new TextDecoder();
|
|
1270
|
+
try {
|
|
1271
|
+
for (;;) {
|
|
1272
|
+
const { done, value } = await reader.read();
|
|
1273
|
+
if (done) return false;
|
|
1274
|
+
if (decoder.decode(value, { stream: true }).includes("data:")) return true;
|
|
1275
|
+
}
|
|
1276
|
+
} catch {
|
|
1277
|
+
return false;
|
|
1278
|
+
} finally {
|
|
1279
|
+
await reader.cancel().catch(() => {});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1053
1282
|
//#endregion
|
|
1054
1283
|
//#region src/version.ts
|
|
1055
|
-
const WORKBUDDY_CONNECT_VERSION = "0.
|
|
1284
|
+
const WORKBUDDY_CONNECT_VERSION = "0.4.0";
|
|
1056
1285
|
//#endregion
|
|
1057
1286
|
//#region src/host-heartbeat.ts
|
|
1058
1287
|
/**
|
|
@@ -1195,4 +1424,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
1195
1424
|
return startAtMs <= heartbeat.registeredAt;
|
|
1196
1425
|
}
|
|
1197
1426
|
//#endregion
|
|
1198
|
-
export {
|
|
1427
|
+
export { defaultDesktopAuthPath as C, defaultDesktopAuthCandidates as S, workbuddyOwnAuthPath as T, FALLBACK_WORKBUDDY_MODELS as _, readHostHeartbeat as a, WORKBUDDY_AUTH_FILE_ENV as b, WORKBUDDY_CONNECT_VERSION as c, normalizeCredits as d, prepareChatBody as f, randomSentinel as g, probeModel as h, processStartTimeMs as i, WorkBuddyUpstreamClient as l, PROBE_EFFORT_CANDIDATES as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, regionOf as p, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, classifyUpstreamError as u, WorkBuddyCatalog as v, parseWorkBuddyAuth as w, WorkBuddyCredentialStore as x, WORKBUDDY_AUTH_FILENAME as y };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,66 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import "@earendil-works/pi-ai";
|
|
2
3
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
3
4
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
5
|
import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
5
6
|
import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
7
|
+
//#region src/probe.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* The canonical values a probe tests, in a fixed order.
|
|
10
|
+
*
|
|
11
|
+
* `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
|
|
12
|
+
* by policy — disabling thinking is a separate capability the upstream must
|
|
13
|
+
* declare through `canDisableThinking`, never something probing may infer.
|
|
14
|
+
*/
|
|
15
|
+
declare const PROBE_EFFORT_CANDIDATES: readonly WorkBuddyEffort[];
|
|
16
|
+
/** Sentinel generator; injectable so tests get deterministic values. */
|
|
17
|
+
type SentinelFactory = () => string;
|
|
18
|
+
/** Default sentinel: unmistakably non-canonical, different on every call. */
|
|
19
|
+
declare function randomSentinel(): string;
|
|
20
|
+
/**
|
|
21
|
+
* One response as the probe sees it, split into the only distinctions the
|
|
22
|
+
* attribution rule needs.
|
|
23
|
+
*/
|
|
24
|
+
interface ProbeAttempt {
|
|
25
|
+
/** HTTP status, or 0 for a transport failure. */
|
|
26
|
+
status: number;
|
|
27
|
+
/** True when a parseable SSE event arrived. */
|
|
28
|
+
streamed: boolean;
|
|
29
|
+
/** `extError.code` from a JSON error body, when present. */
|
|
30
|
+
errorCode?: string;
|
|
31
|
+
/** Free-form detail for logs; never shown as a capability claim. */
|
|
32
|
+
detail?: string;
|
|
33
|
+
}
|
|
34
|
+
/** How one attempt is performed; the caller owns credentials and HTTP. */
|
|
35
|
+
type ProbeSender = (effort: string | undefined, signal: AbortSignal) => Promise<ProbeAttempt>;
|
|
36
|
+
/** The outcome of probing one model. */
|
|
37
|
+
type ProbeOutcome = {
|
|
38
|
+
validation: 'validating';
|
|
39
|
+
efforts: readonly WorkBuddyEffort[];
|
|
40
|
+
requests: number;
|
|
41
|
+
} | {
|
|
42
|
+
validation: 'non-validating';
|
|
43
|
+
efforts: readonly [];
|
|
44
|
+
requests: number;
|
|
45
|
+
} | {
|
|
46
|
+
validation: 'unknown';
|
|
47
|
+
efforts: readonly [];
|
|
48
|
+
requests: number;
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Probe one model.
|
|
53
|
+
*
|
|
54
|
+
* `options.candidates` exists so tests can shorten the sweep; production always
|
|
55
|
+
* uses {@link PROBE_EFFORT_CANDIDATES}.
|
|
56
|
+
*/
|
|
57
|
+
declare function probeModel(options: {
|
|
58
|
+
send: ProbeSender;
|
|
59
|
+
sentinel?: SentinelFactory;
|
|
60
|
+
candidates?: readonly WorkBuddyEffort[];
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
}): Promise<ProbeOutcome>;
|
|
63
|
+
//#endregion
|
|
6
64
|
//#region src/upstream.d.ts
|
|
7
65
|
/** WorkBuddy region selected by the credential's login domain. */
|
|
8
66
|
type WorkBuddyRegion = 'cn' | 'global';
|
|
@@ -137,6 +195,20 @@ declare class WorkBuddyUpstreamClient {
|
|
|
137
195
|
fetchModels(credential: WorkBuddyCredential): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
138
196
|
/** POST the billing endpoint for the aggregated remaining credit. */
|
|
139
197
|
fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
|
|
198
|
+
/**
|
|
199
|
+
* One probe request: a real streaming chat call carrying the effort under
|
|
200
|
+
* test.
|
|
201
|
+
*
|
|
202
|
+
* Shares `chatHeaders` with the normal chat path on purpose — the plan
|
|
203
|
+
* forbids probing through anything but the plugin's own credential handling,
|
|
204
|
+
* so a result describes what a real message would experience.
|
|
205
|
+
*
|
|
206
|
+
* The caller aborts as soon as a parseable event arrives; the body is never
|
|
207
|
+
* assembled into an answer. `reasoning_effort` is omitted entirely (rather
|
|
208
|
+
* than sent empty) when `effort` is undefined, so the baseline case is a
|
|
209
|
+
* genuinely bare request.
|
|
210
|
+
*/
|
|
211
|
+
probeEffort(credential: WorkBuddyCredential, model: string, effort: string | undefined, signal: AbortSignal): Promise<ProbeAttempt>;
|
|
140
212
|
}
|
|
141
213
|
//#endregion
|
|
142
214
|
//#region src/auth.d.ts
|
|
@@ -277,6 +349,93 @@ declare class WorkBuddyCatalog {
|
|
|
277
349
|
set(models: readonly WorkBuddyModelInfo[]): void;
|
|
278
350
|
}
|
|
279
351
|
//#endregion
|
|
352
|
+
//#region src/probe-store.d.ts
|
|
353
|
+
/** Basename of the probe record inside the Harness home. */
|
|
354
|
+
declare const WORKBUDDY_PROBE_FILENAME = ".workbuddy-probe.json";
|
|
355
|
+
/**
|
|
356
|
+
* Whether the model's effort parameter is actually validated.
|
|
357
|
+
*
|
|
358
|
+
* - `validating`: the upstream rejected an unknown sentinel value, so a
|
|
359
|
+
* per-level answer is meaningful.
|
|
360
|
+
* - `non-validating`: the upstream accepted the sentinel, so it ignores or
|
|
361
|
+
* loosely coerces the parameter and no per-level answer can be trusted.
|
|
362
|
+
* - `unknown`: baseline or sentinel failed for an unrelated reason (auth,
|
|
363
|
+
* rate limit, transport, ambiguous error body). Not a negative claim.
|
|
364
|
+
*/
|
|
365
|
+
type WorkBuddyProbeValidation = 'validating' | 'non-validating' | 'unknown';
|
|
366
|
+
/** One model's recorded observation. */
|
|
367
|
+
interface WorkBuddyProbeRecord {
|
|
368
|
+
/** Fingerprint of the catalog row this observation was made against. */
|
|
369
|
+
fingerprint: string;
|
|
370
|
+
validation: WorkBuddyProbeValidation;
|
|
371
|
+
/** Efforts verified as accepted; only ever non-empty for `validating`. */
|
|
372
|
+
efforts: readonly WorkBuddyEffort[];
|
|
373
|
+
/** When the probe ran, epoch milliseconds. */
|
|
374
|
+
probedAtMs: number;
|
|
375
|
+
/** Plugin version that produced the record. */
|
|
376
|
+
pluginVersion: string;
|
|
377
|
+
}
|
|
378
|
+
/** Plugin-owned probe record path inside the Harness home. */
|
|
379
|
+
declare function workbuddyProbePath(): string;
|
|
380
|
+
/**
|
|
381
|
+
* Fingerprint the catalog fields a probe depends on.
|
|
382
|
+
*
|
|
383
|
+
* Deliberately excludes display-only fields (`name`, `billing`, `contextWindow`)
|
|
384
|
+
* so a rename or a promo badge does not throw away a valid observation, and
|
|
385
|
+
* deliberately includes the whole reasoning object so any change to the
|
|
386
|
+
* declared shape re-probes.
|
|
387
|
+
*/
|
|
388
|
+
declare function fingerprintModel(info: WorkBuddyModelInfo): string;
|
|
389
|
+
/** Options for {@link WorkBuddyProbeStore}. */
|
|
390
|
+
interface WorkBuddyProbeStoreOptions {
|
|
391
|
+
/** Explicit state-file path, overriding the `$DSH_HOME` default. */
|
|
392
|
+
path?: string;
|
|
393
|
+
/** Observation lifetime; defaults to 14 days. */
|
|
394
|
+
ttlMs?: number;
|
|
395
|
+
/** Plugin version stamped into new records. */
|
|
396
|
+
pluginVersion: string;
|
|
397
|
+
/** Clock injection for tests. */
|
|
398
|
+
now?: () => number;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* The plugin's probe records: read once, written atomically, never trusted
|
|
402
|
+
* across a fingerprint change or past the TTL.
|
|
403
|
+
*/
|
|
404
|
+
declare class WorkBuddyProbeStore {
|
|
405
|
+
private readonly path;
|
|
406
|
+
private readonly ttlMs;
|
|
407
|
+
private readonly pluginVersion;
|
|
408
|
+
private readonly now;
|
|
409
|
+
private records;
|
|
410
|
+
constructor(options: WorkBuddyProbeStoreOptions | string);
|
|
411
|
+
/** Resolved state-file path, for the CLI and tests. */
|
|
412
|
+
filePath(): string;
|
|
413
|
+
private load;
|
|
414
|
+
/**
|
|
415
|
+
* The usable record for a model, or `undefined` when there is none, it is
|
|
416
|
+
* expired, or it was taken against a different catalog row.
|
|
417
|
+
*/
|
|
418
|
+
get(modelId: string, fingerprint: string): WorkBuddyProbeRecord | undefined;
|
|
419
|
+
/**
|
|
420
|
+
* Store one observation. Only a decisive answer (`validating` /
|
|
421
|
+
* `non-validating`) replaces an existing decisive record: a transient
|
|
422
|
+
* `unknown` must not erase knowledge the user already paid for.
|
|
423
|
+
*/
|
|
424
|
+
set(modelId: string, record: WorkBuddyProbeRecord): void;
|
|
425
|
+
/** Drop every record; used by the card's explicit "clear" action. */
|
|
426
|
+
clear(): void;
|
|
427
|
+
/** Every record currently held, for status display. */
|
|
428
|
+
all(): Readonly<Record<string, WorkBuddyProbeRecord>>;
|
|
429
|
+
/** Build a record stamped with this store's clock and version. */
|
|
430
|
+
record(fingerprint: string, validation: WorkBuddyProbeValidation, efforts: readonly WorkBuddyEffort[]): WorkBuddyProbeRecord;
|
|
431
|
+
/**
|
|
432
|
+
* Write through a temporary file and rename, so a crash mid-write cannot
|
|
433
|
+
* leave a half-parsed document that reads as "no records" and silently drops
|
|
434
|
+
* every observation.
|
|
435
|
+
*/
|
|
436
|
+
private persist;
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
280
439
|
//#region src/shim.d.ts
|
|
281
440
|
/** Minimal logger surface the plugin context already provides. */
|
|
282
441
|
interface ShimLogger {
|
|
@@ -324,6 +483,11 @@ interface WorkBuddyAdapterOptions {
|
|
|
324
483
|
catalog: WorkBuddyCatalog;
|
|
325
484
|
/** Resolve the durable attachment service at request time, when present. */
|
|
326
485
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
486
|
+
/**
|
|
487
|
+
* Look up a local probe observation for a model. Consulted only for rows the
|
|
488
|
+
* upstream left undeclared; absent means declared-set-only behavior.
|
|
489
|
+
*/
|
|
490
|
+
observe?: (modelId: string) => WorkBuddyProbeRecord | undefined;
|
|
327
491
|
}
|
|
328
492
|
/** What {@link createWorkBuddyAdapter} hands back. */
|
|
329
493
|
interface WorkBuddyAdapter {
|
|
@@ -345,6 +509,59 @@ interface WorkBuddyAdapter {
|
|
|
345
509
|
*/
|
|
346
510
|
declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
|
|
347
511
|
//#endregion
|
|
512
|
+
//#region src/probe-service.d.ts
|
|
513
|
+
/** What the caller learns about a completed probe. */
|
|
514
|
+
type WorkBuddyProbeStatus = {
|
|
515
|
+
state: 'ok';
|
|
516
|
+
validation: WorkBuddyProbeRecord['validation'];
|
|
517
|
+
efforts: readonly string[];
|
|
518
|
+
requests: number;
|
|
519
|
+
} | {
|
|
520
|
+
state: 'unavailable';
|
|
521
|
+
reason: string;
|
|
522
|
+
};
|
|
523
|
+
/** Options for {@link WorkBuddyProbeService}. */
|
|
524
|
+
interface WorkBuddyProbeServiceOptions {
|
|
525
|
+
store: WorkBuddyProbeStore;
|
|
526
|
+
catalog: WorkBuddyCatalog;
|
|
527
|
+
credentials: WorkBuddyCredentialStore;
|
|
528
|
+
client: WorkBuddyUpstreamClient;
|
|
529
|
+
/** Whether probing is permitted at all; consulted before every sweep. */
|
|
530
|
+
consent: () => boolean;
|
|
531
|
+
sentinel?: SentinelFactory;
|
|
532
|
+
/** Injectable for tests; defaults to the live upstream sender. */
|
|
533
|
+
send?: (modelId: string) => ProbeSender;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Serial probe runner. One instance is shared by the manual API and any
|
|
537
|
+
* future automatic trigger, so the two can never overlap.
|
|
538
|
+
*/
|
|
539
|
+
declare class WorkBuddyProbeService {
|
|
540
|
+
private readonly options;
|
|
541
|
+
private queue;
|
|
542
|
+
private readonly pending;
|
|
543
|
+
private running;
|
|
544
|
+
constructor(options: WorkBuddyProbeServiceOptions);
|
|
545
|
+
/** Whether a sweep is in flight right now. */
|
|
546
|
+
isRunning(): boolean;
|
|
547
|
+
/**
|
|
548
|
+
* The record the adapter may use for this model, or `undefined`.
|
|
549
|
+
*
|
|
550
|
+
* Applies the plan's precedence (§5): a declared set always wins, so a model
|
|
551
|
+
* that declares `supportedEfforts` is never answered from an observation.
|
|
552
|
+
*/
|
|
553
|
+
recordFor(modelId: string): WorkBuddyProbeRecord | undefined;
|
|
554
|
+
/**
|
|
555
|
+
* Probe one model, serially.
|
|
556
|
+
*
|
|
557
|
+
* The authenticated manual route supplies one-request consent after UI
|
|
558
|
+
* confirmation. Other callers must pass the configured consent gate.
|
|
559
|
+
* Manual consent never changes the automatic-probing configuration.
|
|
560
|
+
* Explicit requests bypass historical results, but share an ongoing run.
|
|
561
|
+
*/
|
|
562
|
+
probe(modelId: string, manualConsent?: boolean): Promise<WorkBuddyProbeStatus>;
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
348
565
|
//#region src/host-heartbeat.d.ts
|
|
349
566
|
/**
|
|
350
567
|
* Host-side heartbeat: a small JSON file written under `$DSH_HOME` once the
|
|
@@ -432,6 +649,12 @@ declare const WORKBUDDY_SETTINGS_NS: SettingsNamespace;
|
|
|
432
649
|
interface Config {
|
|
433
650
|
/** Explicit WorkBuddy desktop auth-file path, overriding env and platform defaults. */
|
|
434
651
|
authFile?: string;
|
|
652
|
+
/**
|
|
653
|
+
* Whether the user has authorized sending probe requests about reasoning
|
|
654
|
+
* efforts. Off by default: a probe spends real credit, so nothing is sent
|
|
655
|
+
* until the user explicitly agrees.
|
|
656
|
+
*/
|
|
657
|
+
probeConsent?: boolean;
|
|
435
658
|
}
|
|
436
659
|
declare const Config: z<Config>;
|
|
437
660
|
/**
|
|
@@ -442,4 +665,4 @@ declare const Config: z<Config>;
|
|
|
442
665
|
*/
|
|
443
666
|
declare function apply(ctx: Context, config: Config): void;
|
|
444
667
|
//#endregion
|
|
445
|
-
export { Config, FALLBACK_WORKBUDDY_MODELS, type UpstreamErrorKind, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, inject, isHeartbeatProcessAlive, name, normalizeCredits, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath };
|
|
668
|
+
export { Config, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type UpstreamErrorKind, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, fingerprintModel, inject, isHeartbeatProcessAlive, name, normalizeCredits, parseWorkBuddyAuth, prepareChatBody, probeModel, processStartTimeMs, randomSentinel, readHostHeartbeat, regionOf, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath };
|