@sanity/workflow-mcp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Operate a running instance:
15
15
  | Tool | Read or write | What it's for |
16
16
  | ------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
17
17
  | `list_workflow_instances` | read | Discover what's running. Filterable, capped. |
18
- | `get_workflow_state` | read | Project one instance into a flat shape: current stage, tasks, available actions, recent history. |
18
+ | `get_workflow_state` | read | Project one instance into a flat shape: current stage, activities, available actions, recent history. |
19
19
  | `diagnose_workflow` | read | Explain why an instance is or isn't progressing: a verdict, a one-line summary, and — when stuck — the cause plus suggested remediations. |
20
20
  | `fire_action` | write | Advance state. The only write. Mirrors the engine's universal "something happened" entry point. |
21
21
 
package/dist/index.cjs CHANGED
@@ -3,16 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: !0 });
3
3
  var workflowEngine = require("@sanity/workflow-engine"), define = require("@sanity/workflow-engine/define");
4
4
  const minimalExample = {
5
5
  name: "quick-approval",
6
- version: 1,
7
6
  title: "Quick approval",
8
7
  description: "One review stage with a single approve action, then a terminal stage.",
9
8
  initialStage: "review",
10
- fields: [{ type: "doc.ref", name: "subject", title: "Document", source: { type: "init" } }],
9
+ fields: [{ type: "doc.ref", name: "subject", title: "Document", initialValue: { type: "input" } }],
11
10
  stages: [
12
11
  {
13
12
  name: "review",
14
13
  title: "Review",
15
- tasks: [
14
+ activities: [
16
15
  {
17
16
  name: "decide",
18
17
  title: "Decide",
@@ -26,16 +25,15 @@ const minimalExample = {
26
25
  ]
27
26
  }, reviewLoopExample = {
28
27
  name: "doc-review",
29
- version: 1,
30
28
  title: "Document review",
31
29
  description: "Draft, then editorial review that approves or rejects back to drafting.",
32
30
  initialStage: "drafting",
33
- fields: [{ type: "doc.ref", name: "subject", title: "Document", source: { type: "init" } }],
31
+ fields: [{ type: "doc.ref", name: "subject", title: "Document", initialValue: { type: "input" } }],
34
32
  stages: [
35
33
  {
36
34
  name: "drafting",
37
35
  title: "Drafting",
38
- tasks: [
36
+ activities: [
39
37
  {
40
38
  name: "write",
41
39
  title: "Write the draft",
@@ -48,7 +46,7 @@ const minimalExample = {
48
46
  {
49
47
  name: "review",
50
48
  title: "Editorial review",
51
- tasks: [
49
+ activities: [
52
50
  {
53
51
  name: "review",
54
52
  title: "Review the draft",
@@ -61,18 +59,18 @@ const minimalExample = {
61
59
  ],
62
60
  transitions: [
63
61
  { name: "to-approved", title: "Approve and finish", to: "approved" },
64
- // Reject flips the task to `failed`; this routes the instance back.
62
+ // Reject flips the activity to `failed`; this routes the instance back.
65
63
  {
66
64
  name: "to-drafting",
67
65
  title: "Send back to drafting",
68
66
  to: "drafting",
69
- filter: "$anyTaskFailed"
67
+ filter: "$anyActivityFailed"
70
68
  }
71
69
  ]
72
70
  },
73
71
  { name: "approved", title: "Approved", description: "Terminal \u2014 no transitions out." }
74
72
  ]
75
- }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it \u2014 every\ncondition (filters, predicates, guards) is a GROQ *string*. Generate the JSON,\nthen call `validate_workflow_definition` to check it; fix the reported errors\nand re-validate until it returns `valid: true`. The validator returns the\n*desugared* definition under `definition` \u2014 that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, version (positive int), title, description?, initialStage, fields?, stages[], predicates? }`.\n `initialStage` must be the `name` of a declared stage.\n- **Stage**: `{ name, title?, description?, tasks?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Task**: `{ name, title?, activation?, actions? }`. `activation` is `'auto'`\n (starts when the stage is entered) or `'manual'` (default \u2014 a human starts it).\n- **Action**: `{ name, title?, status?, params?, ops? }`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that flips the *firing task* to\n that status when the action fires. `ops` are mutations applied when the action\n fires (e.g. `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values the action collects from the caller (referenced by\n `{type:'param', param:'<name>'}` sources).\n- **Transition**: `{ name, title?, to, filter? }`. `to` must name a declared\n stage. `filter` is a GROQ condition gating the exit; omit it and it defaults\n to `$allTasksDone`.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, source }`.\n Common `type`s: `doc.ref`, `doc.refs`, `value.string`, `value.url`,\n `value.number`, `value.boolean`. `source` is where the value comes from:\n `{type:'init'}` (supplied when the instance starts), `{type:'write'}` (a user\n edits it), `{type:'param', param:'<actionParamName>'}`, `{type:'now'}`, `{type:'actor'}`.\n By convention a workflow has an `init`-sourced `doc.ref` named `subject` \u2014 the\n headline document it is about.\n\n## GROQ in conditions\n\nBuilt-in variables available in filters/predicates: `$allTasksDone`,\n`$anyTaskFailed` (booleans over the current stage's tasks), `$tasks` (the task\nlist), `$fields` (field values), `$now`, and the caller-scoped vars `$actor`\n(the acting user), `$assigned` (whether the caller is the task's assignee \u2014 the\nidiomatic permission gate, used as `filter: '$assigned'`), and `$can`. Define\nreusable named conditions under top-level `predicates: { name: '<groq>' }` and\nreference them as `$name` (e.g.\n`predicates: { ready: \"count($tasks[status != 'done']) == 0\" }` \u2192 `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) \u2014 **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` \u2014 flips the firing task (shown above).\n- Omitted transition `filter` \u2014 defaults to `$allTasksDone`.\n- Omitted task `activation` \u2014 defaults to `'manual'`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review \u2192 drafting` gated\n on `$anyTaskFailed`), not to a terminal stage. Reserve terminal stages for\n completion and for explicit cancellation/abandonment \u2014 a workflow should not\n dead-end just because something was declined.\n- **Prefer draft \u2192 review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, task names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Custom `predicates` must not shadow a built-in (e.g. `allTasksDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}
73
+ }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it \u2014 every\ncondition (filters, predicates, guards) is a GROQ *string*. Generate the JSON,\nthen call `validate_workflow_definition` to check it; fix the reported errors\nand re-validate until it returns `valid: true`. The validator returns the\n*desugared* definition under `definition` \u2014 that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `initialStage` must be the `name` of a declared stage. Do NOT include a\n `version` \u2014 definitions are immutable and content-addressed; deploy assigns\n the version from the content, the author never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, activation?, actions? }`. `activation` is `'auto'`\n (starts when the stage is entered) or `'manual'` (default \u2014 a human starts it).\n- **Action**: `{ name, title?, status?, params?, ops? }`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that flips the *firing activity* to\n that status when the action fires. `ops` are mutations applied when the action\n fires (e.g. `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values the action collects from the caller (referenced by\n `{type:'param', param:'<name>'}` sources).\n- **Transition**: `{ name, title?, to, filter? }`. `to` must name a declared\n stage. `filter` is a GROQ condition gating the exit; omit it and it defaults\n to `$allActivitiesDone`.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `boolean`, `date` (YYYY-MM-DD), `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`), identities (`actor` \u2014 one concrete\n principal; `assignee` / `assignees` \u2014 one or many user-or-role assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) \u2014 `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the slot once at\n materialisation and is **optional** \u2014 omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n By convention a workflow has an `input`-sourced `doc.ref` named `subject` \u2014 the\n headline document it is about.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work \u2014 rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log \u2014 rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in filters/predicates: `$allActivitiesDone`,\n`$anyActivityFailed` (booleans over the current stage's activities), `$activities` (the activity\nlist), `$fields` (field values), `$now`, and the caller-scoped vars `$actor`\n(the acting user), `$assigned` (whether the caller is the activity's assignee \u2014 the\nidiomatic permission gate, used as `filter: '$assigned'`), and `$can`. Define\nreusable named conditions under top-level `predicates: { name: '<groq>' }` and\nreference them as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` \u2192 `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) \u2014 **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` \u2014 flips the firing activity (shown above).\n- Omitted transition `filter` \u2014 defaults to `$allActivitiesDone`.\n- Omitted activity `activation` \u2014 defaults to `'manual'`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review \u2192 drafting` gated\n on `$anyActivityFailed`), not to a terminal stage. Reserve terminal stages for\n completion and for explicit cancellation/abandonment \u2014 a workflow should not\n dead-end just because something was declined.\n- **Prefer draft \u2192 review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}
76
74
 
77
75
  ## Examples
78
76
 
@@ -107,7 +105,7 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
107
105
  },
108
106
  {
109
107
  name: "get_workflow_state",
110
- description: "Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable `workflowTitle`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject \u2014 its ref and title), every task on the current stage with its available actions (and whether each action is currently allowed), and the most recent history entries. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read \u2014 it does not change anything. If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same `workflowTitle` and `subject` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.",
108
+ description: "Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable `workflowTitle`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject \u2014 its ref and title), every activity on the current stage with its available actions (and whether each action is currently allowed), and the most recent history entries. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read \u2014 it does not change anything. If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same `workflowTitle` and `subject` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.",
111
109
  input_schema: {
112
110
  type: "object",
113
111
  properties: {
@@ -122,7 +120,7 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
122
120
  },
123
121
  {
124
122
  name: "diagnose_workflow",
125
- description: "Explain why a single workflow instance is or isn't progressing. Returns a verdict (`state`: progressing, waiting, blocked, completed, aborted, or stuck), a one-line `summary`, and \u2014 when stuck \u2014 a structured `cause` plus the `remediations` that would unstick it. Use this when an instance seems stalled or the user asks \"why isn't this moving?\": it distinguishes a healthy instance (waiting on a human, or will advance on its own) from a genuinely stuck one (a failed effect or task, a dead-end transition). This is a pure read \u2014 it changes nothing, and the remediations it names are advisory: none can be executed through this server. To actually advance a healthy waiting instance use fire_action; for per-task action detail use get_workflow_state.",
123
+ description: "Explain why a single workflow instance is or isn't progressing. Returns a verdict (`state`: progressing, waiting, blocked, completed, aborted, or stuck), a one-line `summary`, and \u2014 when stuck \u2014 a structured `cause` plus the `remediations` that would unstick it. Use this when an instance seems stalled or the user asks \"why isn't this moving?\": it distinguishes a healthy instance (waiting on a human, or will advance on its own) from a genuinely stuck one (a failed effect or activity, a dead-end transition). This is a pure read \u2014 it changes nothing, and the remediations it names are advisory: none can be executed through this server. To actually advance a healthy waiting instance use fire_action; for per-activity action detail use get_workflow_state.",
126
124
  input_schema: {
127
125
  type: "object",
128
126
  properties: {
@@ -137,7 +135,7 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
137
135
  },
138
136
  {
139
137
  name: "fire_action",
140
- description: "Advance a workflow instance by firing an action on one of its tasks. This is the only way to advance workflow state from the outside \u2014 there is no separate 'complete task' or 'transition stage' tool. To find the right (task, action) pair, call get_workflow_state first and pick from the allowed actions listed on the current stage's tasks. After firing, the engine cascades any auto-transitions that become eligible (so an 'approve' action on a review task may transition the workflow to a terminal stage in one shot). Returns the resulting state, same shape as get_workflow_state. If the action is not currently allowed (e.g. the task is already done, or a guard fails), this returns an error describing why.",
138
+ description: "Advance a workflow instance by firing an action on one of its activities. This is the only way to advance workflow state from the outside \u2014 there is no separate 'complete activity' or 'transition stage' tool. To find the right (activity, action) pair, call get_workflow_state first and pick from the allowed actions listed on the current stage's activities. After firing, the engine cascades any auto-transitions that become eligible (so an 'approve' action on a review activity may transition the workflow to a terminal stage in one shot). Returns the resulting state, same shape as get_workflow_state. If the action is not currently allowed (e.g. the activity is already done, or a guard fails), this returns an error describing why.",
141
139
  input_schema: {
142
140
  type: "object",
143
141
  properties: {
@@ -145,16 +143,21 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
145
143
  type: "string",
146
144
  description: "The workflow instance id."
147
145
  },
148
- task: {
146
+ activity: {
149
147
  type: "string",
150
- description: "The id of the task on the current stage. Must be one of the tasks returned by get_workflow_state."
148
+ description: "The id of the activity on the current stage. Must be one of the activities returned by get_workflow_state."
151
149
  },
152
150
  action: {
153
151
  type: "string",
154
- description: "The id of the action on that task. Must be one of the actions listed as allowed=true on the task."
152
+ description: "The id of the action on that activity. Must be one of the actions listed as allowed=true on the activity."
153
+ },
154
+ params: {
155
+ type: "object",
156
+ description: `Optional. Values for the action's declared params, keyed by param name (e.g. {"note": "Unsupported claim in paragraph 3."}). Required when the action declares a required param \u2014 get_workflow_state lists each action's params and whether they are required. The shape is per-action, so this is a free-form object; the engine validates the supplied values against the action's declared params and rejects the call if a required one is missing.`,
157
+ additionalProperties: !0
155
158
  }
156
159
  },
157
- required: ["instance_id", "task", "action"],
160
+ required: ["instance_id", "activity", "action"],
158
161
  additionalProperties: !1
159
162
  }
160
163
  },
@@ -237,8 +240,12 @@ function projectSummary(doc, subjectTitles) {
237
240
  ...subject !== void 0 ? { subject } : {}
238
241
  };
239
242
  }
240
- function projectState(instance, evaluation, subjectTitles) {
241
- const workflowTitle = JSON.parse(instance.definitionSnapshot).title, stageTitle = evaluation.currentStage.stage.title, tasks = evaluation.currentStage.tasks.map((te) => {
243
+ function projectState({
244
+ instance,
245
+ evaluation,
246
+ subjectTitles
247
+ }) {
248
+ const workflowTitle = JSON.parse(instance.definitionSnapshot).title, stageTitle = evaluation.currentStage.stage.title, activities = evaluation.currentStage.activities.map((te) => {
242
249
  const actions = te.actions.map((ae) => {
243
250
  const verdict = workflowEngine.actionVerdict(te, ae);
244
251
  return {
@@ -249,9 +256,9 @@ function projectState(instance, evaluation, subjectTitles) {
249
256
  };
250
257
  });
251
258
  return {
252
- task: te.task.name,
253
- ...te.task.title !== void 0 ? { title: te.task.title } : {},
254
- ...te.task.description !== void 0 ? { description: te.task.description } : {},
259
+ activity: te.activity.name,
260
+ ...te.activity.title !== void 0 ? { title: te.activity.title } : {},
261
+ ...te.activity.description !== void 0 ? { description: te.activity.description } : {},
255
262
  status: te.status,
256
263
  actions
257
264
  };
@@ -268,7 +275,7 @@ function projectState(instance, evaluation, subjectTitles) {
268
275
  ...stageTitle !== void 0 ? { currentStageTitle: stageTitle } : {},
269
276
  done: isInstanceDone(instance),
270
277
  ...subject !== void 0 ? { subject } : {},
271
- tasks,
278
+ activities,
272
279
  recentHistory
273
280
  };
274
281
  }
@@ -281,15 +288,21 @@ function isInstanceDone(instance) {
281
288
  return !1;
282
289
  }
283
290
  }
284
- const DISABLED_REASON_TEXT = {
285
- "filter-failed": "action's filter condition did not hold for this actor",
286
- "task-not-active": "task is not currently active",
287
- "stage-terminal": "stage is terminal \u2014 no further actions possible",
288
- "instance-completed": "instance has already completed",
289
- "requirements-unmet": "task's declared requirements are not yet satisfied"
290
- };
291
291
  function formatDisabledReason(reason) {
292
- return DISABLED_REASON_TEXT[reason.kind] ?? reason.kind.replace(/-/g, " ");
292
+ switch (reason.kind) {
293
+ case "filter-failed":
294
+ return "action's filter condition did not hold for this actor";
295
+ case "activity-not-active":
296
+ return "activity is not currently active";
297
+ case "stage-terminal":
298
+ return "stage is terminal \u2014 no further actions possible";
299
+ case "mutation-guard-denied":
300
+ return `blocked by mutation guard(s): ${reason.guardIds.join(", ")}`;
301
+ case "instance-completed":
302
+ return "instance has already completed";
303
+ case "requirements-unmet":
304
+ return "activity's declared requirements are not yet satisfied";
305
+ }
293
306
  }
294
307
  function summariseHistoryEntry(entry) {
295
308
  switch (entry._type) {
@@ -297,12 +310,12 @@ function summariseHistoryEntry(entry) {
297
310
  return `entered stage "${String(entry.stage)}"`;
298
311
  case "stageExited":
299
312
  return `exited stage "${String(entry.stage)}" \u2192 "${String(entry.toStage)}"`;
300
- case "taskActivated":
301
- return `task "${String(entry.task)}" activated`;
302
- case "taskStatusChanged":
303
- return `task "${String(entry.task)}" status: ${String(entry.from)} \u2192 ${String(entry.to)}`;
313
+ case "activityActivated":
314
+ return `activity "${String(entry.activity)}" activated`;
315
+ case "activityStatusChanged":
316
+ return `activity "${String(entry.activity)}" status: ${String(entry.from)} \u2192 ${String(entry.to)}`;
304
317
  case "actionFired":
305
- return `action "${String(entry.action)}" fired on task "${String(entry.task)}"`;
318
+ return `action "${String(entry.action)}" fired on activity "${String(entry.activity)}"`;
306
319
  case "transitionFired":
307
320
  return `transition fired: "${String(entry.fromStage)}" \u2192 "${String(entry.toStage)}"`;
308
321
  case "effectQueued":
@@ -310,7 +323,7 @@ function summariseHistoryEntry(entry) {
310
323
  case "effectCompleted":
311
324
  return `effect "${String(entry.effect)}" completed (${String(entry.status)})`;
312
325
  case "spawned":
313
- return `spawned child workflow from task "${String(entry.task)}"`;
326
+ return `spawned child workflow from activity "${String(entry.activity)}"`;
314
327
  default:
315
328
  return entry._type;
316
329
  }
@@ -320,9 +333,9 @@ function diagnosisSummary(diagnosis) {
320
333
  case "progressing":
321
334
  return "This instance will advance on its own.";
322
335
  case "waiting":
323
- return `Waiting for action on task "${diagnosis.task}": ${diagnosis.actions.join(" or ")}. This is the normal in-flight state \u2014 it advances when someone acts.`;
336
+ return `Waiting for action on activity "${diagnosis.activity}": ${diagnosis.actions.join(" or ")}. This is the normal in-flight state \u2014 it advances when someone acts.`;
324
337
  case "blocked":
325
- return `Task "${diagnosis.task}" is visible but not yet executable \u2014 unmet requirement(s): ${diagnosis.requirements.join(", ")}. It will not advance until those are satisfied.`;
338
+ return `Activity "${diagnosis.activity}" is visible but not yet executable \u2014 unmet requirement(s): ${diagnosis.requirements.join(", ")}. It will not advance until those are satisfied.`;
326
339
  case "completed":
327
340
  return `Completed at ${diagnosis.at}.`;
328
341
  case "aborted":
@@ -334,13 +347,13 @@ function diagnosisSummary(diagnosis) {
334
347
  function stuckSummary(cause) {
335
348
  switch (cause.kind) {
336
349
  case "failed-effect":
337
- return `Stuck: a failed effect "${cause.effect.name}" is blocking task "${cause.effect.origin.name}", which can't complete until the effect succeeds.`;
350
+ return `Stuck: a failed effect "${cause.effect.name}" is blocking activity "${cause.effect.origin.name}", which can't complete until the effect succeeds.`;
338
351
  case "hung-effect":
339
352
  return `Stuck: effect "${cause.effect.name}" was claimed but never completed \u2014 the drainer likely died mid-dispatch, so it won't drain on its own.`;
340
- case "failed-task":
341
- return `Stuck: task "${cause.task}" is in a terminal failed state, so any exit transition gated on it can never fire.`;
353
+ case "failed-activity":
354
+ return `Stuck: activity "${cause.activity}" is in a terminal failed state, so any exit transition gated on it can never fire.`;
342
355
  case "no-transition-fires":
343
- return "Stuck: every task is resolved but no exit transition's filter is satisfied \u2014 likely a routing state value a filter reads was never written.";
356
+ return "Stuck: every activity is resolved but no exit transition's filter is satisfied \u2014 likely a routing state value a filter reads was never written.";
344
357
  case "transition-unevaluable":
345
358
  return `Stuck (recoverable): exit transition(s) ${cause.transitions.join(", ")} read an operand that is missing or unreadable (GROQ null). It advances on its own once that data becomes readable \u2014 no manual fix needed.`;
346
359
  }
@@ -354,20 +367,21 @@ function buildTools(engine, options = {}) {
354
367
  },
355
368
  get_workflow_state: async (rawInput) => {
356
369
  const input = parseInstanceIdInput(rawInput, "get_workflow_state");
357
- return getInstanceState(engine, input.instance_id, accessOverride);
370
+ return getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
358
371
  },
359
372
  diagnose_workflow: async (rawInput) => {
360
373
  const input = parseInstanceIdInput(rawInput, "diagnose_workflow");
361
- return getInstanceDiagnosis(engine, input.instance_id, accessOverride);
374
+ return getInstanceDiagnosis({ engine, instanceId: input.instance_id, accessOverride });
362
375
  },
363
376
  fire_action: async (rawInput) => {
364
377
  const input = parseFireActionInput(rawInput);
365
378
  return await engine.fireAction({
366
379
  instanceId: input.instance_id,
367
- task: input.task,
380
+ activity: input.activity,
368
381
  action: input.action,
382
+ ...input.params !== void 0 ? { params: input.params } : {},
369
383
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
370
- }), getInstanceState(engine, input.instance_id, accessOverride);
384
+ }), getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
371
385
  },
372
386
  // Authoring tools — engine-independent: they teach and check the DSL, they
373
387
  // don't touch the lake or an instance.
@@ -389,33 +403,44 @@ function parseListInput(raw) {
389
403
  const obj = raw, status = obj.status === "done" || obj.status === "any" ? obj.status : "in_flight", definition = typeof obj.definition == "string" ? obj.definition : void 0;
390
404
  return definition !== void 0 ? { definition, status } : { status };
391
405
  }
392
- function parseValidateInput(raw) {
406
+ function asObject(raw, tool) {
393
407
  if (raw === null || typeof raw != "object")
394
- throw new Error("validate_workflow_definition: input must be an object");
395
- const obj = raw;
408
+ throw new Error(`${tool}: input must be an object`);
409
+ return raw;
410
+ }
411
+ function requireString({
412
+ obj,
413
+ key,
414
+ tool
415
+ }) {
416
+ const value = obj[key];
417
+ if (typeof value != "string" || value === "")
418
+ throw new Error(`${tool}: ${key} is required`);
419
+ return value;
420
+ }
421
+ function parseValidateInput(raw) {
422
+ const obj = asObject(raw, "validate_workflow_definition");
396
423
  if (obj.definition === null || typeof obj.definition != "object")
397
424
  throw new Error("validate_workflow_definition: `definition` is required and must be an object");
398
425
  return { definition: obj.definition };
399
426
  }
400
427
  function parseInstanceIdInput(raw, tool) {
401
- if (raw === null || typeof raw != "object")
402
- throw new Error(`${tool}: input must be an object`);
403
- const obj = raw;
404
- if (typeof obj.instance_id != "string" || obj.instance_id === "")
405
- throw new Error(`${tool}: instance_id is required`);
406
- return { instance_id: obj.instance_id };
428
+ const obj = asObject(raw, tool);
429
+ return { instance_id: requireString({ obj, key: "instance_id", tool }) };
407
430
  }
408
431
  function parseFireActionInput(raw) {
409
- if (raw === null || typeof raw != "object")
410
- throw new Error("fire_action: input must be an object");
411
- const obj = raw;
412
- for (const key of ["instance_id", "task", "action"])
413
- if (typeof obj[key] != "string" || obj[key] === "")
414
- throw new Error(`fire_action: ${key} is required`);
432
+ const obj = asObject(raw, "fire_action");
433
+ let params;
434
+ if (obj.params !== void 0) {
435
+ if (obj.params === null || typeof obj.params != "object" || Array.isArray(obj.params))
436
+ throw new Error("fire_action: params must be an object");
437
+ params = obj.params;
438
+ }
415
439
  return {
416
- instance_id: obj.instance_id,
417
- task: obj.task,
418
- action: obj.action
440
+ instance_id: requireString({ obj, key: "instance_id", tool: "fire_action" }),
441
+ activity: requireString({ obj, key: "activity", tool: "fire_action" }),
442
+ action: requireString({ obj, key: "action", tool: "fire_action" }),
443
+ ...params !== void 0 ? { params } : {}
419
444
  };
420
445
  }
421
446
  async function listInstances(engine, input) {
@@ -431,14 +456,22 @@ async function listInstances(engine, input) {
431
456
  instances: filtered.map((doc) => projectSummary(doc, subjectTitles))
432
457
  };
433
458
  }
434
- async function getInstanceState(engine, instanceId, accessOverride) {
459
+ async function getInstanceState({
460
+ engine,
461
+ instanceId,
462
+ accessOverride
463
+ }) {
435
464
  const evaluation = await engine.evaluateInstance({
436
465
  instanceId,
437
466
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
438
467
  }), instance = await engine.getInstance({ instanceId }), subjectTitles = await fetchSubjectTitles(engine, [instance]);
439
- return projectState(instance, evaluation, subjectTitles);
468
+ return projectState({ instance, evaluation, subjectTitles });
440
469
  }
441
- async function getInstanceDiagnosis(engine, instanceId, accessOverride) {
470
+ async function getInstanceDiagnosis({
471
+ engine,
472
+ instanceId,
473
+ accessOverride
474
+ }) {
442
475
  const { diagnosis, remediations } = await engine.diagnose({
443
476
  instanceId,
444
477
  ...accessOverride !== void 0 ? { access: accessOverride } : {}