dsh-mnemon-source-skills 0.5.2 → 0.5.4

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.
@@ -239,6 +239,30 @@ interface RunTarget {
239
239
  draftId?: string;
240
240
  versionId?: string;
241
241
  }
242
+ /** One Provider and model, as a user picked it; never carries a reasoning effort. */
243
+ interface ModelRoute {
244
+ provider: string;
245
+ model: string;
246
+ }
247
+ /**
248
+ * The models the user picked for skill work, kept beside the ledger. Every
249
+ * entry is optional: absent means "follow the task Agent's own route".
250
+ * `suggestionModels` is keyed by suggestion id, `skillRunModels` by skill name.
251
+ */
252
+ interface DraftPreferences {
253
+ version: 1;
254
+ draftModel?: ModelRoute;
255
+ runModel?: ModelRoute;
256
+ suggestionModels: Record<string, ModelRoute>;
257
+ skillRunModels: Record<string, ModelRoute>;
258
+ }
259
+ /** What the pickers read: the stored picks, named the way the pages use them. */
260
+ interface DraftPreferencesView {
261
+ model?: ModelRoute;
262
+ runModel?: ModelRoute;
263
+ suggestionModels: Record<string, ModelRoute>;
264
+ skillRunModels: Record<string, ModelRoute>;
265
+ }
242
266
  interface CaseResult {
243
267
  caseId: string;
244
268
  title: string;
@@ -298,6 +322,10 @@ interface ReviewLogEntry {
298
322
  }
299
323
  /** How the Source admits a review's suggestions; the Host sends its settings with each review. */
300
324
  interface SkillsPolicy {
325
+ /** Whether a review may propose new skills; omitted by older Hosts, default true. */
326
+ allowCreate?: boolean;
327
+ /** Whether a review may improve existing skills; omitted by older Hosts, default true. */
328
+ allowUpdate?: boolean;
301
329
  minConfidence: number;
302
330
  expiryDays: number;
303
331
  maxPendingPerSession: number;
@@ -592,4 +620,4 @@ interface MarketInstall {
592
620
  installedAt: string;
593
621
  }
594
622
  //#endregion
595
- export { BacktestRun, CaseResult, CatalogView, DEFAULT_SKILLS_POLICY, type DetectedSource, DraftStatus, DraftView, EVOLUTION_SIGNALS, EvalCase, EvalCheck, EvidencePack, EvidenceReference, EvolutionSignal, LocalizedText, MarketAudit, MarketEntry, MarketInstall, MarketInstallState, MarketItem, MarketPreview, MarketRegistry, MarketRegistryFile, MarketSearchResult, MarketSource, type MarketSourceKind, MarketView, ReviewLogEntry, ReviewRecordInput, ReviewRecordResult, ReviewScore, RunStatus, RunTarget, RunTargetKind, SKILL_ROOT_KINDS, SKILL_ROOT_RANK, SUGGESTION_STATUSES, SkillCatalog, SkillDocument, SkillDraft, SkillInvocation, SkillProblem, SkillRoot, SkillRootKind, SkillSuggestion, SkillSummary, SkillVersion, SkillVersionReason, SkillsOverview, SkillsPolicy, SuggestionStatus, SuggestionView, Trajectory, TrajectoryAnnotation, TrajectoryStats, TrajectoryStep, TrajectoryStepKind, TrajectorySummary, TrashEntry, TrashRestoreInput, TrashRestorePreview };
623
+ export { BacktestRun, CaseResult, CatalogView, DEFAULT_SKILLS_POLICY, type DetectedSource, DraftPreferences, DraftPreferencesView, DraftStatus, DraftView, EVOLUTION_SIGNALS, EvalCase, EvalCheck, EvidencePack, EvidenceReference, EvolutionSignal, LocalizedText, MarketAudit, MarketEntry, MarketInstall, MarketInstallState, MarketItem, MarketPreview, MarketRegistry, MarketRegistryFile, MarketSearchResult, MarketSource, type MarketSourceKind, MarketView, ModelRoute, ReviewLogEntry, ReviewRecordInput, ReviewRecordResult, ReviewScore, RunStatus, RunTarget, RunTargetKind, SKILL_ROOT_KINDS, SKILL_ROOT_RANK, SUGGESTION_STATUSES, SkillCatalog, SkillDocument, SkillDraft, SkillInvocation, SkillProblem, SkillRoot, SkillRootKind, SkillSuggestion, SkillSummary, SkillVersion, SkillVersionReason, SkillsOverview, SkillsPolicy, SuggestionStatus, SuggestionView, Trajectory, TrajectoryAnnotation, TrajectoryStats, TrajectoryStep, TrajectoryStepKind, TrajectorySummary, TrashEntry, TrashRestoreInput, TrashRestorePreview };
package/lib/contracts.js CHANGED
@@ -31,6 +31,8 @@ const SUGGESTION_STATUSES = [
31
31
  "invalidated"
32
32
  ];
33
33
  const DEFAULT_SKILLS_POLICY = Object.freeze({
34
+ allowCreate: true,
35
+ allowUpdate: true,
34
36
  minConfidence: .6,
35
37
  expiryDays: 7,
36
38
  maxPendingPerSession: 5
package/lib/index.js CHANGED
@@ -489,6 +489,43 @@ var SkillsStore = class {
489
489
  installs: array(value.installs)
490
490
  };
491
491
  }
492
+ /**
493
+ * What the user picked for draft and backtest runs, kept out of `fingerprint()`
494
+ * on purpose: changing a picker must not invalidate an open page's revision.
495
+ */
496
+ preferences() {
497
+ const stored = object$1(this.readJson("preferences.json")) ?? {};
498
+ const route = (value) => {
499
+ const picked = object$1(value);
500
+ const provider = typeof picked?.provider === "string" ? picked.provider : void 0;
501
+ const model = typeof picked?.model === "string" ? picked.model : void 0;
502
+ return provider !== void 0 && model !== void 0 ? {
503
+ provider,
504
+ model
505
+ } : void 0;
506
+ };
507
+ const routes = (value) => {
508
+ const result = {};
509
+ const map = object$1(value);
510
+ if (map !== void 0) for (const [key, item] of Object.entries(map)) {
511
+ const picked = route(item);
512
+ if (picked !== void 0) result[key] = picked;
513
+ }
514
+ return result;
515
+ };
516
+ const draft = route(stored.draftModel);
517
+ const run = route(stored.runModel);
518
+ return {
519
+ version: 1,
520
+ ...draft === void 0 ? {} : { draftModel: draft },
521
+ ...run === void 0 ? {} : { runModel: run },
522
+ suggestionModels: routes(stored.suggestionModels),
523
+ skillRunModels: routes(stored.skillRunModels)
524
+ };
525
+ }
526
+ savePreferences(preferences) {
527
+ this.writeJson("preferences.json", preferences);
528
+ }
492
529
  saveLedger(ledger) {
493
530
  ledger.generation += 1;
494
531
  ledger.reviews = ledger.reviews.slice(0, 200);
@@ -732,13 +769,33 @@ function clip$1(value, maximum) {
732
769
  const text = value.trim();
733
770
  return text.length <= maximum ? text : text.slice(0, maximum - 1) + "…";
734
771
  }
735
- /** Move pending suggestions past their expiry to `expired`; true when anything changed. */
772
+ /**
773
+ * `drafting` was set before this process started, so no live writer owns it any
774
+ * more. A draft write is cancelled only by the Host going away, and the catch
775
+ * that would put the suggestion back never runs then, so the row would sit in
776
+ * "drafting" forever and refuse both a retry and a delete.
777
+ */
778
+ const PROCESS_STARTED_AT = Date.now() - Math.round(process.uptime() * 1e3);
779
+ const ORPHAN_DRAFT_ERROR = "the host restarted while this draft was being written";
780
+ function orphanedDraft(suggestion) {
781
+ const at = Date.parse(suggestion.updatedAt);
782
+ return !Number.isFinite(at) || at < PROCESS_STARTED_AT;
783
+ }
784
+ /**
785
+ * Move pending suggestions past their expiry to `expired`, and suggestions whose
786
+ * draft was abandoned by a dead process back to `pending`; true when anything changed.
787
+ */
736
788
  function expireSuggestions(ledger, now) {
737
789
  let changed = false;
738
790
  for (const suggestion of ledger.suggestions) if (suggestion.status === "pending" && Date.parse(suggestion.expiresAt) < now.getTime()) {
739
791
  suggestion.status = "expired";
740
792
  suggestion.updatedAt = now.toISOString();
741
793
  changed = true;
794
+ } else if (suggestion.status === "drafting" && orphanedDraft(suggestion)) {
795
+ suggestion.status = "pending";
796
+ suggestion.updatedAt = now.toISOString();
797
+ suggestion.lastError = ORPHAN_DRAFT_ERROR;
798
+ changed = true;
742
799
  }
743
800
  return changed;
744
801
  }
@@ -766,7 +823,14 @@ function policyFrom(value) {
766
823
  const candidate = value?.[key];
767
824
  return typeof candidate === "number" && Number.isFinite(candidate) && candidate >= minimum && candidate <= maximum ? candidate : DEFAULT_SKILLS_POLICY[key];
768
825
  };
826
+ const allowed = (key) => {
827
+ const candidate = value?.[key];
828
+ if (candidate !== void 0 && typeof candidate !== "boolean") throw new Error(`skills policy ${key} must be a boolean`);
829
+ return candidate ?? true;
830
+ };
769
831
  return {
832
+ allowCreate: allowed("allowCreate"),
833
+ allowUpdate: allowed("allowUpdate"),
770
834
  minConfidence: pick("minConfidence", 0, 1),
771
835
  expiryDays: Math.round(pick("expiryDays", 1, 90)),
772
836
  maxPendingPerSession: Math.round(pick("maxPendingPerSession", 1, 20))
@@ -785,6 +849,7 @@ function admitReview(context, input) {
785
849
  const stepIndexes = new Set(input.trajectory.steps.map((step) => step.index));
786
850
  const steps = (values) => [...new Set(values.filter((value) => Number.isInteger(value) && stepIndexes.has(value)))].sort((a, b) => a - b);
787
851
  const skipped = [];
852
+ const allowed = (action) => (action === "create" ? policy.allowCreate : policy.allowUpdate) !== false;
788
853
  let inserted = 0;
789
854
  let updated = 0;
790
855
  let casesAdded = 0;
@@ -826,6 +891,14 @@ function admitReview(context, input) {
826
891
  }
827
892
  const existing = activeSkill(catalog, name);
828
893
  const action = existing !== void 0 ? "update" : "create";
894
+ if (!allowed(action)) {
895
+ skipped.push({
896
+ skillName: name,
897
+ action,
898
+ reason: "suggestion_type_disabled"
899
+ });
900
+ continue;
901
+ }
829
902
  if (raw.action === "update" && existing === void 0) {
830
903
  skipped.push({
831
904
  skillName: name,
@@ -940,6 +1013,13 @@ function admitReview(context, input) {
940
1013
  });
941
1014
  continue;
942
1015
  }
1016
+ if (!allowed(activeSkill(catalog, name) === void 0 ? "create" : "update")) {
1017
+ skipped.push({
1018
+ skillName: name,
1019
+ reason: "suggestion_type_disabled"
1020
+ });
1021
+ continue;
1022
+ }
943
1023
  if (similarCase(evals.cases, name, title, prompt) !== void 0) {
944
1024
  skipped.push({
945
1025
  skillName: name,
@@ -3387,6 +3467,39 @@ var SkillsService = class {
3387
3467
  return { purged: entries.length };
3388
3468
  });
3389
3469
  }
3470
+ /** The model the user picked for drafts and backtests, if any. */
3471
+ draftPreferences() {
3472
+ const stored = this.store.preferences();
3473
+ return {
3474
+ ...stored.draftModel === void 0 ? {} : { model: stored.draftModel },
3475
+ ...stored.runModel === void 0 ? {} : { runModel: stored.runModel },
3476
+ suggestionModels: stored.suggestionModels,
3477
+ skillRunModels: stored.skillRunModels
3478
+ };
3479
+ }
3480
+ /**
3481
+ * Record one picker change. `suggestionId` targets a single suggestion,
3482
+ * `skillName` with `slot: 'run'` a single skill, and a null model clears the
3483
+ * entry so the page falls back to following the task Agent again.
3484
+ */
3485
+ setDraftPreference(input) {
3486
+ return this.write(() => {
3487
+ const preferences = this.store.preferences();
3488
+ if (input.slot === "run") {
3489
+ if (input.skillName !== void 0) {
3490
+ if (input.model === null) delete preferences.skillRunModels[input.skillName];
3491
+ else preferences.skillRunModels[input.skillName] = input.model;
3492
+ } else if (input.model === null) delete preferences.runModel;
3493
+ else preferences.runModel = input.model;
3494
+ } else if (input.suggestionId !== void 0) {
3495
+ if (input.model === null) delete preferences.suggestionModels[input.suggestionId];
3496
+ else preferences.suggestionModels[input.suggestionId] = input.model;
3497
+ } else if (input.model === null) delete preferences.draftModel;
3498
+ else preferences.draftModel = input.model;
3499
+ this.store.savePreferences(preferences);
3500
+ return this.draftPreferences();
3501
+ });
3502
+ }
3390
3503
  restoreVersion(workspace, input) {
3391
3504
  return this.write(() => {
3392
3505
  const skill = findSkill(this.catalog(workspace), input.key);
@@ -3426,6 +3539,25 @@ var SkillsService = class {
3426
3539
  return suggestion;
3427
3540
  });
3428
3541
  }
3542
+ /** Remove a suggestion for good, together with any draft it produced and its model pick. */
3543
+ deleteSuggestion(id) {
3544
+ return this.write(() => {
3545
+ const ledger = this.store.ledger();
3546
+ expireSuggestions(ledger, this.clock());
3547
+ const index = ledger.suggestions.findIndex((candidate) => candidate.id === id);
3548
+ if (index < 0) throw new Error("suggestion not found: " + id);
3549
+ if (ledger.suggestions[index].status === "drafting") throw new Error("a draft is being written for this suggestion; wait for it to finish");
3550
+ ledger.suggestions.splice(index, 1);
3551
+ ledger.drafts = ledger.drafts.filter((draft) => draft.suggestionId !== id);
3552
+ const preferences = this.store.preferences();
3553
+ if (preferences.suggestionModels[id] !== void 0) {
3554
+ delete preferences.suggestionModels[id];
3555
+ this.store.savePreferences(preferences);
3556
+ }
3557
+ this.store.saveLedger(ledger);
3558
+ return { deleted: id };
3559
+ });
3560
+ }
3429
3561
  /** The Host marks a suggestion `drafting` before a task Agent writes, and back to `pending` if it fails. */
3430
3562
  suggestionStatus(input) {
3431
3563
  return this.withSuggestion(input.id, (suggestion) => {
@@ -3675,6 +3807,40 @@ var SkillsService = class {
3675
3807
  return draft;
3676
3808
  });
3677
3809
  }
3810
+ /** Remove a draft for good; its suggestion goes back to `pending` so it can be drafted again. */
3811
+ deleteDraft(id) {
3812
+ return this.write(() => {
3813
+ const ledger = this.store.ledger();
3814
+ const index = ledger.drafts.findIndex((candidate) => candidate.id === id);
3815
+ if (index < 0) throw new Error("draft not found: " + id);
3816
+ const draft = ledger.drafts[index];
3817
+ ledger.drafts.splice(index, 1);
3818
+ if (draft.suggestionId !== void 0) {
3819
+ const suggestion = ledger.suggestions.find((candidate) => candidate.id === draft.suggestionId);
3820
+ if (suggestion?.draftId === draft.id) {
3821
+ delete suggestion.draftId;
3822
+ if (suggestion.status === "review") suggestion.status = "pending";
3823
+ suggestion.updatedAt = this.clock().toISOString();
3824
+ }
3825
+ }
3826
+ this.store.saveLedger(ledger);
3827
+ return { deleted: id };
3828
+ });
3829
+ }
3830
+ /**
3831
+ * Put suggestions whose draft write died with the previous Host back to
3832
+ * `pending`, so the page stops showing them as "drafting" forever.
3833
+ */
3834
+ reclaimOrphanDrafts() {
3835
+ return this.write(() => {
3836
+ const ledger = this.store.ledger();
3837
+ const before = ledger.suggestions.filter((suggestion) => suggestion.status === "drafting").length;
3838
+ if (!expireSuggestions(ledger, this.clock())) return { reclaimed: 0 };
3839
+ this.store.saveLedger(ledger);
3840
+ const after = ledger.suggestions.filter((suggestion) => suggestion.status === "drafting").length;
3841
+ return { reclaimed: Math.max(0, before - after) };
3842
+ });
3843
+ }
3678
3844
  /** The content a run tests, resolved from its target. */
3679
3845
  targetContent(workspace, skillName, target) {
3680
3846
  if (target.kind === "none") return { target: { kind: "none" } };
@@ -4268,6 +4434,16 @@ function rootKind(value) {
4268
4434
  if (typeof value !== "string" || !SKILL_ROOT_KINDS.includes(value)) throw new Error("root must be one of " + SKILL_ROOT_KINDS.join(", "));
4269
4435
  return value;
4270
4436
  }
4437
+ /** A picked model, or null to clear the pick and follow the task Agent again. */
4438
+ function draftModelInput(value) {
4439
+ if (value === void 0 || value === null) return null;
4440
+ if (typeof value !== "object" || Array.isArray(value)) throw new Error("model must be an object or null");
4441
+ const picked = value;
4442
+ return {
4443
+ provider: memoryInputText(picked.provider, "model.provider", 100),
4444
+ model: memoryInputText(picked.model, "model.model", 200)
4445
+ };
4446
+ }
4271
4447
  function caseInput(value, skillName) {
4272
4448
  const input = memoryInputRecord(value, "case");
4273
4449
  const checks = input.checks;
@@ -4362,7 +4538,14 @@ function step(value) {
4362
4538
  function policyInput(value) {
4363
4539
  const input = memoryInputRecord(value, "policy");
4364
4540
  const number = (key) => typeof input[key] === "number" && Number.isFinite(input[key]) ? { [key]: input[key] } : {};
4541
+ const allowed = (key) => {
4542
+ if (input[key] === void 0) return {};
4543
+ if (typeof input[key] !== "boolean") throw new Error(`skills policy ${key} must be a boolean`);
4544
+ return { [key]: input[key] };
4545
+ };
4365
4546
  return {
4547
+ ...allowed("allowCreate"),
4548
+ ...allowed("allowUpdate"),
4366
4549
  ...number("minConfidence"),
4367
4550
  ...number("expiryDays"),
4368
4551
  ...number("maxPendingPerSession")
@@ -4485,6 +4668,7 @@ function createSkillsMemorySource(config = {}) {
4485
4668
  ...effective.dshHome === void 0 ? {} : { dshHome: effective.dshHome },
4486
4669
  ...effective.agentsHome === void 0 ? {} : { agentsHome: effective.agentsHome }
4487
4670
  });
4671
+ service.reclaimOrphanDrafts().catch(() => void 0);
4488
4672
  const facts = (scope, management) => management ? {
4489
4673
  sourceInstanceKey: context.sourceInstanceKey,
4490
4674
  sourceTypeId: SKILLS_SOURCE_TYPE_ID,
@@ -4542,6 +4726,7 @@ function createSkillsMemorySource(config = {}) {
4542
4726
  ...optionalText(fields.skill, "skill", 64) === void 0 ? {} : { skill: optionalText(fields.skill, "skill", 64) }
4543
4727
  });
4544
4728
  case "draft": return service.draft(root, memoryInputText(fields.id, "id", 100));
4729
+ case "draft-preferences": return service.draftPreferences();
4545
4730
  case "reviews": return service.reviews({
4546
4731
  ...typeof fields.sessionId === "string" ? { sessionId: fields.sessionId } : {},
4547
4732
  ...typeof fields.limit === "number" ? { limit: fields.limit } : {}
@@ -4629,6 +4814,9 @@ function createSkillsMemorySource(config = {}) {
4629
4814
  case "suggestion-dismiss":
4630
4815
  value = await service.dismissSuggestion(memoryInputText(fields.id, "id", 100));
4631
4816
  break;
4817
+ case "suggestion-delete":
4818
+ value = await service.deleteSuggestion(memoryInputText(fields.id, "id", 100));
4819
+ break;
4632
4820
  case "suggestion-status": {
4633
4821
  const status = fields.status;
4634
4822
  if (status !== "drafting" && status !== "pending") throw new Error("status must be drafting or pending");
@@ -4685,6 +4873,22 @@ function createSkillsMemorySource(config = {}) {
4685
4873
  case "draft-discard":
4686
4874
  value = await service.discardDraft(memoryInputText(fields.id, "id", 100));
4687
4875
  break;
4876
+ case "draft-delete":
4877
+ value = await service.deleteDraft(memoryInputText(fields.id, "id", 100));
4878
+ break;
4879
+ case "draft-preference": {
4880
+ const slot = optionalText(fields.slot, "slot", 10);
4881
+ if (slot !== void 0 && slot !== "draft" && slot !== "run") throw new Error("slot must be draft or run");
4882
+ const suggestionId = optionalText(fields.suggestionId, "suggestionId", 100);
4883
+ if (suggestionId !== void 0 && slot === "run") throw new Error("a suggestion pick cannot be a backtest pick");
4884
+ value = await service.setDraftPreference({
4885
+ ...slot === void 0 ? {} : { slot },
4886
+ ...suggestionId === void 0 ? {} : { suggestionId },
4887
+ ...optionalText(fields.skillName, "skillName", 64) === void 0 ? {} : { skillName: optionalText(fields.skillName, "skillName", 64) },
4888
+ model: draftModelInput(fields.model)
4889
+ });
4890
+ break;
4891
+ }
4688
4892
  case "run-start":
4689
4893
  value = await service.startRun({
4690
4894
  skillName: memoryInputText(fields.skillName, "skillName", 64),
@@ -1,5 +1,5 @@
1
- import type { MnemonSourceManagementClient } from 'dsh-mnemon/client';
2
- import type { BacktestRun, CatalogView, DraftView, EvalCase, MarketPreview, MarketRegistry, MarketSearchResult, MarketView, EvidencePack, ReviewLogEntry, ReviewRecordResult, RunTarget, SkillDocument, SkillDraft, SkillRootKind, SkillSuggestion, SkillSummary, SkillVersion, SkillsOverview, SuggestionStatus, SuggestionView, Trajectory, TrajectorySummary, TrashEntry, TrashRestoreInput, TrashRestorePreview } from '../contracts.ts';
1
+ import type { MnemonSourceManagementClient, TaskAgentModelCatalog } from 'dsh-mnemon/client';
2
+ import type { BacktestRun, CatalogView, DraftPreferencesView, DraftView, EvalCase, MarketPreview, MarketRegistry, MarketSearchResult, MarketView, EvidencePack, ModelRoute, ReviewLogEntry, ReviewRecordResult, RunTarget, SkillDocument, SkillDraft, SkillRootKind, SkillSuggestion, SkillSummary, SkillVersion, SkillsOverview, SuggestionStatus, SuggestionView, Trajectory, TrajectorySummary, TrashEntry, TrashRestoreInput, TrashRestorePreview } from '../contracts.ts';
3
3
  export interface CaseDraft {
4
4
  id?: string;
5
5
  skillName: string;
@@ -75,6 +75,14 @@ export interface SkillsPageClient {
75
75
  }>;
76
76
  restoreVersion(key: string, versionId: string, expectedHash: string): Promise<SkillSummary>;
77
77
  dismissSuggestion(id: string): Promise<SkillSuggestion>;
78
+ /** Delete a waiting suggestion and any draft written for it. */
79
+ deleteSuggestion(id: string): Promise<{
80
+ deleted: string;
81
+ }>;
82
+ /** Delete a draft; its suggestion goes back to waiting. */
83
+ deleteDraft(id: string): Promise<{
84
+ deleted: string;
85
+ }>;
78
86
  pinTrajectory(id: string, pinned: boolean): Promise<TrajectorySummary>;
79
87
  deleteTrajectory(id: string): Promise<{
80
88
  deleted: string;
@@ -92,6 +100,18 @@ export interface SkillsPageClient {
92
100
  discardDraft(id: string): Promise<SkillDraft>;
93
101
  /** Attach a baseline run to the draft it is compared with. */
94
102
  linkRun(draftId: string, runId: string): Promise<void>;
103
+ /** The model each piece of model work follows: the two global picks and their per-item overrides. */
104
+ draftPreferences(): Promise<DraftPreferencesView>;
105
+ /** Pick the model for drafts, for backtests, or for one suggestion or skill; `null` follows the task Agent again. */
106
+ saveDraftModel(model: ModelRoute | null): Promise<DraftPreferencesView>;
107
+ saveSuggestionModel(id: string, model: ModelRoute | null): Promise<DraftPreferencesView>;
108
+ saveRunModel(model: ModelRoute | null): Promise<DraftPreferencesView>;
109
+ saveSkillRunModel(skillName: string, model: ModelRoute | null): Promise<DraftPreferencesView>;
110
+ /**
111
+ * The routes and model directory a page may offer. Rejects when the Host
112
+ * offers none, which is also when the page hides its pickers.
113
+ */
114
+ models(): Promise<TaskAgentModelCatalog>;
95
115
  market(): Promise<MarketView>;
96
116
  marketPreview(registryId: string, name: string): Promise<MarketPreview>;
97
117
  /** Search the search sources; the page filters the listed ones itself. */
@@ -118,6 +138,8 @@ export interface SkillsPageClient {
118
138
  skill?: string;
119
139
  guidance?: string;
120
140
  targetRoot?: SkillRootKind;
141
+ provider?: string;
142
+ model?: string;
121
143
  }): Promise<{
122
144
  started: true;
123
145
  }>;
@@ -125,6 +147,8 @@ export interface SkillsPageClient {
125
147
  skillName: string;
126
148
  target: RunTarget;
127
149
  caseIds?: string[];
150
+ provider?: string;
151
+ model?: string;
128
152
  }): Promise<{
129
153
  runId: string;
130
154
  }>;
@@ -0,0 +1,45 @@
1
+ import type { FieldOption, TaskAgentModelCatalog } from 'dsh-mnemon/client';
2
+ import type { DraftPreferencesView, ModelRoute } from '../contracts.ts';
3
+ import { type SkillsTranslate } from './presentation.ts';
4
+ import type { SkillsPageContext } from './ui.tsx';
5
+ /**
6
+ * The model pickers the Evolution and Evaluation pages share: which model
7
+ * drafts and backtests run on, globally and per item, offered from the same
8
+ * directory the task Agent resolves its own route against.
9
+ *
10
+ * A pick is stored as `provider|model`, so one menu value names one route, and
11
+ * an absent pick is `auto`: the page follows the task Agent, as it did before
12
+ * the user picked anything.
13
+ */
14
+ /** The menu value of a pick; `auto` asks for no pick at all. */
15
+ export declare function modelOptionValue(model: ModelRoute | null | undefined): string;
16
+ /** The pick a menu value names; `auto` and a malformed value both pick nothing. */
17
+ export declare function modelOptionPick(value: string): ModelRoute | null;
18
+ /**
19
+ * Follow-default first, then every Provider's models. The first entry exists
20
+ * before the directory arrives, so the toolbar holds its place instead of
21
+ * appearing a round trip later than the rest of the page.
22
+ */
23
+ export declare function modelOptionsFrom(models: TaskAgentModelCatalog | null, t: SkillsTranslate, autoKey: 'tests.modelAuto' | 'evolution.modelAuto'): FieldOption<string>[];
24
+ /** The same list for one card: follow-default, then the directory without its own default entry. */
25
+ export declare function modelCardOptionsFrom(modelOptions: readonly FieldOption<string>[], t: SkillsTranslate, autoKey: 'tests.modelAuto' | 'evolution.modelAuto'): FieldOption<string>[];
26
+ /** What the pickers read and write, and the directory behind them. */
27
+ export interface ModelChoice {
28
+ /** `null` until the directory arrives; the pickers are disabled meanwhile. */
29
+ models: TaskAgentModelCatalog | null;
30
+ preferences: DraftPreferencesView;
31
+ /** Every routable model, follow-default first. */
32
+ options: FieldOption<string>[];
33
+ /** The same for one card's own pick. */
34
+ cardOptions: FieldOption<string>[];
35
+ saveDraftModel(model: ModelRoute | null): void;
36
+ saveSuggestionModel(id: string, model: ModelRoute | null): void;
37
+ saveRunModel(model: ModelRoute | null): void;
38
+ saveSkillRunModel(skillName: string, model: ModelRoute | null): void;
39
+ }
40
+ /**
41
+ * Read the directory and the stored picks alongside the page's own data, and
42
+ * write one pick at a time. A write shows locally first so the menu answers at
43
+ * once, then takes the Host's answer, which is also what a second window sees.
44
+ */
45
+ export declare function useModelChoice(context: SkillsPageContext, autoKey: 'tests.modelAuto' | 'evolution.modelAuto'): ModelChoice;
@@ -1,5 +1,5 @@
1
1
  import { type JSX } from 'react';
2
- import type { BacktestRun } from '../contracts.ts';
2
+ import type { BacktestRun, ModelRoute } from '../contracts.ts';
3
3
  import type { SkillsPageContext } from './ui.tsx';
4
4
  /**
5
5
  * A skill's test cases and its backtests: the cases with a switch each, one
@@ -9,6 +9,7 @@ import type { SkillsPageContext } from './ui.tsx';
9
9
  export declare function SkillTests(props: {
10
10
  context: SkillsPageContext;
11
11
  skillName: string;
12
+ runModel?: ModelRoute | null;
12
13
  }): JSX.Element;
13
14
  /** One run's cases: the verdict, each check with the judge's note, and what the simulated run did and answered. */
14
15
  export declare function RunDetail(props: {
@@ -221,6 +221,30 @@ export interface RunTarget {
221
221
  draftId?: string;
222
222
  versionId?: string;
223
223
  }
224
+ /** One Provider and model, as a user picked it; never carries a reasoning effort. */
225
+ export interface ModelRoute {
226
+ provider: string;
227
+ model: string;
228
+ }
229
+ /**
230
+ * The models the user picked for skill work, kept beside the ledger. Every
231
+ * entry is optional: absent means "follow the task Agent's own route".
232
+ * `suggestionModels` is keyed by suggestion id, `skillRunModels` by skill name.
233
+ */
234
+ export interface DraftPreferences {
235
+ version: 1;
236
+ draftModel?: ModelRoute;
237
+ runModel?: ModelRoute;
238
+ suggestionModels: Record<string, ModelRoute>;
239
+ skillRunModels: Record<string, ModelRoute>;
240
+ }
241
+ /** What the pickers read: the stored picks, named the way the pages use them. */
242
+ export interface DraftPreferencesView {
243
+ model?: ModelRoute;
244
+ runModel?: ModelRoute;
245
+ suggestionModels: Record<string, ModelRoute>;
246
+ skillRunModels: Record<string, ModelRoute>;
247
+ }
224
248
  export interface CaseResult {
225
249
  caseId: string;
226
250
  title: string;
@@ -280,6 +304,10 @@ export interface ReviewLogEntry {
280
304
  }
281
305
  /** How the Source admits a review's suggestions; the Host sends its settings with each review. */
282
306
  export interface SkillsPolicy {
307
+ /** Whether a review may propose new skills; omitted by older Hosts, default true. */
308
+ allowCreate?: boolean;
309
+ /** Whether a review may improve existing skills; omitted by older Hosts, default true. */
310
+ allowUpdate?: boolean;
283
311
  minConfidence: number;
284
312
  expiryDays: number;
285
313
  maxPendingPerSession: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mnemon-source-skills",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Skills Source for Composable View Memory: DSH skill management, evolution suggestions, evidence trajectories and eval sets.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@deepseek-ai/cordis": "^4.0.1",
56
- "dsh-mnemon": "^0.5.33",
56
+ "dsh-mnemon": "^0.5.37",
57
57
  "react": "^18.2.0"
58
58
  },
59
59
  "devDependencies": {
@@ -62,7 +62,7 @@
62
62
  "@types/node": "^22.0.0",
63
63
  "@types/react": "^18.3.0",
64
64
  "@types/react-dom": "^18.3.0",
65
- "dsh-mnemon": "^0.5.35",
65
+ "dsh-mnemon": "^0.5.37",
66
66
  "jsdom": "^26.0.0",
67
67
  "react": "^18.2.0",
68
68
  "react-dom": "^18.2.0",
@@ -335,7 +335,17 @@
335
335
  "trash.original": "原位置",
336
336
  "trash.chooseHint": "可选择恢复位置或改名。同名替换需要明确确认,现有技能会保留在回收站。",
337
337
  "trash.replace": "替换同名技能,并将现有版本保留在回收站",
338
- "trash.confirmName": "输入 {name} 确认彻底删除"
338
+ "trash.confirmName": "输入 {name} 确认彻底删除",
339
+ "evolution.model": "全局模型",
340
+ "evolution.modelCard": "所选草案模型",
341
+ "evolution.modelAuto": "跟随默认",
342
+ "evolution.modelAutoHint": "使用当前任务 Agent 的模型",
343
+ "evolution.deleteTitle": "删除这条建议?",
344
+ "evolution.deleteBody": "它生成的草案和证据引用会一起删掉,无法恢复。",
345
+ "evolution.deleted": "已删除",
346
+ "tests.model": "全局模型",
347
+ "tests.modelCard": "所选回测模型",
348
+ "tests.modelAuto": "跟随默认"
339
349
  },
340
350
  "en": {
341
351
  "page.title": "Skills",
@@ -673,6 +683,16 @@
673
683
  "trash.original": "Original location",
674
684
  "trash.chooseHint": "Choose a location or a different name. Replacing a skill requires confirmation and keeps its current version in the Trash.",
675
685
  "trash.replace": "Replace the existing skill and keep its current version in the Trash",
676
- "trash.confirmName": "Type {name} to confirm permanent deletion"
686
+ "trash.confirmName": "Type {name} to confirm permanent deletion",
687
+ "evolution.model": "Global model",
688
+ "evolution.modelCard": "Selected model",
689
+ "evolution.modelAuto": "Follow default",
690
+ "evolution.modelAutoHint": "Use the model of the current task Agent",
691
+ "evolution.deleteTitle": "Delete this suggestion?",
692
+ "evolution.deleteBody": "Its draft and the evidence references it cites go with it, and this cannot be undone.",
693
+ "evolution.deleted": "Deleted",
694
+ "tests.model": "Global model",
695
+ "tests.modelCard": "Selected model",
696
+ "tests.modelAuto": "Follow default"
677
697
  }
678
698
  }
@@ -180,6 +180,12 @@
180
180
  .skList { display: flex; flex-direction: column; overflow: hidden; padding-bottom: 6px; }
181
181
  .skPane { padding: clamp(16px, 2vw, 24px); overflow: hidden; }
182
182
 
183
+ /* Keep the evolution inbox beside its detail while browsing long lists. */
184
+ .skEvolutionSplit { height: max(320px, calc(100dvh - 320px)); min-height: 0; }
185
+ .skEvolutionSplit > .skList,
186
+ .skEvolutionSplit > .skPane { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; }
187
+ .skEvolutionSplit > .skList > * { flex-shrink: 0; }
188
+
183
189
  .skListBar { display: flex; align-items: center; gap: 8px; padding: 10px; border-bottom: .5px solid var(--mn-line); }
184
190
  .skListBar > :first-child { min-width: 0; flex: 1; }
185
191
  .skListNote { margin: 0; padding: 18px 16px; color: var(--mn-faint); font-size: 12px; line-height: 18px; text-align: center; }
@@ -574,6 +580,8 @@
574
580
  @media (max-width: 860px) {
575
581
  .skSplit { grid-template-columns: minmax(0, 1fr); }
576
582
  .skList { max-height: 360px; overflow-y: auto; }
583
+ .skEvolutionSplit { height: auto; }
584
+ .skEvolutionSplit > .skPane { overflow: visible; overscroll-behavior-y: auto; }
577
585
  }
578
586
 
579
587
  @media (max-width: 560px) {