@ixo/oracle-runtime-workers 0.2.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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Action catalog access. Wraps the editor registry (`getAllActions`/`getAction`/
3
+ * `getActionByCan`) and merges the plugin-side metadata overlay (action-metadata.ts)
4
+ * to power discovery (`list_actions`/`describe_action`) and the event-capability
5
+ * validation that `onEvent` relies on (spec §2.3).
6
+ *
7
+ * The `action` identifier exposed to the agent is the registry `type` (e.g.
8
+ * "qi/email.send") — never the `can` (leak guard, §1).
9
+ */
10
+ import type { ActionDefinition, OutputSchemaField } from '@ixo/editor/core';
11
+ import { getAllActions, getAction, getActionByCan } from '@ixo/editor/core';
12
+ import { ACTION_METADATA, type OverlayEntry } from './action-metadata';
13
+
14
+ /** The lifecycle hook types any step can attach via `on` (spec Appendix A.4). */
15
+ export const HOOK_TYPES = [
16
+ 'sendEmail',
17
+ 'addLinkedEntity',
18
+ 'sendMatrixDM',
19
+ ] as const;
20
+
21
+ export interface ActionSummary {
22
+ action: string;
23
+ summary: string;
24
+ whenToUse: string[];
25
+ tags: string[];
26
+ }
27
+
28
+ export interface ActionField {
29
+ name: string;
30
+ type: string;
31
+ required?: boolean;
32
+ description?: string;
33
+ }
34
+
35
+ export interface ActionEvent {
36
+ name: string;
37
+ displayName?: string;
38
+ description?: string;
39
+ }
40
+
41
+ export interface ActionDescription {
42
+ action: string;
43
+ summary: string;
44
+ whenToUse: string[];
45
+ whenNotToUse: string[];
46
+ inputs: ActionField[];
47
+ /**
48
+ * Whether this action's input requirements are known. False means the field
49
+ * list above is NOT authoritative — the agent must ask the user what the step
50
+ * needs rather than assume it takes no inputs.
51
+ */
52
+ inputsDeclared: boolean;
53
+ outputs: ActionField[];
54
+ events: ActionEvent[];
55
+ hooks: string[];
56
+ isForm: boolean;
57
+ requiresConfirmation: boolean;
58
+ }
59
+
60
+ /** Resolve a friendly `action` (registry type, or a `can`) to its definition. */
61
+ export function getActionDef(action: string): ActionDefinition | undefined {
62
+ return getAction(action) ?? getActionByCan(action);
63
+ }
64
+
65
+ function overlayFor(def: ActionDefinition): OverlayEntry {
66
+ return ACTION_METADATA[def.type] ?? {};
67
+ }
68
+
69
+ /**
70
+ * Whether `onEvent` can target this action as a source (spec §2.3): it must be
71
+ * `eligibleForEventTrigger` AND declare at least one event.
72
+ */
73
+ export function isEventCapable(def: ActionDefinition): boolean {
74
+ if (!def.eligibleForEventTrigger) return false;
75
+ return (
76
+ (def.events?.length ?? 0) > 0 || typeof def.getDynamicEvents === 'function'
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Whether a step is a human-form action (`qi/form.submit` / `qi/human.form.submit`
82
+ * and friends). Form steps need special output handling: they emit one bundled
83
+ * `answers` object rather than per-field outputs, so discovery + linkage expand
84
+ * the individual question paths separately (see `formOutputFields`).
85
+ */
86
+ export function isFormAction(def: ActionDefinition): boolean {
87
+ return def.can === 'human/form' || /form|survey/i.test(def.type);
88
+ }
89
+
90
+ type InputSchemaShape =
91
+ | {
92
+ properties?: Record<string, { type?: string; description?: string }>;
93
+ required?: string[];
94
+ }
95
+ | undefined;
96
+
97
+ /** Whether we have any authoritative input-requirement data for this action. */
98
+ function hasDeclaredInputs(
99
+ def: ActionDefinition,
100
+ overlay: OverlayEntry,
101
+ ): boolean {
102
+ if ((overlay.inputPorts?.length ?? 0) > 0) return true;
103
+ return !!(def.inputSchema as InputSchemaShape)?.properties;
104
+ }
105
+
106
+ /**
107
+ * The action's input fields. The registry's `inputSchema` is the single source
108
+ * of truth for requirements (each action declares its required inputs); the
109
+ * overlay's `inputPorts` are only a fallback for an action that ships none.
110
+ * Empty when neither is present — pair with `hasDeclaredInputs` so callers know
111
+ * the difference between "no inputs" and "inputs unknown".
112
+ */
113
+ function inputFields(
114
+ def: ActionDefinition,
115
+ overlay: OverlayEntry,
116
+ ): ActionField[] {
117
+ const schema = def.inputSchema as InputSchemaShape;
118
+ const props = schema?.properties;
119
+ if (props) {
120
+ const required = new Set(schema?.required ?? []);
121
+ return Object.entries(props).map(([name, spec]) => ({
122
+ name,
123
+ type: typeof spec?.type === 'string' ? spec.type : 'unknown',
124
+ required: required.has(name),
125
+ description: spec?.description,
126
+ }));
127
+ }
128
+ if (overlay.inputPorts && overlay.inputPorts.length > 0) {
129
+ return overlay.inputPorts.map((p) => ({
130
+ name: p.path,
131
+ type: p.portType,
132
+ required: p.required ?? false,
133
+ description: p.description,
134
+ }));
135
+ }
136
+ return [];
137
+ }
138
+
139
+ function outputFields(def: ActionDefinition): ActionField[] {
140
+ const out: OutputSchemaField[] | undefined = def.outputSchema;
141
+ if (!out) return [];
142
+ return out.map((f) => ({
143
+ name: f.path,
144
+ type: f.type,
145
+ description: f.description,
146
+ }));
147
+ }
148
+
149
+ export function listActions(filter?: {
150
+ category?: string;
151
+ tag?: string;
152
+ }): ActionSummary[] {
153
+ const result: ActionSummary[] = [];
154
+ for (const def of getAllActions()) {
155
+ const overlay = overlayFor(def);
156
+ if (filter?.tag && !(overlay.tags ?? []).includes(filter.tag)) continue;
157
+ result.push({
158
+ action: def.type,
159
+ summary: overlay.summary ?? '',
160
+ whenToUse: overlay.whenToUse ?? [],
161
+ tags: overlay.tags ?? [],
162
+ });
163
+ }
164
+ return result.sort((a, b) => a.action.localeCompare(b.action));
165
+ }
166
+
167
+ export function describeAction(action: string): ActionDescription | null {
168
+ const def = getActionDef(action);
169
+ if (!def) return null;
170
+ const overlay = overlayFor(def);
171
+ return {
172
+ action: def.type,
173
+ summary: overlay.summary ?? '',
174
+ whenToUse: overlay.whenToUse ?? [],
175
+ whenNotToUse: overlay.whenNotToUse ?? [],
176
+ inputs: inputFields(def, overlay),
177
+ inputsDeclared: hasDeclaredInputs(def, overlay),
178
+ outputs: outputFields(def),
179
+ events: (def.events ?? []).map(
180
+ (e: { name: string; displayName?: string; description?: string }) => ({
181
+ name: e.name,
182
+ displayName: e.displayName,
183
+ description: e.description,
184
+ }),
185
+ ),
186
+ hooks: [...HOOK_TYPES],
187
+ isForm: isFormAction(def),
188
+ requiresConfirmation: def.defaultRequiresConfirmation,
189
+ };
190
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Per-block / delta edit dispatcher (spec §4.2). Every mutation touches only
3
+ * its target step; unrelated steps' data is never rewritten or dropped.
4
+ *
5
+ * All edits run in oracle-runtime's native yjs on the connected provider doc
6
+ * (the editor package's fragment helpers are cross-version-broken here — see
7
+ * read.ts). Value-prop writes reuse the editor plugin's native `editBlock`
8
+ * (which writes the BlockNote shape the portal renders); remove/reorder reuse
9
+ * its native `deleteBlock`/`moveBlock` and we keep the `qi.flow.*` maps in sync.
10
+ *
11
+ * Conditions are written DIRECTLY as `props.conditions` in the FE evaluator's
12
+ * operator vocabulary (never via the compiler's `cap.condition`, which never
13
+ * evaluates — see translator.ts).
14
+ */
15
+ import type { Doc as YDoc } from 'yjs';
16
+ import * as Y from 'yjs';
17
+ import { deleteBlock, editBlock, findParentOf } from './ydoc-helpers';
18
+ import { getActionDef, isEventCapable } from './actions';
19
+ import { FlowError } from './errors';
20
+ import { readFlowSpec } from './read';
21
+ import {
22
+ BLOCK_ID_PREFIX,
23
+ blockIdToStepId,
24
+ buildConditionsProp,
25
+ canToAction,
26
+ friendlyInputsToNb,
27
+ stepIdToBlockId,
28
+ } from './translator';
29
+ import type { Condition } from './types';
30
+
31
+ const DOC = 'document';
32
+
33
+ function requireStep(doc: YDoc, stepId: string): string {
34
+ if (!doc.getMap('qi.flow.nodes').has(stepId)) {
35
+ throw new FlowError('step_not_found', `No step "${stepId}" in this flow.`);
36
+ }
37
+ return stepIdToBlockId(stepId);
38
+ }
39
+
40
+ /** Write a set of value props onto a step's block (per-block, lossless). */
41
+ export function setStepProps(
42
+ doc: YDoc,
43
+ stepId: string,
44
+ props: Record<string, string>,
45
+ ): void {
46
+ const blockId = requireStep(doc, stepId);
47
+ editBlock(doc, { blockId, attributes: { props }, docName: DOC });
48
+ }
49
+
50
+ export function setStepInputs(
51
+ doc: YDoc,
52
+ stepId: string,
53
+ inputs: Record<string, unknown>,
54
+ ): void {
55
+ setStepProps(doc, stepId, {
56
+ inputs: JSON.stringify(friendlyInputsToNb(inputs) ?? {}),
57
+ });
58
+ }
59
+
60
+ export function setStepConditions(
61
+ doc: YDoc,
62
+ stepId: string,
63
+ conditions: Condition[],
64
+ ): void {
65
+ setStepProps(doc, stepId, {
66
+ conditions: conditions.length > 0 ? buildConditionsProp(conditions) : '',
67
+ });
68
+ }
69
+
70
+ export function setStepSchedule(
71
+ doc: YDoc,
72
+ stepId: string,
73
+ due: { at?: string; within?: string; afterCommitment?: string } | undefined,
74
+ commitTo?: string,
75
+ ): void {
76
+ setStepProps(doc, stepId, {
77
+ ttlAbsoluteDueDate: due?.at ?? '',
78
+ ttlFromEnablement: due?.within ?? '',
79
+ ttlFromCommitment: commitTo ?? due?.afterCommitment ?? '',
80
+ });
81
+ }
82
+
83
+ export function setStepAssignment(
84
+ doc: YDoc,
85
+ stepId: string,
86
+ assignTo: string | undefined,
87
+ ): void {
88
+ // The portal shows + nudges the assignee from `props.assignment`; the
89
+ // `authorisedActors` whitelist also lets that actor run the step. Write both
90
+ // so the assignee is visible AND authorized; clear both when unassigned.
91
+ setStepProps(doc, stepId, {
92
+ assignment: assignTo
93
+ ? JSON.stringify({ assignedActor: { did: assignTo } })
94
+ : '',
95
+ authorisedActors: assignTo ? JSON.stringify([assignTo]) : '',
96
+ });
97
+ }
98
+
99
+ export function setStepConfirmation(
100
+ doc: YDoc,
101
+ stepId: string,
102
+ requireConfirmation: boolean,
103
+ ): void {
104
+ setStepProps(doc, stepId, {
105
+ requiresConfirmation: requireConfirmation ? 'true' : '',
106
+ });
107
+ }
108
+
109
+ /** Update the stable semantic phase stored on the compiled Flow node. */
110
+ export function setStepPhase(
111
+ doc: YDoc,
112
+ stepId: string,
113
+ phase: string | undefined,
114
+ ): void {
115
+ requireStep(doc, stepId);
116
+ const node = doc.getMap('qi.flow.nodes').get(stepId);
117
+ if (!(node instanceof Y.Map)) {
118
+ throw new FlowError('step_not_found', `No step "${stepId}" in this flow.`);
119
+ }
120
+ doc.transact(() => {
121
+ if (phase) node.set('phase', phase);
122
+ else node.delete('phase');
123
+ });
124
+ }
125
+
126
+ /** Persist the human-only versus agent-capable execution boundary. */
127
+ export function setStepExecution(
128
+ doc: YDoc,
129
+ stepId: string,
130
+ execution: 'human-only' | 'agent-capable',
131
+ ): void {
132
+ setStepProps(doc, stepId, {
133
+ flowAgentExecutionMode:
134
+ execution === 'human-only' ? 'human-only' : 'saved-input',
135
+ });
136
+ }
137
+
138
+ /** Persist governed skill requirements used by Flow Agent actor matching. */
139
+ export function setStepSkills(
140
+ doc: YDoc,
141
+ stepId: string,
142
+ skills: string[],
143
+ ): void {
144
+ setStepProps(doc, stepId, {
145
+ requiredSkill: skills[0] ?? '',
146
+ requiredSkills: skills.length > 0 ? JSON.stringify(skills) : '',
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Set a step's trigger to `manual` (default) or `flow-start`. Writes the same
152
+ * `trigger`/`triggerMode` props the compiler would. Event triggers are written
153
+ * by {@link setStepEventTrigger}; setting `manual` here clears one.
154
+ */
155
+ export function setStepTrigger(
156
+ doc: YDoc,
157
+ stepId: string,
158
+ trigger: 'manual' | 'flow-start',
159
+ ): void {
160
+ const type = trigger === 'flow-start' ? 'flow.start' : 'manual';
161
+ setStepProps(doc, stepId, {
162
+ trigger: trigger === 'flow-start' ? JSON.stringify({ type }) : '',
163
+ triggerMode: type,
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Auto-trigger a step when an upstream step emits an event. Writes the same
169
+ * `trigger`/`triggerMode` props the compiler would for a `block.event` trigger.
170
+ * The stored `sourceBlockId` carries the BLOCK id (`flow_block_<stepId>`) —
171
+ * that is what the FE reconciler matches against the source block's real `.id`
172
+ * — while callers keep passing the friendly step id.
173
+ */
174
+ export function setStepEventTrigger(
175
+ doc: YDoc,
176
+ stepId: string,
177
+ onEvent: { fromStep: string; event: string },
178
+ ): void {
179
+ if (onEvent.fromStep.startsWith(BLOCK_ID_PREFIX)) {
180
+ throw new FlowError(
181
+ 'validation_failed',
182
+ `"${onEvent.fromStep}" is an internal block id — use the short step id ("${blockIdToStepId(onEvent.fromStep)}").`,
183
+ );
184
+ }
185
+ const sourceBlockId = requireStep(doc, onEvent.fromStep);
186
+
187
+ // Same event-capability rule validate_flow enforces at plan level (spec §2.3).
188
+ const source = doc.getMap('qi.flow.nodes').get(onEvent.fromStep);
189
+ const registryType =
190
+ source instanceof Y.Map ? source.get('registryType') : undefined;
191
+ const can = source instanceof Y.Map ? source.get('can') : undefined;
192
+ const action =
193
+ typeof registryType === 'string'
194
+ ? registryType
195
+ : typeof can === 'string'
196
+ ? canToAction(can)
197
+ : undefined;
198
+ const def = action ? getActionDef(action) : undefined;
199
+ if (def && !isEventCapable(def)) {
200
+ throw new FlowError(
201
+ 'validation_failed',
202
+ `Step "${stepId}" can't auto-trigger on "${onEvent.fromStep}" — its action "${action}" cannot emit events. ` +
203
+ 'Use ordering (after) + an input reference instead.',
204
+ );
205
+ }
206
+
207
+ setStepProps(doc, stepId, {
208
+ trigger: JSON.stringify({
209
+ type: 'block.event',
210
+ sourceBlockId,
211
+ eventName: onEvent.event,
212
+ }),
213
+ triggerMode: 'block.event',
214
+ });
215
+ }
216
+
217
+ /** Steps that depend on `stepId` (via an input ref or a condition source). */
218
+ function referrersOf(doc: YDoc, ref: string, stepId: string): string[] {
219
+ const flow = readFlowSpec(doc, ref);
220
+ if (!flow) return [];
221
+ const referrers: string[] = [];
222
+ for (const step of flow.steps) {
223
+ if (step.id === stepId) continue;
224
+ const refsInput = Object.values(step.inputs ?? {}).some(
225
+ (v) =>
226
+ typeof v === 'string' && new RegExp(`\\{\\{\\s*${stepId}\\.`).test(v),
227
+ );
228
+ const conds = [
229
+ ...(step.runWhen ? [step.runWhen] : []),
230
+ ...(step.conditions ?? []),
231
+ ];
232
+ const refsCondition = conds.some((c) => c.fromStep === stepId);
233
+ const refsEvent = step.onEvent?.fromStep === stepId;
234
+ if (refsInput || refsCondition || refsEvent) referrers.push(step.id);
235
+ }
236
+ return referrers;
237
+ }
238
+
239
+ /** Remove a step entirely: fragment block + every `qi.flow.*` trace + runtime. Rejects if referenced. */
240
+ export function removeStep(doc: YDoc, ref: string, stepId: string): void {
241
+ const blockId = requireStep(doc, stepId);
242
+ const referrers = referrersOf(doc, ref, stepId);
243
+ if (referrers.length > 0) {
244
+ throw new FlowError(
245
+ 'referenced',
246
+ `Can't remove "${stepId}" — it is used by ${referrers.join(', ')}. Update those steps first.`,
247
+ );
248
+ }
249
+
250
+ doc.transact(() => {
251
+ doc.getMap('qi.flow.nodes').delete(stepId);
252
+ doc.getMap('qi.flow.blockIndex').delete(stepId);
253
+ doc.getMap('runtime').delete(blockId);
254
+
255
+ const order = doc.getArray<string>('qi.flow.order');
256
+ const idx = order.toArray().indexOf(stepId);
257
+ if (idx >= 0) order.delete(idx, 1);
258
+
259
+ const edges = doc.getMap('qi.flow.edges');
260
+ const toDelete: string[] = [];
261
+ edges.forEach((value, edgeId) => {
262
+ const edge = value as { source?: unknown; target?: unknown } | undefined;
263
+ if (edge && (edge.source === stepId || edge.target === stepId))
264
+ toDelete.push(edgeId);
265
+ });
266
+ for (const edgeId of toDelete) edges.delete(edgeId);
267
+ });
268
+
269
+ deleteBlock(doc, { blockId, docName: DOC });
270
+ }
271
+
272
+ /** Reorder a step to a new 0-based index. Sequence is display-only; runtime/edges untouched. */
273
+ export function reorderStep(doc: YDoc, stepId: string, toIndex: number): void {
274
+ const blockId = requireStep(doc, stepId);
275
+ const orderArr = doc.getArray<string>('qi.flow.order');
276
+ const current = orderArr.toArray();
277
+ const from = current.indexOf(stepId);
278
+ if (from < 0)
279
+ throw new FlowError('step_not_found', `No step "${stepId}" in this flow.`);
280
+
281
+ const target = Math.max(0, Math.min(toIndex, current.length - 1));
282
+ if (target === from) return;
283
+
284
+ const without = current.filter((id) => id !== stepId);
285
+ without.splice(target, 0, stepId);
286
+
287
+ doc.transact(() => {
288
+ // Move the fragment block by CLONE (yjs can't reinsert a deleted element).
289
+ const fragment = doc.getXmlFragment(DOC);
290
+ const found = findParentOf(fragment, blockId);
291
+ if (found) {
292
+ const element = found.parent.toArray()[found.index];
293
+ if (element instanceof Y.XmlElement) {
294
+ const clone = element.clone();
295
+ found.parent.delete(found.index, 1);
296
+ found.parent.insert(Math.min(target, found.parent.length), [clone]);
297
+ }
298
+ }
299
+ orderArr.delete(0, orderArr.length);
300
+ orderArr.push(without);
301
+ });
302
+ }
303
+
304
+ /** Update flow-level metadata (title / goal). */
305
+ export function updateFlowMeta(
306
+ doc: YDoc,
307
+ patch: { title?: string; goal?: string },
308
+ ): void {
309
+ const meta = doc.getMap('qi.flow.meta');
310
+ doc.transact(() => {
311
+ if (patch.title !== undefined) meta.set('title', patch.title);
312
+ if (patch.goal !== undefined) meta.set('goal', patch.goal);
313
+ });
314
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Uniform error model (spec §6.1). Tools never throw to the agent; expected
3
+ * failures come back as `{ ok: false, error: { code, message } }` with
4
+ * friendly, leak-safe messages.
5
+ */
6
+ export type FlowErrorCode =
7
+ | 'no_flow_ref'
8
+ | 'not_in_room'
9
+ | 'flow_not_found'
10
+ | 'validation_failed'
11
+ | 'step_not_found'
12
+ | 'referenced'
13
+ | 'unknown_action'
14
+ | 'error';
15
+
16
+ export class FlowError extends Error {
17
+ constructor(
18
+ readonly code: FlowErrorCode,
19
+ message: string,
20
+ ) {
21
+ super(message);
22
+ this.name = 'FlowError';
23
+ }
24
+ }
25
+
26
+ export interface ToolErrorResult {
27
+ ok: false;
28
+ error: { code: FlowErrorCode; message: string };
29
+ }
30
+
31
+ /** Normalize any thrown value into the structured tool-error result. */
32
+ export function toToolError(err: unknown): ToolErrorResult {
33
+ if (err instanceof FlowError) {
34
+ return { ok: false, error: { code: err.code, message: err.message } };
35
+ }
36
+ const message = err instanceof Error ? err.message : String(err);
37
+ return { ok: false, error: { code: 'error', message } };
38
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Plain-language step explanation (spec §3.6, `explain_step`). Read-only: it
3
+ * describes what a step will do and the inputs it will run with, so the agent
4
+ * can explain a step to the user before they run it. The action's diff resolver
5
+ * (Appendix A.7) is a later enhancement — this version reports intent + inputs.
6
+ */
7
+ import type { Doc as YDoc } from 'yjs';
8
+ import { describeAction } from './actions';
9
+ import { readFlowSpec } from './read';
10
+ import type { StepStatus } from './types';
11
+
12
+ export interface StepExplanation {
13
+ willDo: string;
14
+ action: string;
15
+ inputs: Record<string, unknown>;
16
+ requiresConfirmation: boolean;
17
+ status?: StepStatus;
18
+ }
19
+
20
+ export function explainStep(
21
+ doc: YDoc,
22
+ ref: string,
23
+ stepId: string,
24
+ ): StepExplanation | null {
25
+ const flow = readFlowSpec(doc, ref);
26
+ const step = flow?.steps.find((s) => s.id === stepId);
27
+ if (!step) return null;
28
+
29
+ const description = describeAction(step.action);
30
+ const summary = description?.summary?.trim();
31
+ const willDo = summary
32
+ ? summary
33
+ : `Runs the "${step.action}" action${step.title ? ` (${step.title})` : ''}.`;
34
+
35
+ return {
36
+ willDo,
37
+ action: step.action,
38
+ inputs: step.inputs ?? {},
39
+ requiresConfirmation:
40
+ step.requireConfirmation ?? description?.requiresConfirmation ?? false,
41
+ status: step.status,
42
+ };
43
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Connect to a flow's Matrix-backed Y.Doc and run a unit of work against it,
3
+ * then dispose. This is the only place the plugin touches Matrix; every read
4
+ * and mutation goes through `withFlowDoc`. We reuse the editor plugin's
5
+ * provider, config builder, and matrix-client resolver verbatim — the flows
6
+ * plugin never re-implements the connection.
7
+ *
8
+ * `flowRef` IS the Matrix room id (opaque to the agent). The default flow
9
+ * (when the agent omits `ref`) is the one bound to `state.editorRoomId`.
10
+ */
11
+ import type { Doc as YDoc } from 'yjs';
12
+ import type { MatrixClient } from 'matrix-js-sdk';
13
+ import { MatrixProviderManager, type AppConfig } from '../editor/provider';
14
+ import { buildBlocknoteToolsConfig } from '../editor/editor-config';
15
+ import { resolveEditorMatrixClient } from '../editor/editor-mx';
16
+ import { isUserInRoom } from '../editor/room-membership';
17
+ import type { RuntimeContext } from '../../plugin-api/types';
18
+ import { FlowError } from './errors';
19
+
20
+ interface MatrixCreds {
21
+ baseUrl: string;
22
+ userId: string;
23
+ accessToken: string;
24
+ }
25
+
26
+ async function readMatrixCreds(rtCtx: RuntimeContext): Promise<MatrixCreds> {
27
+ try {
28
+ const { baseUrl, userId, accessToken } =
29
+ await rtCtx.matrix.botCredentials();
30
+ return { baseUrl, userId, accessToken };
31
+ } catch (error) {
32
+ const detail = error instanceof Error ? error.message : String(error);
33
+ throw new FlowError(
34
+ 'error',
35
+ `Matrix is not available for this oracle: ${detail}`,
36
+ );
37
+ }
38
+ }
39
+
40
+ /** The flow handle for a request: the explicit `ref`, else the open flow's room. */
41
+ export function resolveFlowRef(rtCtx: RuntimeContext, ref?: string): string {
42
+ const candidate = ref ?? rtCtx.history.state.editorRoomId;
43
+ if (typeof candidate === 'string' && candidate.length > 0) return candidate;
44
+ throw new FlowError(
45
+ 'no_flow_ref',
46
+ 'No flow is open. Create a flow first, or specify which flow to use.',
47
+ );
48
+ }
49
+
50
+ /** Enforce the room-membership guard (fail closed). */
51
+ export async function requireRoomMembership(
52
+ rtCtx: RuntimeContext,
53
+ roomId: string,
54
+ ): Promise<void> {
55
+ if (!(await isUserInRoom(rtCtx, roomId, rtCtx.user.matrixUserId))) {
56
+ throw new FlowError('not_in_room', 'You do not have access to this flow.');
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Resolve a Matrix client for compile-driven authoring (`setupFlowFromBaseUcan`),
62
+ * which manages its own provider/doc rather than taking the one `withFlowDoc` opens.
63
+ */
64
+ export async function resolveFlowsMatrixClient(
65
+ rtCtx: RuntimeContext,
66
+ injectedClient: MatrixClient | undefined,
67
+ ): Promise<MatrixClient> {
68
+ return resolveEditorMatrixClient({
69
+ ...(await readMatrixCreds(rtCtx)),
70
+ matrixClient: injectedClient,
71
+ });
72
+ }
73
+
74
+ function buildAppConfig(creds: MatrixCreds, roomId: string): AppConfig {
75
+ const base = buildBlocknoteToolsConfig(creds);
76
+ return {
77
+ matrix: { ...base.matrix, room: { type: 'id', value: roomId } },
78
+ provider: { ...base.provider },
79
+ blocknote: { ...base.blocknote },
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Resolve the flow ref, enforce the room-membership guard (fail closed), open
85
+ * the room's Y.Doc, run `fn`, and always dispose the provider.
86
+ */
87
+ export async function withFlowDoc<T>(
88
+ rtCtx: RuntimeContext,
89
+ ref: string | undefined,
90
+ injectedClient: MatrixClient | undefined,
91
+ fn: (doc: YDoc, roomId: string) => Promise<T>,
92
+ ): Promise<T> {
93
+ const roomId = resolveFlowRef(rtCtx, ref);
94
+ await requireRoomMembership(rtCtx, roomId);
95
+
96
+ const creds = await readMatrixCreds(rtCtx);
97
+ const matrixClient = await resolveEditorMatrixClient({
98
+ ...creds,
99
+ matrixClient: injectedClient,
100
+ });
101
+ const manager = new MatrixProviderManager(
102
+ matrixClient,
103
+ buildAppConfig(creds, roomId),
104
+ );
105
+ try {
106
+ const { doc } = await manager.init();
107
+ return await fn(doc, roomId);
108
+ } finally {
109
+ await manager.dispose();
110
+ }
111
+ }