-
Notifications
You must be signed in to change notification settings - Fork 0
feat(sdk): YAML helper verbs compile to effect steps (#345) #349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
bdc75b2
feat(sdk): YAML helper verbs compile to effect steps (#345)
miyaontherelay 7cd0b05
test(sdk): preflight walker reaches helper_mount_required
e6d8ddf
test(sdk): yaml-helper-live honors RELAYFLOWD_BIN before cargo metadata
1e507e3
fix(sdk): narrow yaml slack.post text to string post-Y Block Kit typing
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # YAML helper verbs | ||
|
|
||
| Helpers compile into ordinary agent steps that execute journal-backed effects | ||
| through `@relayfile/relay-helpers`. Slack uses the same writeback path as | ||
| `f.slack`. No provider step type is added to the kernel. | ||
|
|
||
| ```yaml | ||
| version: 0.1.0 | ||
| steps: | ||
| - id: notify | ||
| slack: | ||
| post: | ||
| channel: "#test" | ||
| text: hi | ||
| - id: ticket | ||
| dependsOn: [notify] | ||
| linear: | ||
| createIssue: | ||
| teamId: engineering | ||
| title: Follow up | ||
| ``` | ||
|
|
||
| Each step has an `id` and exactly one provider containing exactly one verb. | ||
| Optional step fields are `dependsOn`, `maxIterations`, `verification`, and | ||
| `output`. Arguments are literal JSON-compatible data; templates and dynamic | ||
| argument bindings are not supported. Unknown fields, verbs, and malformed | ||
| arguments fail compilation. `YamlFlowSpec` and `YamlHelperStepSpec` describe | ||
| the authoring shapes; `compileSpec` returns the normalized `FlowSpec`. | ||
|
|
||
| Supported verbs: | ||
|
|
||
| | Provider | Verbs | | ||
| | --- | --- | | ||
| | Slack | `post`, `dm`, `reply`, `react` | | ||
| | GitHub | `comment`, `createIssue`, `createPullRequest`, `closePullRequest` | | ||
| | Linear | `comment`, `createIssue`, `updateIssue` | | ||
|
|
||
| Single-object client arguments appear directly under the verb. Slack's | ||
| positional arguments become named fields (`channel`, `text`, `opts`, etc.). | ||
| GitHub `comment` takes `{target: {owner, repo, number}, body}`; Linear `comment` | ||
| takes `{issueId, body}`, and `updateIssue` takes `{issueId, args}`. | ||
|
|
||
| Configure a relayfile mount containing the provider directory using the same | ||
| mount environment variables as TS Slack helpers (`RELAYFILE_MOUNT_PATH`, | ||
| `WORKSPACE_ROOT`, `WORKFORCE_SANDBOX_ROOT`, `RELAYFILE_MOUNT_ROOT`, or | ||
| `RELAYFILE_ROOT`). `flows check` refuses missing mounts. To run locally: | ||
|
|
||
| ```sh | ||
| flows run notify.yaml --local-agent | ||
| ``` | ||
|
|
||
| An attached SDK `AgentWorker` can also execute these steps; it must have a | ||
| `dataDir` for durable receipts. Completion output contains the effect call, | ||
| its stable idempotency key, and `receipt`. As with TS Slack, receipts are saved | ||
| before effect confirmation, allowing unfinished attempts to recover without | ||
| repeating a confirmed provider write. Keep the worker's data directory across | ||
| restarts. |
Large diffs are not rendered by default.
Oops, something went wrong.
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
|
|
||
| > @relayflows/sdk@2.0.8 typecheck | ||
| > tsc --noEmit && tsc -p tsconfig.type-tests.json | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { createHash } from 'node:crypto'; | ||
| import { statSync } from 'node:fs'; | ||
| import { join } from 'node:path'; | ||
| import { writeJsonFile } from '@relayfile/adapter-core/vfs-client'; | ||
| import type { RelayTransport } from '@relayfile/relay-helpers'; | ||
| import type { JournalClient } from './journal-client.js'; | ||
| import type { StepDispatchEvent } from './protocol.js'; | ||
| import { atomicJson, readSlackReceipt, receiptPath, slackWriteback } from './slack-writeback.js'; | ||
| import { invokeHelper, type HelperCall } from './yaml-helpers.js'; | ||
| import { withWorkerLease } from './worker-lease.js'; | ||
|
|
||
| export function helperMount(provider: string, env = process.env): string | undefined { | ||
| const root = [env.RELAYFILE_MOUNT_PATH, env.WORKSPACE_ROOT, env.WORKFORCE_SANDBOX_ROOT, | ||
| env.RELAYFILE_MOUNT_ROOT, env.RELAYFILE_ROOT].find(value => value?.trim()); | ||
| if (!root) return undefined; | ||
| try { return statSync(join(root, provider)).isDirectory() ? root : undefined; } | ||
| catch { return undefined; } | ||
| } | ||
|
|
||
| export function helperReady(provider: string): boolean { | ||
| return (provider === 'slack' && process.env.RELAYFLOWS_SLACK_MOCK === '1') || helperMount(provider) !== undefined; | ||
| } | ||
|
|
||
| async function writeback(call: HelperCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal) { | ||
| // Exactly the TS surface's client, transport, idempotency stamp and receipt checks. | ||
| if (call.provider === 'slack') return slackWriteback(call, dataDir, runId, stepId, signal); | ||
| const mount = helperMount(call.provider); | ||
| if (mount === undefined) throw new Error(`${call.provider} helper requires a relayfile mount`); | ||
| const idempotencyKey = `${runId}:${stepId}`; | ||
| const transport: RelayTransport = { | ||
| async read() { throw new Error('Helper effect transport is write-only'); }, | ||
| async list() { throw new Error('Helper effect transport is write-only'); }, | ||
| async write(request) { | ||
| signal.throwIfAborted(); | ||
| const body = { ...request.body as Record<string, unknown>, idempotencyKey }; | ||
| // Item updates keep the client's canonical path. Creates use a stable draft. | ||
| const path = request.path.endsWith('.json') ? request.path | ||
| : `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`; | ||
| const result = await writeJsonFile({ relayfileMountRoot: mount }, request.provider, | ||
| `write.${request.resource}`, path, body); | ||
| if (result.deliveryStatus !== 'confirmed' || !result.receipt) { | ||
| throw new Error(`${call.provider} writeback is pending; no delivery receipt`); | ||
| } | ||
| signal.throwIfAborted(); | ||
| return result; | ||
| }, | ||
| }; | ||
| return invokeHelper(call, transport); | ||
| } | ||
|
|
||
| /** Existing lease/effect election protocol; provider verbs never enter the kernel. */ | ||
| export async function completeHelperDispatch( | ||
| client: JournalClient, dispatch: StepDispatchEvent, call: HelperCall, dataDir: string, | ||
| ): Promise<void> { | ||
| const surfacePath = `/${call.provider}`; | ||
| const output = await withWorkerLease(client, dispatch, async signal => { | ||
| const file = receiptPath(dataDir, dispatch.run_id, dispatch.step_id); | ||
| let receipt: unknown; | ||
| await client.performEffect({ | ||
| runId: dispatch.run_id, stepId: dispatch.step_id, attempt: dispatch.attempt, | ||
| idempotencyKey: dispatch.idempotency_key, surfacePath, | ||
| revisionBefore: 'pending', revisionAfter: `${dispatch.run_id}:${dispatch.step_id}`, | ||
| }, async () => { | ||
| try { receipt = await readSlackReceipt(file); } | ||
| catch (error) { | ||
| if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; | ||
| receipt = await writeback(call, dataDir, dispatch.run_id, dispatch.step_id, signal); | ||
| await atomicJson(file, receipt); | ||
| } | ||
| signal.throwIfAborted(); | ||
| }); | ||
| if (receipt === undefined) receipt = await readSlackReceipt(file); | ||
| return { ...call, idempotencyKey: `${dispatch.run_id}:${dispatch.step_id}`, receipt }; | ||
| }); | ||
| await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt, | ||
| dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins, | ||
| effects: [{ surface_path: surfacePath, idempotency_key: dispatch.idempotency_key }] }); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.