@frontera-sdk/automation 1.45.11 → 1.45.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/automation",
3
- "version": "1.45.11",
3
+ "version": "1.45.12",
4
4
  "description": "Author Frontera automations: manifest, triggers and the typed handler contract.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -42,7 +42,7 @@
42
42
  "typescript": "^5.9.3"
43
43
  },
44
44
  "dependencies": {
45
- "@frontera-sdk/blueprint": "1.45.9",
45
+ "@frontera-sdk/blueprint": "1.45.12",
46
46
  "cron-parser": "^5.0.6"
47
47
  }
48
48
  }
package/src/manifest.ts CHANGED
@@ -71,6 +71,17 @@ const TYPED_NAMESPACES: Record<string, { re: RegExp; hint: string }> = {
71
71
  },
72
72
  }
73
73
 
74
+ /**
75
+ * Shortest description an agent-callable automation may carry.
76
+ *
77
+ * Not a round number picked for looks: it is about the length of one honest
78
+ * clause ("Reconcile open invoices against the settlement file"), and it is
79
+ * chosen to be long enough that the slug restated as a sentence — "reconcile
80
+ * invoices" — does not clear it. A description that only repeats the name
81
+ * tells a model nothing it did not already have from the tool name.
82
+ */
83
+ export const AGENT_DESCRIPTION_MIN_CHARS = 24
84
+
74
85
  const KNOWN_KEYS = new Set([
75
86
  'name', 'trigger', 'grants', 'inputs', 'concurrency', 'retries', 'description',
76
87
  ])
@@ -111,6 +122,7 @@ export function validateManifest(input: unknown): ValidationResult {
111
122
  inputs?: unknown
112
123
  concurrency?: unknown
113
124
  retries?: unknown
125
+ description?: unknown
114
126
  }
115
127
 
116
128
  if (typeof m.name !== 'string' || !NAME_RE.test(m.name)) {
@@ -119,10 +131,27 @@ export function validateManifest(input: unknown): ValidationResult {
119
131
  errors.push('name must be 64 characters or fewer')
120
132
  }
121
133
 
122
- const trigger = m.trigger as { cron?: string; manual?: boolean } | undefined
123
- if (!trigger || (trigger.cron === undefined && trigger.manual !== true)) {
124
- errors.push('trigger must be { cron } or { manual: true }')
125
- } else if (trigger.cron !== undefined) {
134
+ const trigger = m.trigger as
135
+ | { cron?: string; manual?: boolean; agent?: boolean }
136
+ | undefined
137
+ if (
138
+ !trigger
139
+ || (trigger.cron === undefined && trigger.manual !== true && trigger.agent !== true)
140
+ ) {
141
+ errors.push('trigger must be { cron }, { manual: true }, or { agent: true }')
142
+ } else if (trigger.agent !== undefined && trigger.agent !== true) {
143
+ // Not folded into the arm above: `{ manual: true, agent: false }` is a
144
+ // legal-looking manifest that means nothing. `agent` is a permission, and
145
+ // the way to withhold a permission is to omit it, not to write it false —
146
+ // the same rule the grant list follows.
147
+ errors.push(
148
+ 'trigger.agent must be true when present — omit the key to mean "not agent-callable"',
149
+ )
150
+ }
151
+ // Separate `if`, not the old `else if`: with three arms the cron check has to
152
+ // run whenever a cron is present, including on `{ cron, agent: true }`, and
153
+ // an `else if` chained off the acceptance test above would skip it there.
154
+ if (trigger?.cron !== undefined) {
126
155
  if (typeof trigger.cron !== 'string') {
127
156
  errors.push('invalid cron expression: must be a string')
128
157
  } else {
@@ -252,6 +281,36 @@ export function validateManifest(input: unknown): ValidationResult {
252
281
  }
253
282
  }
254
283
 
284
+ // `trigger: { agent: true }` turns this manifest into the source of a tool
285
+ // definition a language model reads and decides from. Two fields that are
286
+ // courtesies everywhere else become load-bearing here, so they are errors
287
+ // rather than warnings: a model handed an undescribed tool, or an undescribed
288
+ // argument, does not fail loudly — it guesses, and the guess starts a real
289
+ // run against real systems. Checked at deploy, where the author still has the
290
+ // file open, rather than at bind time in a Console someone else is using.
291
+ if (trigger?.agent === true) {
292
+ const description = m.description
293
+ if (typeof description !== 'string' || description.trim().length < AGENT_DESCRIPTION_MIN_CHARS) {
294
+ errors.push(
295
+ `trigger { agent: true } requires a description of at least ${AGENT_DESCRIPTION_MIN_CHARS} `
296
+ + 'characters — it becomes the tool description an agent reads before calling this '
297
+ + 'automation.',
298
+ )
299
+ }
300
+ const agentInputs = m.inputs
301
+ if (agentInputs && typeof agentInputs === 'object' && !Array.isArray(agentInputs)) {
302
+ for (const [key, raw] of Object.entries(agentInputs as Record<string, unknown>)) {
303
+ const spec = raw as { description?: unknown } | null
304
+ if (typeof spec?.description !== 'string' || spec.description.trim().length === 0) {
305
+ errors.push(
306
+ `input "${key}" needs a description: trigger { agent: true } publishes every input as `
307
+ + 'a tool argument, and an agent cannot fill an argument it has no description for.',
308
+ )
309
+ }
310
+ }
311
+ }
312
+ }
313
+
255
314
  if (input && typeof input === 'object' && !Array.isArray(input)) {
256
315
  for (const key of Object.keys(input)) {
257
316
  if (!KNOWN_KEYS.has(key)) {
package/src/types.ts CHANGED
@@ -7,16 +7,29 @@
7
7
  import type { WhereNode } from '@frontera-sdk/blueprint/types'
8
8
 
9
9
  /**
10
- * Cron and manual only for now; event and webhook land with Event Triggers.
10
+ * Cron, manual, and agent for now; event and webhook land with Event Triggers.
11
11
  *
12
12
  * The `?: never` members are load-bearing. Without them `{ cron, manual }`
13
13
  * typechecks — TypeScript's excess-property check admits any key present on
14
14
  * *some* member of a union — and the runner would have to decide at runtime
15
15
  * what a both-shaped trigger means.
16
+ *
17
+ * `agent` is deliberately NOT part of that exclusion. Cron and manual answer
18
+ * "what fires this on its own"; `agent: true` answers "may a bound agent call
19
+ * this", which is an orthogonal question — a nightly reconciliation that an
20
+ * analyst can also ask an agent to run on demand is one automation, not two.
21
+ * So `agent` rides alongside either, and the third arm exists for the
22
+ * agent-only automation, which has no self-starting trigger at all.
23
+ *
24
+ * Declaring it is only the AUTHOR's half of the permission. A workspace
25
+ * operator must still bind the automation to one named agent before any tool
26
+ * is projected; see the design in
27
+ * docs/superpowers/specs/2026-08-27-agent-callable-automations-design.md.
16
28
  */
17
29
  export type AutomationTrigger =
18
- | { cron: string; manual?: never }
19
- | { cron?: never; manual: true }
30
+ | { cron: string; manual?: never; agent?: true }
31
+ | { cron?: never; manual: true; agent?: true }
32
+ | { cron?: never; manual?: never; agent: true }
20
33
 
21
34
  /**
22
35
  * An author-time affordance, not a validation gate.