@sellable/install 0.1.787 → 0.1.790

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.
Files changed (30) hide show
  1. package/container/Dockerfile +7 -7
  2. package/container/README.md +1 -1
  3. package/lib/sellable-agent/default-profile-bundles/shared/manifest.fragment.json +5 -5
  4. package/lib/sellable-agent/default-profile-bundles/shared/skills/sellable/sellable-campaign-daily-results/SKILL.md +78 -40
  5. package/lib/sellable-agent/default-profile-bundles/shared/skills/sellable/sellable-campaign-daily-review/SKILL.md +83 -17
  6. package/lib/sellable-agent/default-profile-bundles/shared/skills/sellable/sellable-campaign-weekly-review/SKILL.md +69 -48
  7. package/lib/sellable-agent/default-profile-bundles/shared/skills/sellable/sellable-create-campaign/SKILL.md +2 -0
  8. package/lib/sellable-agent/default-profile-bundles/shared/skills/sellable/sellable-refill-sends/SKILL.md +21 -0
  9. package/lib/sellable-agent/external-runtime-builder.mjs +1 -1
  10. package/lib/sellable-agent/fly-admin-image/Dockerfile +1 -1
  11. package/lib/sellable-agent/fly-customer-image/Dockerfile +4 -4
  12. package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +28 -2
  13. package/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs +75 -1
  14. package/lib/sellable-agent/hermes-memory-snapshot.mjs +9 -7
  15. package/lib/sellable-agent/host-bootstrap.mjs +2 -2
  16. package/lib/sellable-agent/host-worker.mjs +2 -2
  17. package/lib/sellable-agent/profile-materializer.mjs +2 -2
  18. package/lib/sellable-agent/provisioning-adapter.mjs +2 -2
  19. package/lib/sellable-agent/public-skills.json +9 -9
  20. package/package.json +1 -1
  21. package/skill-templates/campaign-daily-results.md +78 -40
  22. package/skill-templates/campaign-daily-review.md +83 -17
  23. package/skill-templates/campaign-weekly-review.md +69 -48
  24. package/skill-templates/create-ab-test.md +21 -0
  25. package/skill-templates/create-campaign.md +2 -0
  26. package/skill-templates/create-evergreen-campaigns.md +21 -0
  27. package/skill-templates/find-leads.md +21 -0
  28. package/skill-templates/first-campaign-onboarding.md +21 -0
  29. package/skill-templates/refill-sends.md +21 -0
  30. package/skill-templates/refresh-sender-engagement.md +21 -0
@@ -200,6 +200,39 @@ function canonicalHistory(value) {
200
200
  return result;
201
201
  }
202
202
 
203
+ // The models a caller may name for one endpoint run. The server checks the
204
+ // same list before it hands the run over; the runtime checks it again here so
205
+ // a run can never reach Hermes with a model that was not reviewed.
206
+ export const ENDPOINT_MODEL_SELECTION_ALLOWLIST = Object.freeze({
207
+ openrouter: Object.freeze([
208
+ "anthropic/claude-opus-5.5",
209
+ "anthropic/claude-sonnet-5.5",
210
+ ]),
211
+ });
212
+ const ENDPOINT_MODEL_REASONING_EFFORTS = Object.freeze([
213
+ "low",
214
+ "medium",
215
+ "high",
216
+ "xhigh",
217
+ ]);
218
+
219
+ export function validateEndpointModelSelection(value) {
220
+ if (
221
+ !exactObject(value) ||
222
+ Object.keys(value).sort().join(",") !== "model,provider,reasoningEffort" ||
223
+ !Object.hasOwn(ENDPOINT_MODEL_SELECTION_ALLOWLIST, value.provider) ||
224
+ !ENDPOINT_MODEL_SELECTION_ALLOWLIST[value.provider].includes(value.model) ||
225
+ !ENDPOINT_MODEL_REASONING_EFFORTS.includes(value.reasoningEffort)
226
+ ) {
227
+ throw new Error("hermes_runs_model_selection_rejected");
228
+ }
229
+ return Object.freeze({
230
+ provider: value.provider,
231
+ model: value.model,
232
+ reasoningEffort: value.reasoningEffort,
233
+ });
234
+ }
235
+
203
236
  export function validateOpenerOutputContract(value) {
204
237
  const schema = value?.schema;
205
238
  if (
@@ -232,7 +265,7 @@ function compileCreateBody(input, openerStage = false) {
232
265
  "input",
233
266
  "instructions",
234
267
  "correlation",
235
- ...(openerStage ? ["outputContract"] : []),
268
+ ...(openerStage ? ["outputContract"] : ["modelSelection"]),
236
269
  ];
237
270
  if (Object.keys(input).some((key) => !allowed.includes(key))) {
238
271
  throw new Error("hermes_runs_submit_rejected");
@@ -254,11 +287,24 @@ function compileCreateBody(input, openerStage = false) {
254
287
  if (openerStage && history.length !== 0) {
255
288
  throw new Error("hermes_opener_stage_history_rejected");
256
289
  }
290
+ const modelSelection =
291
+ input.modelSelection === undefined
292
+ ? null
293
+ : validateEndpointModelSelection(input.modelSelection);
257
294
  return Object.freeze({
258
295
  input: compiledInput,
259
296
  instructions,
260
297
  conversation_history: history,
261
298
  session_id: `${openerStage ? "sellable_opener_stage" : "sellable_endpoint"}_${sha256(correlation).slice(0, 32)}`,
299
+ // Hermes 0.21.5 honours a per-run provider and model on /v1/runs and
300
+ // resolves the provider's credential from the machine's own pool.
301
+ ...(modelSelection === null
302
+ ? {}
303
+ : {
304
+ provider: modelSelection.provider,
305
+ model: modelSelection.model,
306
+ model_options: { reasoning_effort: modelSelection.reasoningEffort },
307
+ }),
262
308
  ...(input.outputContract === undefined
263
309
  ? {}
264
310
  : {
@@ -287,6 +333,30 @@ async function readBounded(response, maximum = MAX_RESPONSE_BYTES) {
287
333
  }
288
334
  }
289
335
 
336
+ const SERVED_RUNTIME_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
337
+
338
+ // The provider and model that actually served a run (a fallback can differ
339
+ // from the request) and its token counts, so cost and model are auditable.
340
+ function servedRuntime(value) {
341
+ return exactObject(value) &&
342
+ SERVED_RUNTIME_ID.test(value.provider ?? "") &&
343
+ SERVED_RUNTIME_ID.test(value.model ?? "")
344
+ ? Object.freeze({ provider: value.provider, model: value.model })
345
+ : null;
346
+ }
347
+
348
+ function runUsage(value) {
349
+ if (!exactObject(value)) return null;
350
+ const count = (key) =>
351
+ Number.isSafeInteger(value[key]) && value[key] >= 0 ? value[key] : null;
352
+ return Object.freeze({
353
+ inputTokens: count("input_tokens"),
354
+ outputTokens: count("output_tokens"),
355
+ cacheReadTokens: count("cache_read_tokens"),
356
+ cacheWriteTokens: count("cache_write_tokens"),
357
+ });
358
+ }
359
+
290
360
  function ensureRunId(value) {
291
361
  if (!RUN_ID.test(value ?? "")) throw new Error("hermes_runs_run_id_rejected");
292
362
  return value;
@@ -340,6 +410,8 @@ function nativeStatus(value, expectedRunId) {
340
410
  classification: "COMPLETED",
341
411
  output,
342
412
  outputDigest: sha256(stableJson(output)),
413
+ servedRuntime: servedRuntime(value.runtime),
414
+ usage: runUsage(value.usage),
343
415
  });
344
416
  }
345
417
  if (status === "failed") {
@@ -439,6 +511,8 @@ export function sanitizeHermesRunEvent(value, options = {}) {
439
511
  output: value.output,
440
512
  outputDigest: sha256(stableJson(value.output)),
441
513
  usageDigest: sha256(stableJson(value.usage)),
514
+ servedRuntime: servedRuntime(value.runtime),
515
+ usage: runUsage(value.usage),
442
516
  });
443
517
  }
444
518
  if (value.event === "run.failed") {
@@ -79,15 +79,17 @@ export const HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL =
79
79
  const HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL_GROUP = "sellable-defaults";
80
80
  export const HERMES_CAMPAIGN_DAILY_REVIEW_SKILL =
81
81
  "sellable-campaign-daily-review";
82
- // Each job wakes only on weekdays (weekly: Mondays) in a short window after its Los
83
- // Angeles report time (review 08:00, results and weekly 09:00). The UTC hours cover
84
- // both PDT and PST; the product's due check ignores early ticks and finished
85
- // periods, so the remaining ticks are the retries.
82
+ // Each job ticks every hour of every day so that a customer in any time zone is
83
+ // reached. The product's due check decides: it wakes the agent only on the
84
+ // customer's local working days (weekly: Mondays), from the job's report time
85
+ // (review 08:00, results and weekly 09:00) for four hours, and never for a
86
+ // finished period, so the later ticks in that window are the retries. Every
87
+ // other tick is a cheap "not due" answer and the agent stays asleep.
86
88
  export const HERMES_CUSTOMER_PROGRAM_CRONS = Object.freeze([
87
89
  Object.freeze({
88
90
  jobKey: "campaign_daily_review",
89
91
  name: "sellable-default-campaign-daily-review",
90
- schedule: "0 15-17 * * 1-5",
92
+ schedule: "0 * * * *",
91
93
  script: "customer-program-due-campaign-daily-review.py",
92
94
  skills: Object.freeze([
93
95
  HERMES_CAMPAIGN_DAILY_REVIEW_SKILL,
@@ -106,7 +108,7 @@ export const HERMES_CUSTOMER_PROGRAM_CRONS = Object.freeze([
106
108
  Object.freeze({
107
109
  jobKey: "campaign_daily_results",
108
110
  name: "sellable-default-campaign-daily-results",
109
- schedule: "0 16-18 * * 1-5",
111
+ schedule: "0 * * * *",
110
112
  script: "customer-program-due-campaign-daily-results.py",
111
113
  skills: Object.freeze(["sellable-campaign-daily-results"]),
112
114
  prompt: Object.freeze([
@@ -119,7 +121,7 @@ export const HERMES_CUSTOMER_PROGRAM_CRONS = Object.freeze([
119
121
  Object.freeze({
120
122
  jobKey: "campaign_weekly_review",
121
123
  name: "sellable-default-campaign-weekly-review",
122
- schedule: "0 16-18 * * 1",
124
+ schedule: "0 * * * *",
123
125
  script: "customer-program-due-campaign-weekly-review.py",
124
126
  skills: Object.freeze([
125
127
  "sellable-campaign-weekly-review",
@@ -38,8 +38,8 @@ import { installAgentHostServices } from "./service-installer.mjs";
38
38
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
39
39
  const VERSION = "sellable-agent-host-bootstrap/v1";
40
40
  const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
41
- const INSTALLER_PACKAGE = "@sellable/install@0.1.787";
42
- const MCP_PACKAGE = "@sellable/mcp@0.1.953";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.790";
42
+ const MCP_PACKAGE = "@sellable/mcp@0.1.955";
43
43
  const HERMES_VERSION = "0.18.0";
44
44
  const SHA256 = /^[a-f0-9]{64}$/;
45
45
  const SHA256_16 = /^[a-f0-9]{16}$/;
@@ -1546,8 +1546,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1546
1546
  ]) ||
1547
1547
  pinned.hermesCli !== "hermes" ||
1548
1548
  pinned.hermesVersion !== "0.18.0" ||
1549
- pinned.installerPackage !== "@sellable/install@0.1.787" ||
1550
- pinned.mcpPackage !== "@sellable/mcp@0.1.953" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.790" ||
1550
+ pinned.mcpPackage !== "@sellable/mcp@0.1.955" ||
1551
1551
  !Array.isArray(policy.toolInclude) ||
1552
1552
  policy.toolInclude.length === 0 ||
1553
1553
  !Number.isSafeInteger(slack.generation) ||
@@ -312,8 +312,8 @@ function validateDesired(desired) {
312
312
  desired.serviceCredentialGeneration < 1 ||
313
313
  desired.hermesCli !== "hermes" ||
314
314
  desired.hermesVersion !== "0.18.0" ||
315
- desired.installerPackage !== "@sellable/install@0.1.787" ||
316
- desired.mcpPackage !== "@sellable/mcp@0.1.953" ||
315
+ desired.installerPackage !== "@sellable/install@0.1.790" ||
316
+ desired.mcpPackage !== "@sellable/mcp@0.1.955" ||
317
317
  !Array.isArray(desired.toolInclude) ||
318
318
  desired.toolInclude.length === 0 ||
319
319
  desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
@@ -25,8 +25,8 @@ import { fileURLToPath } from "node:url";
25
25
  import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
- export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.787";
29
- export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.953";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.790";
29
+ export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.955";
30
30
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
31
31
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
32
32
  );