switchroom 0.15.13 → 0.15.15
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/dist/agent-scheduler/index.js +312 -13
- package/dist/auth-broker/index.js +49 -3
- package/dist/cli/notion-write-pretool.mjs +49 -3
- package/dist/cli/switchroom.js +335 -107
- package/dist/host-control/main.js +49 -3
- package/dist/vault/approvals/kernel-server.js +50 -4
- package/dist/vault/broker/server.js +50 -4
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +49 -15
- package/telegram-plugin/bridge/bridge.ts +7 -1
- package/telegram-plugin/dist/bridge/bridge.js +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +108 -9
- package/telegram-plugin/dist/server.js +1 -1
- package/telegram-plugin/gateway/gateway.ts +46 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +31 -1
- package/telegram-plugin/gateway/ipc-server.ts +29 -0
- package/telegram-plugin/runtime-metrics.ts +14 -0
- package/telegram-plugin/tests/bridge-liveness-override.test.ts +21 -0
- package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +54 -0
- package/telegram-plugin/tests/runtime-metrics.test.ts +9 -0
- package/telegram-plugin/tests/send-outbound-wiring.test.ts +63 -0
|
@@ -10989,11 +10989,29 @@ var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
|
10989
10989
|
HttpDiffPollSchema,
|
|
10990
10990
|
TelegramReactionsPollSchema
|
|
10991
10991
|
]);
|
|
10992
|
+
var TelegramMessageActionSchema = exports_external.object({
|
|
10993
|
+
type: exports_external.literal("telegram-message"),
|
|
10994
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
10995
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
10996
|
+
});
|
|
10997
|
+
var WebhookActionSchema = exports_external.object({
|
|
10998
|
+
type: exports_external.literal("webhook"),
|
|
10999
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
11000
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
11001
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
11002
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
11003
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (§6.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
11004
|
+
});
|
|
11005
|
+
var ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11006
|
+
TelegramMessageActionSchema,
|
|
11007
|
+
WebhookActionSchema
|
|
11008
|
+
]);
|
|
10992
11009
|
var ScheduleEntrySchema = exports_external.object({
|
|
10993
11010
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
10994
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
10995
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
11011
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
11012
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
10996
11013
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11014
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
10997
11015
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
10998
11016
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' → a minimal-" + "context cheap cron session (Tier 1). 'agent' → the agent's live " + "session with full persona/memory (Tier 2). Unset → inferred from " + "`model` (cheap→fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
10999
11017
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -11010,13 +11028,41 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
11010
11028
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11011
11029
|
});
|
|
11012
11030
|
}
|
|
11013
|
-
if (kind
|
|
11031
|
+
if (kind !== "poll" && entry.poll) {
|
|
11014
11032
|
ctx.addIssue({
|
|
11015
11033
|
code: exports_external.ZodIssueCode.custom,
|
|
11016
11034
|
path: ["poll"],
|
|
11017
11035
|
message: "`poll` is only valid when kind: poll."
|
|
11018
11036
|
});
|
|
11019
11037
|
}
|
|
11038
|
+
if (kind === "action" && !entry.action) {
|
|
11039
|
+
ctx.addIssue({
|
|
11040
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11041
|
+
path: ["action"],
|
|
11042
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
11043
|
+
});
|
|
11044
|
+
}
|
|
11045
|
+
if (kind !== "action" && entry.action) {
|
|
11046
|
+
ctx.addIssue({
|
|
11047
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11048
|
+
path: ["action"],
|
|
11049
|
+
message: "`action` is only valid when kind: action."
|
|
11050
|
+
});
|
|
11051
|
+
}
|
|
11052
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
11053
|
+
ctx.addIssue({
|
|
11054
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11055
|
+
path: ["prompt"],
|
|
11056
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
11057
|
+
});
|
|
11058
|
+
}
|
|
11059
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
11060
|
+
ctx.addIssue({
|
|
11061
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11062
|
+
path: ["prompt"],
|
|
11063
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
11064
|
+
});
|
|
11065
|
+
}
|
|
11020
11066
|
});
|
|
11021
11067
|
var AgentSoulSchema = exports_external.object({
|
|
11022
11068
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -12244,17 +12290,19 @@ function collectScheduleEntries(config) {
|
|
|
12244
12290
|
const schedule = resolved.schedule ?? [];
|
|
12245
12291
|
for (let i = 0;i < schedule.length; i++) {
|
|
12246
12292
|
const entry = schedule[i];
|
|
12293
|
+
const auditMaterial = entry.prompt ?? `action:${JSON.stringify(entry.action ?? {})}`;
|
|
12247
12294
|
out.push({
|
|
12248
12295
|
agent,
|
|
12249
12296
|
scheduleIndex: i,
|
|
12250
12297
|
cron: entry.cron,
|
|
12251
|
-
prompt: entry.prompt,
|
|
12252
|
-
promptKey: createHash("sha256").update(
|
|
12298
|
+
...entry.prompt !== undefined ? { prompt: entry.prompt } : {},
|
|
12299
|
+
promptKey: createHash("sha256").update(auditMaterial).digest("hex").slice(0, 12),
|
|
12253
12300
|
...entry.topic !== undefined ? { topic: entry.topic } : {},
|
|
12254
12301
|
...entry.kind !== undefined ? { kind: entry.kind } : {},
|
|
12255
12302
|
...entry.model !== undefined ? { model: entry.model } : {},
|
|
12256
12303
|
...entry.context !== undefined ? { context: entry.context } : {},
|
|
12257
|
-
...entry.poll !== undefined ? { poll: entry.poll } : {}
|
|
12304
|
+
...entry.poll !== undefined ? { poll: entry.poll } : {},
|
|
12305
|
+
...entry.action !== undefined ? { action: entry.action } : {}
|
|
12258
12306
|
});
|
|
12259
12307
|
}
|
|
12260
12308
|
}
|
|
@@ -12270,7 +12318,7 @@ function dispatchAsInbound(entry, options, dispatcher) {
|
|
|
12270
12318
|
user: "cron",
|
|
12271
12319
|
userId: 0,
|
|
12272
12320
|
ts,
|
|
12273
|
-
text: entry.prompt,
|
|
12321
|
+
text: entry.prompt ?? "",
|
|
12274
12322
|
meta: {
|
|
12275
12323
|
source: "cron",
|
|
12276
12324
|
schedule_index: String(entry.scheduleIndex),
|
|
@@ -12298,6 +12346,9 @@ function resolveCronModel(model) {
|
|
|
12298
12346
|
return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
|
|
12299
12347
|
}
|
|
12300
12348
|
function resolveCronRouting(input, opts) {
|
|
12349
|
+
if ((input.kind ?? "prompt") === "action") {
|
|
12350
|
+
return { tier: "action", session: null, customModelDowngrade: false };
|
|
12351
|
+
}
|
|
12301
12352
|
if (!opts.cheapCronEnabled) {
|
|
12302
12353
|
return { tier: "main", session: "main", customModelDowngrade: false };
|
|
12303
12354
|
}
|
|
@@ -12329,10 +12380,97 @@ function resolveEscalationRouting(input, opts) {
|
|
|
12329
12380
|
return resolveCronRouting({ ...input, kind: "prompt" }, opts);
|
|
12330
12381
|
}
|
|
12331
12382
|
|
|
12383
|
+
// src/scheduler/cron-cadence.ts
|
|
12384
|
+
function csvSmallestGap(field) {
|
|
12385
|
+
if (!field.includes(","))
|
|
12386
|
+
return null;
|
|
12387
|
+
const parts = field.split(",").map((s) => Number(s)).filter((n) => Number.isInteger(n) && n >= 0);
|
|
12388
|
+
if (parts.length < 2)
|
|
12389
|
+
return null;
|
|
12390
|
+
const sorted = [...parts].sort((a, b) => a - b);
|
|
12391
|
+
let smallest = Infinity;
|
|
12392
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
12393
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
12394
|
+
if (gap > 0 && gap < smallest)
|
|
12395
|
+
smallest = gap;
|
|
12396
|
+
}
|
|
12397
|
+
return Number.isFinite(smallest) ? smallest : null;
|
|
12398
|
+
}
|
|
12399
|
+
function estimateCronGapMin(expr) {
|
|
12400
|
+
const fields = expr.trim().split(/\s+/);
|
|
12401
|
+
if (fields.length < 5)
|
|
12402
|
+
return Infinity;
|
|
12403
|
+
const [min, hour] = fields;
|
|
12404
|
+
if (min === "*")
|
|
12405
|
+
return 1;
|
|
12406
|
+
const minStep = min.match(/^\*\/(\d+)$/);
|
|
12407
|
+
if (minStep) {
|
|
12408
|
+
const n = Number(minStep[1]);
|
|
12409
|
+
return n > 0 ? n : Infinity;
|
|
12410
|
+
}
|
|
12411
|
+
const minCsv = csvSmallestGap(min);
|
|
12412
|
+
if (minCsv !== null)
|
|
12413
|
+
return minCsv;
|
|
12414
|
+
if (!/^\d+$/.test(min))
|
|
12415
|
+
return Infinity;
|
|
12416
|
+
if (hour === "*")
|
|
12417
|
+
return 60;
|
|
12418
|
+
const hourStep = hour.match(/^\*\/(\d+)$/);
|
|
12419
|
+
if (hourStep) {
|
|
12420
|
+
const n = Number(hourStep[1]);
|
|
12421
|
+
return n > 0 ? n * 60 : Infinity;
|
|
12422
|
+
}
|
|
12423
|
+
const hourCsv = csvSmallestGap(hour);
|
|
12424
|
+
if (hourCsv !== null)
|
|
12425
|
+
return hourCsv * 60;
|
|
12426
|
+
if (/^\d+$/.test(hour))
|
|
12427
|
+
return 1440;
|
|
12428
|
+
return Infinity;
|
|
12429
|
+
}
|
|
12430
|
+
|
|
12332
12431
|
// src/scheduler/tier-selector.ts
|
|
12333
12432
|
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
12334
|
-
function
|
|
12335
|
-
|
|
12433
|
+
function resolveCronAutoTier(env = process.env) {
|
|
12434
|
+
const v = (env.SWITCHROOM_CRON_AUTO_TIER ?? "").toLowerCase();
|
|
12435
|
+
return v === "1" || v === "true" || v === "on";
|
|
12436
|
+
}
|
|
12437
|
+
function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
12438
|
+
if (input.kind === "action") {
|
|
12439
|
+
return { tier: "action", source: "explicit", reason: "declared kind: action (model-free verb)" };
|
|
12440
|
+
}
|
|
12441
|
+
if (input.kind === "poll") {
|
|
12442
|
+
return { tier: "poll", source: "explicit", reason: "declared kind: poll (model-free check)" };
|
|
12443
|
+
}
|
|
12444
|
+
if (input.context === "fresh") {
|
|
12445
|
+
return { tier: "cheap", source: "explicit", reason: "declared context: fresh (cheap cron session)" };
|
|
12446
|
+
}
|
|
12447
|
+
if (input.context === "agent") {
|
|
12448
|
+
return { tier: "main", source: "explicit", reason: "declared context: agent (full live session)" };
|
|
12449
|
+
}
|
|
12450
|
+
if (input.model !== undefined) {
|
|
12451
|
+
return isKnownCheapModel(input.model) ? { tier: "cheap", source: "explicit", reason: `cheap model '${input.model}' → cheap cron session` } : { tier: "main", source: "explicit", reason: `model '${input.model}' is not a known-cheap id → full live session` };
|
|
12452
|
+
}
|
|
12453
|
+
if (input.smallestGapMin <= frequentGapMin) {
|
|
12454
|
+
return {
|
|
12455
|
+
tier: "cheap",
|
|
12456
|
+
source: "cadence-default",
|
|
12457
|
+
reason: `fires every ~${input.smallestGapMin}min (≤ ${frequentGapMin}min) — defaulting to a cheap ` + `session; set context: agent (or an Opus/custom model) if this needs the agent's full context`
|
|
12458
|
+
};
|
|
12459
|
+
}
|
|
12460
|
+
return {
|
|
12461
|
+
tier: "main",
|
|
12462
|
+
source: "cadence-default",
|
|
12463
|
+
reason: `fires every ~${input.smallestGapMin}min (> ${frequentGapMin}min) — defaulting to the agent's ` + `full session; set model: sonnet (or context: fresh) to run it cheaply`
|
|
12464
|
+
};
|
|
12465
|
+
}
|
|
12466
|
+
function applyDefaultTier(entry, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN, autoTierEnabled = resolveCronAutoTier()) {
|
|
12467
|
+
if (!autoTierEnabled)
|
|
12468
|
+
return entry;
|
|
12469
|
+
if (entry.kind !== undefined || entry.context !== undefined || entry.model !== undefined) {
|
|
12470
|
+
return entry;
|
|
12471
|
+
}
|
|
12472
|
+
const rec = recommendCronTier({ smallestGapMin: estimateCronGapMin(entry.cron) }, frequentGapMin);
|
|
12473
|
+
return rec.tier === "cheap" ? { ...entry, context: "fresh" } : entry;
|
|
12336
12474
|
}
|
|
12337
12475
|
|
|
12338
12476
|
// src/agent-scheduler/cheap-cron-wiring.ts
|
|
@@ -12549,6 +12687,99 @@ async function readCapped(res, maxBytes) {
|
|
|
12549
12687
|
return new TextDecoder().decode(buf);
|
|
12550
12688
|
}
|
|
12551
12689
|
|
|
12690
|
+
// src/scheduler/action-engine.ts
|
|
12691
|
+
var PLACEHOLDER_RE = /\{\{\s*(date|time|agent)\s*\}\}/g;
|
|
12692
|
+
function substituteDeterministic(text, ctx) {
|
|
12693
|
+
const d = new Date(ctx.now);
|
|
12694
|
+
const date = d.toISOString().slice(0, 10);
|
|
12695
|
+
const time = d.toISOString().slice(11, 16);
|
|
12696
|
+
return text.replace(PLACEHOLDER_RE, (_, name) => {
|
|
12697
|
+
if (name === "date")
|
|
12698
|
+
return date;
|
|
12699
|
+
if (name === "time")
|
|
12700
|
+
return time;
|
|
12701
|
+
return ctx.agent;
|
|
12702
|
+
});
|
|
12703
|
+
}
|
|
12704
|
+
function substituteSecrets2(s, secretValues) {
|
|
12705
|
+
return s.replace(/\{\{([a-zA-Z0-9_\-/]+)\}\}/g, (_, name) => secretValues[name] ?? "");
|
|
12706
|
+
}
|
|
12707
|
+
async function runTelegramMessage(spec, deps) {
|
|
12708
|
+
const text = substituteDeterministic(spec.text, { now: deps.now(), agent: deps.agent });
|
|
12709
|
+
let delivered;
|
|
12710
|
+
try {
|
|
12711
|
+
delivered = await deps.sendMessage(text, { parseMode: spec.parse_mode });
|
|
12712
|
+
} catch (e) {
|
|
12713
|
+
return { ok: false, summary: "", error: `send failed: ${e.message}` };
|
|
12714
|
+
}
|
|
12715
|
+
return delivered ? { ok: true, summary: `message sent (${text.length} chars)` } : { ok: false, summary: "", error: "message not delivered (no gateway / send rejected)" };
|
|
12716
|
+
}
|
|
12717
|
+
async function runWebhook(spec, deps) {
|
|
12718
|
+
const gate = checkEgressUrl(spec.url, deps.allow);
|
|
12719
|
+
if (!gate.ok)
|
|
12720
|
+
return { ok: false, summary: "", error: `egress denied: ${gate.reason}` };
|
|
12721
|
+
const host = normalizeHost(new URL(spec.url).hostname);
|
|
12722
|
+
for (const s of spec.secrets) {
|
|
12723
|
+
const sc = checkSecretBinding(s, host, deps.allow);
|
|
12724
|
+
if (!sc.ok)
|
|
12725
|
+
return { ok: false, summary: "", error: `egress denied: ${sc.reason}` };
|
|
12726
|
+
}
|
|
12727
|
+
try {
|
|
12728
|
+
const ip = await deps.lookup(host);
|
|
12729
|
+
if (ip && isBlockedAddress(ip))
|
|
12730
|
+
return { ok: false, summary: "", error: `resolved ip ${ip} blocked` };
|
|
12731
|
+
} catch (e) {
|
|
12732
|
+
return { ok: false, summary: "", error: `dns lookup failed: ${e.message}` };
|
|
12733
|
+
}
|
|
12734
|
+
const secretValues = {};
|
|
12735
|
+
for (const name of spec.secrets) {
|
|
12736
|
+
try {
|
|
12737
|
+
secretValues[name] = await deps.resolveSecret(name);
|
|
12738
|
+
} catch (e) {
|
|
12739
|
+
return { ok: false, summary: "", error: `secret ${name}: ${e.message}` };
|
|
12740
|
+
}
|
|
12741
|
+
}
|
|
12742
|
+
const headers = {};
|
|
12743
|
+
for (const [k, v] of Object.entries(spec.headers ?? {})) {
|
|
12744
|
+
headers[k] = substituteSecrets2(v, secretValues);
|
|
12745
|
+
}
|
|
12746
|
+
const body = spec.body !== undefined ? substituteSecrets2(spec.body, secretValues) : undefined;
|
|
12747
|
+
const controller = new AbortController;
|
|
12748
|
+
const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? 5000);
|
|
12749
|
+
try {
|
|
12750
|
+
const res = await deps.fetchImpl(spec.url, {
|
|
12751
|
+
method: spec.method,
|
|
12752
|
+
headers,
|
|
12753
|
+
...body !== undefined ? { body } : {},
|
|
12754
|
+
redirect: "error",
|
|
12755
|
+
signal: controller.signal
|
|
12756
|
+
});
|
|
12757
|
+
const buf = await res.arrayBuffer();
|
|
12758
|
+
if (buf.byteLength > (deps.maxBytes ?? 1024 * 1024)) {
|
|
12759
|
+
return { ok: false, summary: "", error: `response ${buf.byteLength}B exceeds cap` };
|
|
12760
|
+
}
|
|
12761
|
+
if (!res.ok)
|
|
12762
|
+
return { ok: false, summary: "", error: `http ${res.status}` };
|
|
12763
|
+
return { ok: true, summary: `webhook ${spec.method} ${host} → ${res.status}` };
|
|
12764
|
+
} catch (e) {
|
|
12765
|
+
return { ok: false, summary: "", error: `fetch failed: ${e.message}` };
|
|
12766
|
+
} finally {
|
|
12767
|
+
clearTimeout(timer);
|
|
12768
|
+
}
|
|
12769
|
+
}
|
|
12770
|
+
async function runAction(spec, deps) {
|
|
12771
|
+
switch (spec.type) {
|
|
12772
|
+
case "telegram-message":
|
|
12773
|
+
return runTelegramMessage(spec, deps);
|
|
12774
|
+
case "webhook":
|
|
12775
|
+
return runWebhook(spec, deps);
|
|
12776
|
+
default: {
|
|
12777
|
+
const _never = spec;
|
|
12778
|
+
return { ok: false, summary: "", error: `unknown action type: ${JSON.stringify(_never)}` };
|
|
12779
|
+
}
|
|
12780
|
+
}
|
|
12781
|
+
}
|
|
12782
|
+
|
|
12552
12783
|
// src/scheduler/poll-state.ts
|
|
12553
12784
|
import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
|
|
12554
12785
|
import { dirname } from "node:path";
|
|
@@ -13072,6 +13303,8 @@ function buildCheapCronHooks(config, env, deps = {}) {
|
|
|
13072
13303
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
13073
13304
|
const lookup = deps.lookup ?? defaultLookup;
|
|
13074
13305
|
const now = deps.now ?? Date.now;
|
|
13306
|
+
const agentName = deps.agentName ?? env.SWITCHROOM_AGENT_NAME ?? "-";
|
|
13307
|
+
const postOutbound = deps.postOutbound;
|
|
13075
13308
|
const runPoll = async (spec, prevCursor) => {
|
|
13076
13309
|
if (spec.type === "http-diff") {
|
|
13077
13310
|
return runHttpDiffPoll(spec, prevCursor, {
|
|
@@ -13084,7 +13317,18 @@ function buildCheapCronHooks(config, env, deps = {}) {
|
|
|
13084
13317
|
}
|
|
13085
13318
|
return { hit: false, baseline: false, error: `poll type '${spec.type}' not yet wired (staged)` };
|
|
13086
13319
|
};
|
|
13087
|
-
|
|
13320
|
+
const runAction2 = async (spec, ctx) => {
|
|
13321
|
+
return runAction(spec, {
|
|
13322
|
+
fetchImpl,
|
|
13323
|
+
resolveSecret,
|
|
13324
|
+
lookup,
|
|
13325
|
+
allow: egress,
|
|
13326
|
+
now,
|
|
13327
|
+
agent: agentName,
|
|
13328
|
+
sendMessage: async (text, opts) => postOutbound ? postOutbound({ threadId: ctx.threadId, text, parseMode: opts.parseMode }) : false
|
|
13329
|
+
});
|
|
13330
|
+
};
|
|
13331
|
+
return { enabled: true, pollState, runPoll, runAction: runAction2 };
|
|
13088
13332
|
}
|
|
13089
13333
|
|
|
13090
13334
|
// src/telegram/topic-router.ts
|
|
@@ -13876,6 +14120,17 @@ function createInjectIpcClient(options) {
|
|
|
13876
14120
|
return false;
|
|
13877
14121
|
try {
|
|
13878
14122
|
return socket.write(JSON.stringify(msg) + `
|
|
14123
|
+
`);
|
|
14124
|
+
} catch (err) {
|
|
14125
|
+
log(`scheduler ipc: write failed: ${err.message}`);
|
|
14126
|
+
return false;
|
|
14127
|
+
}
|
|
14128
|
+
},
|
|
14129
|
+
sendOutbound(msg) {
|
|
14130
|
+
if (!socket || !connected)
|
|
14131
|
+
return false;
|
|
14132
|
+
try {
|
|
14133
|
+
return socket.write(JSON.stringify(msg) + `
|
|
13879
14134
|
`);
|
|
13880
14135
|
} catch (err) {
|
|
13881
14136
|
log(`scheduler ipc: write failed: ${err.message}`);
|
|
@@ -14229,7 +14484,7 @@ function readRecentFires(jsonlPath) {
|
|
|
14229
14484
|
|
|
14230
14485
|
// src/agent-scheduler/index.ts
|
|
14231
14486
|
function templateEscalationPrompt(prompt, diff) {
|
|
14232
|
-
return prompt.replace(/\{\{\s*diff\s*\}\}/g, diff);
|
|
14487
|
+
return (prompt ?? "").replace(/\{\{\s*diff\s*\}\}/g, diff);
|
|
14233
14488
|
}
|
|
14234
14489
|
function recoverPendingEscalations(opts) {
|
|
14235
14490
|
let recovered = 0;
|
|
@@ -14374,6 +14629,28 @@ function registerAgentSchedule(opts) {
|
|
|
14374
14629
|
});
|
|
14375
14630
|
return;
|
|
14376
14631
|
}
|
|
14632
|
+
if (routing.tier === "action") {
|
|
14633
|
+
if (!opts.cheapCron?.runAction || !entry.action) {
|
|
14634
|
+
record({
|
|
14635
|
+
exitCode: -4,
|
|
14636
|
+
summary: !entry.action ? "action skipped — no action spec on entry" : "action skipped — action transport unavailable",
|
|
14637
|
+
tier: "action"
|
|
14638
|
+
});
|
|
14639
|
+
return;
|
|
14640
|
+
}
|
|
14641
|
+
let outcome;
|
|
14642
|
+
try {
|
|
14643
|
+
outcome = await opts.cheapCron.runAction(entry.action, { threadId });
|
|
14644
|
+
} catch (err) {
|
|
14645
|
+
outcome = { ok: false, summary: "", error: err.message };
|
|
14646
|
+
}
|
|
14647
|
+
record({
|
|
14648
|
+
exitCode: outcome.ok ? 0 : -4,
|
|
14649
|
+
summary: outcome.ok ? `action ok: ${outcome.summary}` : `action error: ${outcome.error}`,
|
|
14650
|
+
tier: "action"
|
|
14651
|
+
});
|
|
14652
|
+
return;
|
|
14653
|
+
}
|
|
14377
14654
|
let delivered = false;
|
|
14378
14655
|
let summary = "";
|
|
14379
14656
|
try {
|
|
@@ -14547,6 +14824,19 @@ async function main() {
|
|
|
14547
14824
|
const cheapEnabledForReplay = isCheapCronEnabled(process.env);
|
|
14548
14825
|
for (const m of missed) {
|
|
14549
14826
|
const startedAt = Date.now();
|
|
14827
|
+
if (m.entry.kind === "action") {
|
|
14828
|
+
sink.recordFire({
|
|
14829
|
+
agent: m.entry.agent,
|
|
14830
|
+
scheduleIndex: m.entry.scheduleIndex,
|
|
14831
|
+
promptKey: m.entry.promptKey,
|
|
14832
|
+
exitCode: 0,
|
|
14833
|
+
outputSummary: "action replay skipped — runs on next tick (never double-fired)",
|
|
14834
|
+
startedAt,
|
|
14835
|
+
finishedAt: Date.now(),
|
|
14836
|
+
tier: "action"
|
|
14837
|
+
});
|
|
14838
|
+
continue;
|
|
14839
|
+
}
|
|
14550
14840
|
if (cheapEnabledForReplay && m.entry.kind === "poll") {
|
|
14551
14841
|
sink.recordFire({
|
|
14552
14842
|
agent: m.entry.agent,
|
|
@@ -14577,7 +14867,8 @@ async function main() {
|
|
|
14577
14867
|
process.stdout.write(`agent-scheduler: ${staleSkipped.length} scheduled run(s) ` + `skipped (older than ${windowMinutes}min window) — notifying user
|
|
14578
14868
|
`);
|
|
14579
14869
|
const lines = staleSkipped.map((s) => {
|
|
14580
|
-
const
|
|
14870
|
+
const label = s.entry.prompt ?? `action: ${s.entry.action?.type ?? "?"}`;
|
|
14871
|
+
const oneLine = label.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
14581
14872
|
return `- "${oneLine}" — cron \`${s.entry.cron}\`, ` + `most recent missed run ~${new Date(s.expectedFireMs).toISOString()}`;
|
|
14582
14873
|
});
|
|
14583
14874
|
const noticeText = "[switchroom scheduler notice] While this agent was offline, the " + "following scheduled task(s) had at least one run skipped. They were " + `older than the ${windowMinutes}-minute catch-up window, so they ` + `will NOT be re-run:
|
|
@@ -14633,7 +14924,15 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
14633
14924
|
await client.close().catch(() => {});
|
|
14634
14925
|
}
|
|
14635
14926
|
} : undefined;
|
|
14636
|
-
const
|
|
14927
|
+
const postOutbound = (args) => ipcClient.sendOutbound({
|
|
14928
|
+
type: "send_outbound",
|
|
14929
|
+
agentName,
|
|
14930
|
+
chatId: channel.chatId,
|
|
14931
|
+
...args.threadId != null ? { threadId: args.threadId } : {},
|
|
14932
|
+
text: args.text,
|
|
14933
|
+
parseMode: args.parseMode
|
|
14934
|
+
});
|
|
14935
|
+
const cheapCron = buildCheapCronHooks(config, process.env, { agentName, postOutbound });
|
|
14637
14936
|
if (cheapCron) {
|
|
14638
14937
|
const recovered = recoverPendingEscalations({
|
|
14639
14938
|
entries,
|
|
@@ -10989,11 +10989,29 @@ var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
|
10989
10989
|
HttpDiffPollSchema,
|
|
10990
10990
|
TelegramReactionsPollSchema
|
|
10991
10991
|
]);
|
|
10992
|
+
var TelegramMessageActionSchema = exports_external.object({
|
|
10993
|
+
type: exports_external.literal("telegram-message"),
|
|
10994
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
10995
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
10996
|
+
});
|
|
10997
|
+
var WebhookActionSchema = exports_external.object({
|
|
10998
|
+
type: exports_external.literal("webhook"),
|
|
10999
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
11000
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
11001
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
11002
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
11003
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (§6.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
11004
|
+
});
|
|
11005
|
+
var ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11006
|
+
TelegramMessageActionSchema,
|
|
11007
|
+
WebhookActionSchema
|
|
11008
|
+
]);
|
|
10992
11009
|
var ScheduleEntrySchema = exports_external.object({
|
|
10993
11010
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
10994
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
10995
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
11011
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
11012
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
10996
11013
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11014
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
10997
11015
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
10998
11016
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' → a minimal-" + "context cheap cron session (Tier 1). 'agent' → the agent's live " + "session with full persona/memory (Tier 2). Unset → inferred from " + "`model` (cheap→fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
10999
11017
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -11010,13 +11028,41 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
11010
11028
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11011
11029
|
});
|
|
11012
11030
|
}
|
|
11013
|
-
if (kind
|
|
11031
|
+
if (kind !== "poll" && entry.poll) {
|
|
11014
11032
|
ctx.addIssue({
|
|
11015
11033
|
code: exports_external.ZodIssueCode.custom,
|
|
11016
11034
|
path: ["poll"],
|
|
11017
11035
|
message: "`poll` is only valid when kind: poll."
|
|
11018
11036
|
});
|
|
11019
11037
|
}
|
|
11038
|
+
if (kind === "action" && !entry.action) {
|
|
11039
|
+
ctx.addIssue({
|
|
11040
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11041
|
+
path: ["action"],
|
|
11042
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
11043
|
+
});
|
|
11044
|
+
}
|
|
11045
|
+
if (kind !== "action" && entry.action) {
|
|
11046
|
+
ctx.addIssue({
|
|
11047
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11048
|
+
path: ["action"],
|
|
11049
|
+
message: "`action` is only valid when kind: action."
|
|
11050
|
+
});
|
|
11051
|
+
}
|
|
11052
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
11053
|
+
ctx.addIssue({
|
|
11054
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11055
|
+
path: ["prompt"],
|
|
11056
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
11057
|
+
});
|
|
11058
|
+
}
|
|
11059
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
11060
|
+
ctx.addIssue({
|
|
11061
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11062
|
+
path: ["prompt"],
|
|
11063
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
11064
|
+
});
|
|
11065
|
+
}
|
|
11020
11066
|
});
|
|
11021
11067
|
var AgentSoulSchema = exports_external.object({
|
|
11022
11068
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11737,11 +11737,29 @@ var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
|
11737
11737
|
HttpDiffPollSchema,
|
|
11738
11738
|
TelegramReactionsPollSchema
|
|
11739
11739
|
]);
|
|
11740
|
+
var TelegramMessageActionSchema = exports_external.object({
|
|
11741
|
+
type: exports_external.literal("telegram-message"),
|
|
11742
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable \u2014 fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output \u2014 the text is fully determined by config."),
|
|
11743
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
11744
|
+
});
|
|
11745
|
+
var WebhookActionSchema = exports_external.object({
|
|
11746
|
+
type: exports_external.literal("webhook"),
|
|
11747
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (\u00a76.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
11748
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
11749
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
11750
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
11751
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (\u00a76.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
11752
|
+
});
|
|
11753
|
+
var ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11754
|
+
TelegramMessageActionSchema,
|
|
11755
|
+
WebhookActionSchema
|
|
11756
|
+
]);
|
|
11740
11757
|
var ScheduleEntrySchema = exports_external.object({
|
|
11741
11758
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11742
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11743
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
11759
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
11760
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
11744
11761
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11762
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
11745
11763
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
11746
11764
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' \u2192 a minimal-" + "context cheap cron session (Tier 1). 'agent' \u2192 the agent's live " + "session with full persona/memory (Tier 2). Unset \u2192 inferred from " + "`model` (cheap\u2192fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
11747
11765
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default \u2014 broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary \u2014 " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -11758,13 +11776,41 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
11758
11776
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11759
11777
|
});
|
|
11760
11778
|
}
|
|
11761
|
-
if (kind
|
|
11779
|
+
if (kind !== "poll" && entry.poll) {
|
|
11762
11780
|
ctx.addIssue({
|
|
11763
11781
|
code: exports_external.ZodIssueCode.custom,
|
|
11764
11782
|
path: ["poll"],
|
|
11765
11783
|
message: "`poll` is only valid when kind: poll."
|
|
11766
11784
|
});
|
|
11767
11785
|
}
|
|
11786
|
+
if (kind === "action" && !entry.action) {
|
|
11787
|
+
ctx.addIssue({
|
|
11788
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11789
|
+
path: ["action"],
|
|
11790
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
11791
|
+
});
|
|
11792
|
+
}
|
|
11793
|
+
if (kind !== "action" && entry.action) {
|
|
11794
|
+
ctx.addIssue({
|
|
11795
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11796
|
+
path: ["action"],
|
|
11797
|
+
message: "`action` is only valid when kind: action."
|
|
11798
|
+
});
|
|
11799
|
+
}
|
|
11800
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
11801
|
+
ctx.addIssue({
|
|
11802
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11803
|
+
path: ["prompt"],
|
|
11804
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
11805
|
+
});
|
|
11806
|
+
}
|
|
11807
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
11808
|
+
ctx.addIssue({
|
|
11809
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11810
|
+
path: ["prompt"],
|
|
11811
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
11812
|
+
});
|
|
11813
|
+
}
|
|
11768
11814
|
});
|
|
11769
11815
|
var AgentSoulSchema = exports_external.object({
|
|
11770
11816
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|