@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.
- package/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Builder operating guide — the detailed "how to act" prompt the runtime
|
|
3
|
+
* injects into the main agent's system prompt while the flows capability is
|
|
4
|
+
* loaded (see `main-agent.ts`, gated on `loadedPlugins` containing
|
|
5
|
+
* {@link FLOWS_PLUGIN_NAME}). It is the durable counterpart to the manifest
|
|
6
|
+
* that `load_capability` returns: the manifest sells the capability at
|
|
7
|
+
* discovery time; this guide governs behaviour for the whole flow-building
|
|
8
|
+
* session.
|
|
9
|
+
*
|
|
10
|
+
* Kept here (not in the manifest `summary`) on purpose: the summary is a
|
|
11
|
+
* one-line discovery teaser shown by `list_capabilities`, while this guide is
|
|
12
|
+
* the multi-paragraph operating contract — too long for the summary, and only
|
|
13
|
+
* worth its tokens once the agent is actually building a flow.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The plugin's registered name — single source of truth for the load gate. */
|
|
17
|
+
export const FLOWS_PLUGIN_NAME = 'flows';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The operating guide. Starts with its own `### Flow Builder mode` sub-header
|
|
21
|
+
* so it nests cleanly under the runtime's `## Custom Instructions` section
|
|
22
|
+
* wherever the composer slots it in.
|
|
23
|
+
*/
|
|
24
|
+
export const FLOWS_OPERATING_GUIDE = `### Flow Builder mode
|
|
25
|
+
|
|
26
|
+
You have loaded the **Flow Builder** capability. Use it whenever the user wants to build, change, inspect, or fill in a flow (an automation/workflow). Follow this guide while it is loaded.
|
|
27
|
+
|
|
28
|
+
**What you are:** a flow *builder*, not a runner. You author a reusable flow **template** — its steps (action blocks), the data wired between them, conditions, schedules, assignees, and forms — and you read a flow's live state. You **never** execute, run, sign, mint, enter a PIN, or hold a key. The **user runs the flow in the portal**, where signing, PINs, and any money movement happen. Never claim to have run, signed, or executed a step.
|
|
29
|
+
|
|
30
|
+
**The build loop — discover → plan → confirm → build → hand off.** Don't overthink it: one discovery pass, one short plan, one confirmation, then build.
|
|
31
|
+
|
|
32
|
+
1. **Discover (know the blocks before you plan).** Work out which action blocks the automation needs and look up *exactly* what each one requires before you promise anything:
|
|
33
|
+
- \`list_actions\` to find candidate blocks; \`describe_action\` on each to get its real inputs, outputs, and whether it is a form.
|
|
34
|
+
- \`requirements\` on each block to get the prerequisites the **user** must supply — e.g. a claim collection, a connected account, a template id, a recipient, an endpoint, a PIN, a funding amount. These are things you must ASK for; never invent them.
|
|
35
|
+
Do this as a quiet tool pass — not a wall of text to the user.
|
|
36
|
+
|
|
37
|
+
2. **Plan (planning mode — this is the first thing the user sees).** Before writing anything, present a short, plain-language plan and STOP for approval. For each step, in one line: **"I'll use the \`<action>\` block — it <does X>"**; then how the steps connect (which output feeds which input), plus any form, schedule, assignee, or condition. End the plan with a clear bulleted list of **what you need from the user** — every \`requires\` you could not fill yourself. Keep it tight: a numbered list, not an essay.
|
|
38
|
+
|
|
39
|
+
3. **Confirm.** Ask the user to confirm or adjust the plan, and wait for a yes (or their edits). Do **not** author the flow until they confirm. Ask once, as a single plan — don't drip one question at a time.
|
|
40
|
+
|
|
41
|
+
4. **Build (only after confirmation).** Run \`validate_flow\` first, then:
|
|
42
|
+
- New flow → \`create_template\`. Editing the flow that is already open → the edit tools (\`add_step\`, \`update_step\`, \`remove_step\`, \`reorder_step\`, \`set_step_*\`, \`connect_steps\`). \`create_template\` makes a BRAND-NEW flow in its own room — never use it to change an existing or open flow.
|
|
43
|
+
- Set a step's literal input values with \`set_step_inputs\`; wire data between steps with \`connect_steps\` (one upstream output → one downstream input per call). Use \`list_referenceable_fields\` to see what a step can pull from upstream, and \`check_link\` / \`compatible_actions\` to verify a wire before relying on it.
|
|
44
|
+
- For a **form** step you MUST call \`set_form_schema\` to define its questions — a form with no questions cannot run. \`fill_form\` only pre-fills answers; it never submits.
|
|
45
|
+
- For a phased product journey, set each step's stable \`phase\`. Mark consequential approvals and publication \`human-only\`. Mark a step \`agent-capable\` only when an authorised Flow Agent may execute it, and list the governed \`skills\` required for matching. Never invent agent roles as a substitute for skill ids.
|
|
46
|
+
- Apply conditions, schedule, assignee, and confirmation with the matching \`set_step_*\` tool.
|
|
47
|
+
|
|
48
|
+
5. **Hand off.** Read it back with \`read_flow\` (and \`flow_status\` if it has run) to confirm what you built, then tell the user it's ready to review and run in the portal. You don't run it.
|
|
49
|
+
|
|
50
|
+
**Wiring & sequencing (verified — get this right):**
|
|
51
|
+
- Sequence with **step order + data references.** A step that reads \`{{a.output.x}}\` is not ready until step \`a\` has produced that output — that is the real dependency, and it works for every block.
|
|
52
|
+
- **Always wire step-to-step data with \`connect_steps\`, never \`set_step_inputs\`.** \`set_step_inputs\` REPLACES the step's entire inputs map — using it to add one wire silently drops every other input already configured on that step (e.g. the email block loses its recipient). \`connect_steps\` merges a single \`{{from.output.field}}\` reference into the existing inputs. Reserve \`set_step_inputs\` for setting a step's full set of values in one call.
|
|
53
|
+
- **Auto-run a step when an upstream step finishes:** set that step's \`onEvent\` to \`{ fromStep: "<upstream-step-id>", event: "<event-name>" }\`. This is the ONLY thing that auto-fires a downstream block. Use \`describe_action\` to see a step's emitted events. Example: a **form** step emits \`form.submitted\` — to send an email the moment the form is submitted, set the email step's \`onEvent\` to \`{ fromStep: "<form-step>", event: "form.submitted" }\` and wire its inputs from \`{{<form-step>.output.answers.<field>}}\`.
|
|
54
|
+
- **Do NOT** use \`after\` (ordering only — it does NOT auto-fire) or lifecycle hooks (e.g. \`on.sendEmail\`, which is a separate platform-email side effect, NOT a way to trigger an integration block) to auto-run a step. Only \`onEvent\` auto-fires.
|
|
55
|
+
- \`set_step_trigger\` sets \`manual\` vs \`flow-start\`. \`onEvent\` is only valid when the upstream action actually emits events (\`validate_flow\` enforces this); otherwise fall back to order + data references and the user runs the step manually.
|
|
56
|
+
- \`set_step_conditions\` gate on an upstream step's **configured** value, not its live runtime result.
|
|
57
|
+
|
|
58
|
+
**Asking vs guessing:** STOP and ask for any \`requires\` you can't fill from context (collection ids, DIDs, recipients, connected accounts, template names, endpoints, PINs, amounts). Never fabricate them. But don't ask for things the portal collects at run time, or that you can reasonably infer.
|
|
59
|
+
|
|
60
|
+
**Status is read-only.** \`flow_status\` / \`get_step\` / \`explain_step\` report the user's real runs — use them to report progress or to diagnose a failed step and fix the template. Never invent a step's state, a transaction result, or a proof.
|
|
61
|
+
|
|
62
|
+
**Tools at a glance.** Discover: \`list_actions\`, \`describe_action\`, \`requirements\`, \`list_referenceable_fields\`. Inspect: \`read_flow\`, \`get_step\`, \`flow_status\`, \`explain_step\`. Author: \`validate_flow\`, \`create_template\`, \`add_step\`, \`update_step\`, \`remove_step\`, \`reorder_step\`, \`connect_steps\`, \`update_flow_meta\`. Tune: \`set_step_inputs\`, \`set_step_conditions\`, \`set_step_schedule\`, \`set_step_assignment\`, \`set_step_confirmation\`, \`set_step_trigger\`. Link: \`check_link\`, \`compatible_actions\`. Forms: \`set_form_schema\`, \`describe_form\`, \`fill_form\`.
|
|
63
|
+
|
|
64
|
+
**Step ids are short everywhere.** Always use the short step id in \`onEvent.fromStep\` and in \`{{step-id.output.field}}\` references (e.g. \`{{it-request-form.output.answers.email}}\`). After configuring an \`onEvent\` trigger, report back to the user exactly what it does: which step fires, on which event, and which downstream step it triggers.
|
|
65
|
+
`;
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-source read assembler (spec §4.1). The editor stores a flow across
|
|
3
|
+
* three places and per-block props live only in the document fragment, so a
|
|
4
|
+
* single read path is lossy. `readFlowSpec` gathers from all of them:
|
|
5
|
+
* - the `qi.flow.*` maps -> structure (nodes/order/edges/registryType)
|
|
6
|
+
* - the `document` XmlFragment -> per-block props (inputs/conditions/trigger)
|
|
7
|
+
* - the `runtime` map -> per-step status/error/output
|
|
8
|
+
*
|
|
9
|
+
* We read the `qi.flow.*` maps with oracle-runtime's own yjs rather than the
|
|
10
|
+
* editor package's `readCompiledFlowFromYDoc`: the editor bundles a different
|
|
11
|
+
* yjs minor, and its reader does `value instanceof Y.Map` against ITS yjs, which
|
|
12
|
+
* is false for the native nested maps in the plugin's provider doc — so it
|
|
13
|
+
* returns empty nodes cross-version. Reading the maps directly here (reading,
|
|
14
|
+
* never compiling) keeps every doc access on a single yjs instance. Per-block
|
|
15
|
+
* props + runtime are read via the editor plugin's own (same-yjs) helpers.
|
|
16
|
+
*/
|
|
17
|
+
import type { Doc as YDoc } from 'yjs';
|
|
18
|
+
import * as Y from 'yjs';
|
|
19
|
+
import {
|
|
20
|
+
collectAllBlocks,
|
|
21
|
+
extractBlockProperties,
|
|
22
|
+
readRuntimeState,
|
|
23
|
+
} from './ydoc-helpers';
|
|
24
|
+
import {
|
|
25
|
+
blockIdToStepId,
|
|
26
|
+
canToAction,
|
|
27
|
+
nbToFriendlyInputs,
|
|
28
|
+
parseConditionsProp,
|
|
29
|
+
} from './translator';
|
|
30
|
+
import type {
|
|
31
|
+
FlowSpecRead,
|
|
32
|
+
FlowStepRead,
|
|
33
|
+
StepState,
|
|
34
|
+
StepStatus,
|
|
35
|
+
} from './types';
|
|
36
|
+
|
|
37
|
+
const STEP_STATES: readonly StepState[] = [
|
|
38
|
+
'idle',
|
|
39
|
+
'running',
|
|
40
|
+
'completed',
|
|
41
|
+
'failed',
|
|
42
|
+
'cancelled',
|
|
43
|
+
'awaiting_readback',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === 'object' && value !== null;
|
|
48
|
+
}
|
|
49
|
+
function asString(value: unknown): string | undefined {
|
|
50
|
+
return typeof value === 'string' ? value : undefined;
|
|
51
|
+
}
|
|
52
|
+
function asNumber(value: unknown): number | undefined {
|
|
53
|
+
return typeof value === 'number' ? value : undefined;
|
|
54
|
+
}
|
|
55
|
+
function asStepState(value: unknown): StepState | undefined {
|
|
56
|
+
return STEP_STATES.includes(value as StepState)
|
|
57
|
+
? (value as StepState)
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Parse a value that may be a JSON string (fragment attribute) or already an object. */
|
|
62
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
63
|
+
if (isRecord(value)) return value;
|
|
64
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(value);
|
|
67
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface FlowNode {
|
|
76
|
+
can: string;
|
|
77
|
+
registryType?: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
phase?: string;
|
|
81
|
+
}
|
|
82
|
+
interface CompiledStructure {
|
|
83
|
+
meta: { title: string; goal?: string };
|
|
84
|
+
nodes: Record<string, FlowNode>;
|
|
85
|
+
order: string[];
|
|
86
|
+
blockIndex: Record<string, string>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Read the compiled `qi.flow.*` graph natively (same yjs as the doc). */
|
|
90
|
+
function readCompiledStructure(doc: YDoc): CompiledStructure | null {
|
|
91
|
+
const order = doc
|
|
92
|
+
.getArray('qi.flow.order')
|
|
93
|
+
.toArray()
|
|
94
|
+
.filter((v): v is string => typeof v === 'string');
|
|
95
|
+
if (order.length === 0) return null;
|
|
96
|
+
|
|
97
|
+
const meta = doc.getMap('qi.flow.meta');
|
|
98
|
+
const nodes: Record<string, FlowNode> = {};
|
|
99
|
+
doc.getMap('qi.flow.nodes').forEach((value, nodeId) => {
|
|
100
|
+
if (!(value instanceof Y.Map)) return;
|
|
101
|
+
const can = asString(value.get('can')) ?? '';
|
|
102
|
+
nodes[nodeId] = {
|
|
103
|
+
can,
|
|
104
|
+
registryType: asString(value.get('registryType')),
|
|
105
|
+
title: asString(value.get('title')),
|
|
106
|
+
description: asString(value.get('description')),
|
|
107
|
+
phase: asString(value.get('phase')),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const blockIndex: Record<string, string> = {};
|
|
112
|
+
doc.getMap('qi.flow.blockIndex').forEach((value, key) => {
|
|
113
|
+
if (typeof value === 'string') blockIndex[key] = value;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
meta: {
|
|
118
|
+
title: asString(meta.get('title')) ?? '',
|
|
119
|
+
goal: asString(meta.get('goal')),
|
|
120
|
+
},
|
|
121
|
+
nodes,
|
|
122
|
+
order,
|
|
123
|
+
blockIndex,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface TriggerProp {
|
|
128
|
+
type?: string;
|
|
129
|
+
sourceBlockId?: string;
|
|
130
|
+
eventName?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Parse the `trigger` prop (a JSON-serialized TriggerSpec) the compiler writes. */
|
|
134
|
+
function parseTriggerProp(value: unknown): TriggerProp | undefined {
|
|
135
|
+
const obj = asObject(value);
|
|
136
|
+
if (!obj) return undefined;
|
|
137
|
+
return {
|
|
138
|
+
type: asString(obj.type),
|
|
139
|
+
sourceBlockId: asString(obj.sourceBlockId),
|
|
140
|
+
eventName: asString(obj.eventName),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** First authorised actor from the `authorisedActors` prop (a JSON array string). */
|
|
145
|
+
function firstActor(value: unknown): string | undefined {
|
|
146
|
+
if (typeof value !== 'string' || value.length === 0) return undefined;
|
|
147
|
+
try {
|
|
148
|
+
const parsed: unknown = JSON.parse(value);
|
|
149
|
+
if (Array.isArray(parsed) && typeof parsed[0] === 'string')
|
|
150
|
+
return parsed[0];
|
|
151
|
+
} catch {
|
|
152
|
+
/* ignore */
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function stringList(value: unknown): string[] {
|
|
158
|
+
if (typeof value !== 'string' || value.length === 0) return [];
|
|
159
|
+
try {
|
|
160
|
+
const parsed: unknown = JSON.parse(value);
|
|
161
|
+
return Array.isArray(parsed)
|
|
162
|
+
? parsed.filter(
|
|
163
|
+
(item): item is string => typeof item === 'string' && item.length > 0,
|
|
164
|
+
)
|
|
165
|
+
: [];
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The assignee DID from the `assignment` prop (`{ assignedActor: { did } }` JSON). */
|
|
172
|
+
function assigneeFrom(value: unknown): string | undefined {
|
|
173
|
+
if (typeof value !== 'string' || value.length === 0) return undefined;
|
|
174
|
+
try {
|
|
175
|
+
const parsed: unknown = JSON.parse(value);
|
|
176
|
+
if (parsed && typeof parsed === 'object') {
|
|
177
|
+
const actor = (parsed as { assignedActor?: unknown }).assignedActor;
|
|
178
|
+
if (actor && typeof actor === 'object') {
|
|
179
|
+
const did = (actor as { did?: unknown }).did;
|
|
180
|
+
if (typeof did === 'string' && did.length > 0) return did;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
/* ignore */
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Collect the upstream step ids this step refers to via "{{step.output.*}}" inputs. */
|
|
190
|
+
function referencedSteps(
|
|
191
|
+
inputs: Record<string, unknown> | undefined,
|
|
192
|
+
): string[] {
|
|
193
|
+
if (!inputs) return [];
|
|
194
|
+
const refs = new Set<string>();
|
|
195
|
+
for (const value of Object.values(inputs)) {
|
|
196
|
+
if (typeof value === 'string') {
|
|
197
|
+
const match = /^\{\{\s*([^.}\s]+)\./.exec(value);
|
|
198
|
+
if (match?.[1]) refs.add(match[1]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [...refs];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Derive a step's read-only status from its runtime entry + its dependencies' states. */
|
|
205
|
+
function computeStatus(
|
|
206
|
+
runtime: Record<string, unknown> | undefined,
|
|
207
|
+
dependencies: { referenced: string[]; after: string[] },
|
|
208
|
+
stateByStepId: Map<string, StepState>,
|
|
209
|
+
): StepStatus {
|
|
210
|
+
const state = asStepState(runtime?.state) ?? 'idle';
|
|
211
|
+
const status: StepStatus = { state };
|
|
212
|
+
|
|
213
|
+
const error = isRecord(runtime?.error) ? runtime.error : undefined;
|
|
214
|
+
if (error && typeof error.message === 'string') {
|
|
215
|
+
status.error = {
|
|
216
|
+
message: error.message,
|
|
217
|
+
code: asString(error.code),
|
|
218
|
+
at: asNumber(error.at),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const executedAt = asNumber(runtime?.executedAt);
|
|
222
|
+
if (executedAt !== undefined) status.lastRunAt = executedAt;
|
|
223
|
+
|
|
224
|
+
const blockedBy = new Set<string>();
|
|
225
|
+
for (const dep of [...dependencies.referenced, ...dependencies.after]) {
|
|
226
|
+
const depState = stateByStepId.get(dep);
|
|
227
|
+
const isReferenced = dependencies.referenced.includes(dep);
|
|
228
|
+
if (depState === 'failed' || (isReferenced && depState !== 'completed')) {
|
|
229
|
+
blockedBy.add(dep);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (blockedBy.size > 0) status.blockedBy = [...blockedBy];
|
|
233
|
+
|
|
234
|
+
// Completed without a proof receipt -> flag as stale so the agent doesn't trust it.
|
|
235
|
+
if (state === 'completed') {
|
|
236
|
+
const output = isRecord(runtime?.output) ? runtime.output : {};
|
|
237
|
+
const hasProof =
|
|
238
|
+
typeof output.transactionHash === 'string' ||
|
|
239
|
+
typeof output.claimId === 'string' ||
|
|
240
|
+
typeof runtime?.claimId === 'string';
|
|
241
|
+
if (!hasProof) status.stale = true;
|
|
242
|
+
}
|
|
243
|
+
return status;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Assemble a FlowSpec from a connected flow-room Y.Doc. Returns null when the
|
|
248
|
+
* doc holds no compiled flow (caller surfaces `flow_not_found`).
|
|
249
|
+
*/
|
|
250
|
+
export function readFlowSpec(doc: YDoc, ref: string): FlowSpecRead | null {
|
|
251
|
+
const compiled = readCompiledStructure(doc);
|
|
252
|
+
if (!compiled) return null;
|
|
253
|
+
|
|
254
|
+
// Per-block props, keyed by block id.
|
|
255
|
+
const fragment = doc.getXmlFragment('document');
|
|
256
|
+
const propsByBlockId = new Map<string, Record<string, unknown>>();
|
|
257
|
+
for (const block of collectAllBlocks(fragment)) {
|
|
258
|
+
propsByBlockId.set(block.id, extractBlockProperties(block));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const runtimeByBlockId = readRuntimeState(doc);
|
|
262
|
+
const blockToStep = new Map<string, string>();
|
|
263
|
+
for (const [nodeId, blockId] of Object.entries(compiled.blockIndex)) {
|
|
264
|
+
blockToStep.set(blockId, nodeId);
|
|
265
|
+
}
|
|
266
|
+
const resolveStep = (blockId: string): string =>
|
|
267
|
+
blockToStep.get(blockId) ?? blockId;
|
|
268
|
+
|
|
269
|
+
// First pass: assemble each step (without status) + record raw runtime state.
|
|
270
|
+
const steps: FlowStepRead[] = [];
|
|
271
|
+
const runtimeByStep = new Map<string, Record<string, unknown> | undefined>();
|
|
272
|
+
const dependenciesByStep = new Map<
|
|
273
|
+
string,
|
|
274
|
+
{ referenced: string[]; after: string[] }
|
|
275
|
+
>();
|
|
276
|
+
const stateByStepId = new Map<string, StepState>();
|
|
277
|
+
|
|
278
|
+
for (const nodeId of compiled.order) {
|
|
279
|
+
const node = compiled.nodes[nodeId];
|
|
280
|
+
if (!node) continue;
|
|
281
|
+
const blockId = compiled.blockIndex[nodeId];
|
|
282
|
+
const props = (blockId && propsByBlockId.get(blockId)) || {};
|
|
283
|
+
const runtime = blockId ? runtimeByBlockId[blockId] : undefined;
|
|
284
|
+
|
|
285
|
+
const inputs = nbToFriendlyInputs(asObject(props.inputs));
|
|
286
|
+
const conditions = parseConditionsProp(props.conditions, resolveStep);
|
|
287
|
+
|
|
288
|
+
const step: FlowStepRead = {
|
|
289
|
+
id: nodeId,
|
|
290
|
+
action: node.registryType ?? canToAction(node.can),
|
|
291
|
+
};
|
|
292
|
+
if (node.title && node.title !== node.can) step.title = node.title;
|
|
293
|
+
if (node.description) step.description = node.description;
|
|
294
|
+
if (node.phase) step.phase = node.phase;
|
|
295
|
+
if (props.flowAgentExecutionMode === 'human-only') {
|
|
296
|
+
step.execution = 'human-only';
|
|
297
|
+
} else if (
|
|
298
|
+
props.flowAgentExecutionMode === 'saved-input' ||
|
|
299
|
+
props.flowAgentExecutionMode === 'runtime-input-required'
|
|
300
|
+
) {
|
|
301
|
+
step.execution = 'agent-capable';
|
|
302
|
+
}
|
|
303
|
+
const skills = stringList(props.requiredSkills);
|
|
304
|
+
const primarySkill = asString(props.requiredSkill);
|
|
305
|
+
if (skills.length > 0) step.skills = skills;
|
|
306
|
+
else if (primarySkill) step.skills = [primarySkill];
|
|
307
|
+
if (inputs) step.inputs = inputs;
|
|
308
|
+
if (conditions.length === 1 && conditions[0]) step.runWhen = conditions[0];
|
|
309
|
+
else if (conditions.length > 1) step.conditions = conditions;
|
|
310
|
+
|
|
311
|
+
const trigger = parseTriggerProp(props.trigger);
|
|
312
|
+
if (
|
|
313
|
+
trigger?.type === 'block.event' &&
|
|
314
|
+
trigger.sourceBlockId &&
|
|
315
|
+
trigger.eventName
|
|
316
|
+
) {
|
|
317
|
+
// Compiled props store the BLOCK id; the agent always sees step ids.
|
|
318
|
+
step.onEvent = {
|
|
319
|
+
fromStep: blockIdToStepId(trigger.sourceBlockId),
|
|
320
|
+
event: trigger.eventName,
|
|
321
|
+
};
|
|
322
|
+
} else if (trigger?.type === 'flow.start') {
|
|
323
|
+
step.trigger = 'flow-start';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const due: { at?: string; within?: string } = {};
|
|
327
|
+
const at = asString(props.ttlAbsoluteDueDate);
|
|
328
|
+
const within = asString(props.ttlFromEnablement);
|
|
329
|
+
if (at) due.at = at;
|
|
330
|
+
if (within) due.within = within;
|
|
331
|
+
if (Object.keys(due).length > 0) step.due = due;
|
|
332
|
+
const commitTo = asString(props.ttlFromCommitment);
|
|
333
|
+
if (commitTo) step.commitTo = commitTo;
|
|
334
|
+
|
|
335
|
+
// The assignee lives in props.assignment (what the portal reads); fall back
|
|
336
|
+
// to the authorisedActors whitelist for flows authored before that split.
|
|
337
|
+
const assignTo =
|
|
338
|
+
assigneeFrom(props.assignment) ?? firstActor(props.authorisedActors);
|
|
339
|
+
if (assignTo) step.assignTo = assignTo;
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
props.requiresConfirmation === 'true' ||
|
|
343
|
+
props.requiresConfirmation === true
|
|
344
|
+
) {
|
|
345
|
+
step.requireConfirmation = true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
steps.push(step);
|
|
349
|
+
runtimeByStep.set(nodeId, runtime);
|
|
350
|
+
stateByStepId.set(nodeId, asStepState(runtime?.state) ?? 'idle');
|
|
351
|
+
dependenciesByStep.set(nodeId, {
|
|
352
|
+
referenced: referencedSteps(inputs),
|
|
353
|
+
after: [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Second pass: now that every step's state is known, compute status.
|
|
358
|
+
for (const step of steps) {
|
|
359
|
+
step.status = computeStatus(
|
|
360
|
+
runtimeByStep.get(step.id),
|
|
361
|
+
dependenciesByStep.get(step.id) ?? { referenced: [], after: [] },
|
|
362
|
+
stateByStepId,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const result: FlowSpecRead = { ref, title: compiled.meta.title, steps };
|
|
367
|
+
if (compiled.meta.goal) result.goal = compiled.meta.goal;
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Read a single step (with status) from a connected doc. */
|
|
372
|
+
export function readStep(
|
|
373
|
+
doc: YDoc,
|
|
374
|
+
ref: string,
|
|
375
|
+
stepId: string,
|
|
376
|
+
): FlowStepRead | null {
|
|
377
|
+
const flow = readFlowSpec(doc, ref);
|
|
378
|
+
return flow?.steps.find((s) => s.id === stepId) ?? null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Read just the per-step status (the `flow_status` projection). */
|
|
382
|
+
export function readFlowStatus(
|
|
383
|
+
doc: YDoc,
|
|
384
|
+
ref: string,
|
|
385
|
+
): Array<{ id: string } & StepStatus> | null {
|
|
386
|
+
const flow = readFlowSpec(doc, ref);
|
|
387
|
+
if (!flow) return null;
|
|
388
|
+
return flow.steps.map((s) => ({
|
|
389
|
+
id: s.id,
|
|
390
|
+
...(s.status ?? { state: 'idle' as const }),
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field references (spec §2.4, the `list_referenceable_fields` discovery tool).
|
|
3
|
+
* Returns the friendly output fields a step can pipe from its upstream steps,
|
|
4
|
+
* so the agent never has to guess a "{{step.output.field}}" string.
|
|
5
|
+
*/
|
|
6
|
+
import type { Doc as YDoc } from 'yjs';
|
|
7
|
+
import { ACTION_METADATA } from './action-metadata';
|
|
8
|
+
import { getActionDef, isFormAction } from './actions';
|
|
9
|
+
import { formOutputFields } from './forms';
|
|
10
|
+
import { readFlowSpec } from './read';
|
|
11
|
+
|
|
12
|
+
export interface ReferenceableField {
|
|
13
|
+
fromStep: string;
|
|
14
|
+
field: string;
|
|
15
|
+
type: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Output fields available to `stepId` — gathered from every step ahead of it in
|
|
20
|
+
* the flow (their outputs may be produced before this step runs). Uses the
|
|
21
|
+
* action's output schema, enriched by the metadata overlay's output ports.
|
|
22
|
+
*/
|
|
23
|
+
export function listReferenceableFields(
|
|
24
|
+
doc: YDoc,
|
|
25
|
+
ref: string,
|
|
26
|
+
stepId: string,
|
|
27
|
+
): ReferenceableField[] {
|
|
28
|
+
const flow = readFlowSpec(doc, ref);
|
|
29
|
+
if (!flow) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const index = flow.steps.findIndex((s) => s.id === stepId);
|
|
34
|
+
|
|
35
|
+
const upstream =
|
|
36
|
+
index >= 0
|
|
37
|
+
? flow.steps.slice(0, index)
|
|
38
|
+
: flow.steps.filter((s) => s.id !== stepId);
|
|
39
|
+
|
|
40
|
+
const out: ReferenceableField[] = [];
|
|
41
|
+
for (const step of upstream) {
|
|
42
|
+
const def = getActionDef(step.action);
|
|
43
|
+
// Form steps emit one bundled `answers` output, not a field per question.
|
|
44
|
+
// Expand to `answers`, `form.answers`, and the individual `answers.<name>`
|
|
45
|
+
// runtime paths so a downstream scalar input (e.g. a DID) can reference the
|
|
46
|
+
// single field instead of the whole object (which stringifies to
|
|
47
|
+
// "[object Object]"). The action's outputSchema only declares the bundle.
|
|
48
|
+
if (def && isFormAction(def)) {
|
|
49
|
+
const formFields = formOutputFields(doc, step.id);
|
|
50
|
+
for (const f of formFields) {
|
|
51
|
+
out.push({ fromStep: step.id, field: f.field, type: f.type });
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const overlayPorts = ACTION_METADATA[step.action]?.outputPorts ?? [];
|
|
56
|
+
for (const port of overlayPorts) {
|
|
57
|
+
out.push({ fromStep: step.id, field: port.path, type: port.portType });
|
|
58
|
+
}
|
|
59
|
+
for (const field of def?.outputSchema ?? []) {
|
|
60
|
+
if (!overlayPorts.some((p) => p.path === field.path)) {
|
|
61
|
+
out.push({ fromStep: step.id, field: field.path, type: field.type });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test support: build a compiled-flow Y.Doc offline (no Matrix). Used by the
|
|
3
|
+
* read/edit unit tests so they exercise the real compiled-flow shape.
|
|
4
|
+
*
|
|
5
|
+
* Why we hydrate with oracle-runtime's own yjs instead of the editor's
|
|
6
|
+
* `hydrateYDocFromCompiledFlow`: the editor bundles a different yjs minor than
|
|
7
|
+
* oracle-runtime, and inserting the editor's Y.Map/Y.XmlElement instances into
|
|
8
|
+
* a doc created here throws ("Unexpected content type"). In production the
|
|
9
|
+
* plugin never does that — flows are authored in the editor's own doc and reach
|
|
10
|
+
* the plugin's provider doc as version-agnostic binary CRDT updates, so the
|
|
11
|
+
* plugin's doc is always natively this yjs. We reproduce that native doc here.
|
|
12
|
+
*
|
|
13
|
+
* Only the trivial Y.Doc *writing* is mirrored (the layout is verified and
|
|
14
|
+
* guarded by the read round-trip + the action snapshot canary); the compiler
|
|
15
|
+
* itself is still `compileBaseUcanFlow`. Not a `*.test.ts` file.
|
|
16
|
+
*/
|
|
17
|
+
import * as Y from 'yjs';
|
|
18
|
+
import {
|
|
19
|
+
compileBaseUcanFlow,
|
|
20
|
+
getActionByCan,
|
|
21
|
+
getAllActions,
|
|
22
|
+
typeToCan,
|
|
23
|
+
} from '@ixo/editor/core';
|
|
24
|
+
import type {
|
|
25
|
+
BaseUcanFlow,
|
|
26
|
+
CompiledFlow,
|
|
27
|
+
FlowNodeRuntimeState,
|
|
28
|
+
} from '@ixo/editor/core';
|
|
29
|
+
import { isEventCapable } from './actions';
|
|
30
|
+
import { stepIdToBlockId } from './translator';
|
|
31
|
+
|
|
32
|
+
/** A real registry action type with a `can`, picked dynamically so tests never hardcode a stale name. */
|
|
33
|
+
export function someActionType(): string {
|
|
34
|
+
const withCan = getAllActions().find(
|
|
35
|
+
(a) => typeof typeToCan(a.type) === 'string',
|
|
36
|
+
);
|
|
37
|
+
if (!withCan) throw new Error('no action with a `can` in the registry');
|
|
38
|
+
return withCan.type;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A registry action that can source an `onEvent` trigger (event-capable, with a `can`). */
|
|
42
|
+
export function someEventCapableActionType(): string {
|
|
43
|
+
const match = getAllActions().find(
|
|
44
|
+
(a) => typeof typeToCan(a.type) === 'string' && isEventCapable(a),
|
|
45
|
+
);
|
|
46
|
+
if (!match) throw new Error('no event-capable action in the registry');
|
|
47
|
+
return match.type;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A registry action that can NOT source an `onEvent` trigger, if the registry has one. */
|
|
51
|
+
export function someNonEventActionType(): string | undefined {
|
|
52
|
+
return getAllActions().find(
|
|
53
|
+
(a) => typeof typeToCan(a.type) === 'string' && !isEventCapable(a),
|
|
54
|
+
)?.type;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeNodeMap(doc: Y.Doc, compiled: CompiledFlow): void {
|
|
58
|
+
const nodes = doc.getMap('qi.flow.nodes');
|
|
59
|
+
for (const nodeId of Object.keys(compiled.nodes)) {
|
|
60
|
+
const node = compiled.nodes[nodeId];
|
|
61
|
+
if (!node) continue;
|
|
62
|
+
const yNode = new Y.Map<unknown>();
|
|
63
|
+
yNode.set('id', node.id);
|
|
64
|
+
yNode.set('blockId', node.blockId);
|
|
65
|
+
yNode.set('can', node.can);
|
|
66
|
+
yNode.set('with', node.with);
|
|
67
|
+
yNode.set('registryType', node.registryType);
|
|
68
|
+
yNode.set('title', node.title);
|
|
69
|
+
yNode.set('description', node.description);
|
|
70
|
+
if (node.phase) yNode.set('phase', node.phase);
|
|
71
|
+
if (node.parallelGroup) yNode.set('parallelGroup', node.parallelGroup);
|
|
72
|
+
if (node.actor) yNode.set('actor', node.actor);
|
|
73
|
+
nodes.set(nodeId, yNode);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeFragment(doc: Y.Doc, compiled: CompiledFlow): void {
|
|
78
|
+
// Mirror documentFragment.ts `createBlockContainer`: blockGroup > blockContainer[id] >
|
|
79
|
+
// blockContent[type] with each prop as a direct attribute (no `attrs` wrapper).
|
|
80
|
+
const fragment = doc.getXmlFragment('document');
|
|
81
|
+
const blockGroup = new Y.XmlElement('blockGroup');
|
|
82
|
+
for (const block of compiled.blocks) {
|
|
83
|
+
const props = block.props as Record<string, unknown>;
|
|
84
|
+
const { backgroundColor, textColor, ...contentProps } = props;
|
|
85
|
+
const container = new Y.XmlElement('blockContainer');
|
|
86
|
+
container.setAttribute('id', block.id);
|
|
87
|
+
container.setAttribute(
|
|
88
|
+
'textColor',
|
|
89
|
+
typeof textColor === 'string' ? textColor : 'default',
|
|
90
|
+
);
|
|
91
|
+
container.setAttribute(
|
|
92
|
+
'backgroundColor',
|
|
93
|
+
typeof backgroundColor === 'string' ? backgroundColor : 'default',
|
|
94
|
+
);
|
|
95
|
+
const content = new Y.XmlElement(block.type);
|
|
96
|
+
for (const [key, value] of Object.entries(contentProps)) {
|
|
97
|
+
if (value === '' || value == null) continue;
|
|
98
|
+
content.setAttribute(
|
|
99
|
+
key,
|
|
100
|
+
typeof value === 'string' ? value : JSON.stringify(value),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
container.insert(0, [content]);
|
|
104
|
+
blockGroup.insert(blockGroup.length, [container]);
|
|
105
|
+
}
|
|
106
|
+
fragment.insert(0, [blockGroup]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Compile + hydrate a BaseUcanFlow plan into a standalone, natively-this-yjs Y.Doc. */
|
|
110
|
+
export function hydrateFlowDoc(plan: BaseUcanFlow): Y.Doc {
|
|
111
|
+
const compiled = compileBaseUcanFlow(plan, { getActionByCan });
|
|
112
|
+
const doc = new Y.Doc();
|
|
113
|
+
doc.transact(() => {
|
|
114
|
+
const meta = doc.getMap('qi.flow.meta');
|
|
115
|
+
for (const [key, value] of Object.entries(compiled.meta)) {
|
|
116
|
+
if (value !== undefined) meta.set(key, value);
|
|
117
|
+
}
|
|
118
|
+
writeNodeMap(doc, compiled);
|
|
119
|
+
|
|
120
|
+
const edges = doc.getMap('qi.flow.edges');
|
|
121
|
+
for (const edge of compiled.edges) {
|
|
122
|
+
const yEdge = new Y.Map<unknown>();
|
|
123
|
+
yEdge.set('id', edge.id);
|
|
124
|
+
yEdge.set('source', edge.source);
|
|
125
|
+
yEdge.set('target', edge.target);
|
|
126
|
+
yEdge.set('kind', edge.kind);
|
|
127
|
+
if (edge.condition) yEdge.set('condition', edge.condition);
|
|
128
|
+
edges.set(edge.id, yEdge);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const order = doc.getArray<string>('qi.flow.order');
|
|
132
|
+
for (const nodeId of compiled.order) order.push([nodeId]);
|
|
133
|
+
|
|
134
|
+
const blockIndex = doc.getMap('qi.flow.blockIndex');
|
|
135
|
+
for (const [nodeId, blockId] of Object.entries(compiled.blockIndex))
|
|
136
|
+
blockIndex.set(nodeId, blockId);
|
|
137
|
+
|
|
138
|
+
writeFragment(doc, compiled);
|
|
139
|
+
|
|
140
|
+
const runtime = doc.getMap('runtime');
|
|
141
|
+
for (const nodeId of compiled.order) {
|
|
142
|
+
const blockId = compiled.blockIndex[nodeId];
|
|
143
|
+
if (blockId) runtime.set(blockId, { state: 'idle' });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return doc;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Seed a step's runtime entry (keyed by block id) to simulate a portal run. */
|
|
150
|
+
export function setStepRuntime(
|
|
151
|
+
doc: Y.Doc,
|
|
152
|
+
stepId: string,
|
|
153
|
+
state: Partial<FlowNodeRuntimeState>,
|
|
154
|
+
): void {
|
|
155
|
+
const runtime = doc.getMap('runtime');
|
|
156
|
+
doc.transact(() => {
|
|
157
|
+
const blockId = stepIdToBlockId(stepId);
|
|
158
|
+
const existing = runtime.get(blockId);
|
|
159
|
+
const base =
|
|
160
|
+
existing && typeof existing === 'object'
|
|
161
|
+
? (existing as Record<string, unknown>)
|
|
162
|
+
: {};
|
|
163
|
+
runtime.set(blockId, { ...base, ...state });
|
|
164
|
+
});
|
|
165
|
+
}
|