From cbd70a7063d7a989a62bce0ee507256f72f86851 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sun, 10 May 2026 17:17:47 +0200 Subject: [PATCH 1/2] fix(local): redirect all @agent-relay/sdk subpaths and @agent-relay/config in loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sdk-runtime-loader.mjs that Ricky generates per-run only redirected `@agent-relay/sdk/workflows` to the bundled SDK. Workflow files routinely import other SDK subpaths (e.g. `@agent-relay/sdk/github` for `createGitHubStep`) and the sibling `@agent-relay/config` package (for `ClaudeModels` / `CodexModels`). Those import paths fell through to standard node resolution and failed in consumer repos that hadn't also `npm install`ed the SDK locally — defeating the point of having Ricky bundle it. Reproducible failure (cloud worktree, no local @agent-relay deps): Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@agent-relay/sdk' imported from .../workflows/proactive-runtime-m1.ts at resolve (file:///.../sdk-runtime-loader.mjs:6:10) at nextResolve (...) The bundled SDK already exposes the missing subpaths via its package exports map (`./github`, `./client`, `./communicate/...`, `./broker-path`, etc.), and `@agent-relay/config` ships alongside in the same scope dir inside Ricky's node_modules. The loader can resolve all of them against the bundled location without consumer-repo state. Behavior after this change: • `@agent-relay/sdk/workflows` → bundled (unchanged) • `@agent-relay/sdk` (root) → bundled • `@agent-relay/sdk/` → resolved against bundled SDK root, so the package's own exports map handles the subpath • `@agent-relay/config` → bundled • `@agent-relay/config/` → resolved against bundled config package root • All other specifiers → unchanged (delegate to nextResolve) All 113 src/local/entrypoint.test.ts tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/local/entrypoint.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/local/entrypoint.ts b/src/local/entrypoint.ts index 1e154b8..1fd0261 100644 --- a/src/local/entrypoint.ts +++ b/src/local/entrypoint.ts @@ -922,12 +922,49 @@ async function workflowSdkLoaderNodeOption(cwd: string): Promise/dist/workflows/index.js, so + // step up three dirs to get the SDK package root and one more for the + // parent `@agent-relay` scope dir (which also contains `config/`). + const sdkPackageRoot = dirname(dirname(dirname(runtime.entryPath))); + const scopeRoot = dirname(sdkPackageRoot); + const sdkRootUrl = pathToFileURL(sdkPackageRoot).href; + const sdkIndexUrl = pathToFileURL(join(sdkPackageRoot, 'dist', 'index.js')).href; + const configPackageRoot = join(scopeRoot, 'config'); + const configRootUrl = pathToFileURL(configPackageRoot).href; + const configIndexUrl = pathToFileURL(join(configPackageRoot, 'dist', 'index.js')).href; + const loaderSource = [ `const sdkWorkflowsUrl = ${JSON.stringify(runtimeUrl)};`, + `const sdkRootUrl = ${JSON.stringify(sdkRootUrl)};`, + `const sdkIndexUrl = ${JSON.stringify(sdkIndexUrl)};`, + `const configRootUrl = ${JSON.stringify(configRootUrl)};`, + `const configIndexUrl = ${JSON.stringify(configIndexUrl)};`, + 'const SDK_SUBPATH_PREFIX = "@agent-relay/sdk/";', + 'const CONFIG_SUBPATH_PREFIX = "@agent-relay/config/";', 'export async function resolve(specifier, context, nextResolve) {', " if (specifier === '@agent-relay/sdk/workflows') {", ' return { url: sdkWorkflowsUrl, shortCircuit: true };', ' }', + " if (specifier === '@agent-relay/sdk') {", + ' return { url: sdkIndexUrl, shortCircuit: true };', + ' }', + ' if (specifier.startsWith(SDK_SUBPATH_PREFIX)) {', + ' return nextResolve(sdkRootUrl + "/" + specifier.slice(SDK_SUBPATH_PREFIX.length), context);', + ' }', + " if (specifier === '@agent-relay/config') {", + ' return { url: configIndexUrl, shortCircuit: true };', + ' }', + ' if (specifier.startsWith(CONFIG_SUBPATH_PREFIX)) {', + ' return nextResolve(configRootUrl + "/" + specifier.slice(CONFIG_SUBPATH_PREFIX.length), context);', + ' }', ' return nextResolve(specifier, context);', '}', '', From 2180df332e0767fd928489f145aa37521d96b3c9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sun, 10 May 2026 19:54:15 +0200 Subject: [PATCH 2/2] fix(local): re-resolve subpaths via parentURL anchor instead of file:// concat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit + Devin both flagged the bug correctly: the previous subpath branch did return nextResolve(sdkRootUrl + "/" + "github", context); which passes a fully-qualified file:// URL to nextResolve. Per node's ESM resolution algorithm (Node docs `esm.html` + `packages.html`), file:// URLs are treated as already-resolved and Node SKIPS package- exports lookup. So `@agent-relay/sdk/github` would try to load a literal file at `/github` instead of going through the SDK's exports map (which maps `./github` → `./dist/github.js`). Verified against actual @agent-relay/sdk@6.0.13. The right shape: pass the original BARE specifier to nextResolve with `parentURL` anchored inside the target package root. Node then walks up looking for node_modules, finds the bundled package, and applies its exports map. Behavior after this commit (the three exact-match branches were already correct and stay): • `@agent-relay/sdk/workflows` → bundled (shortCircuit) • `@agent-relay/sdk` → bundled root index • `@agent-relay/sdk/` → re-resolved via parentURL anchor so the SDK's exports map handles the subpath • `@agent-relay/config` → bundled config index • `@agent-relay/config/` → re-resolved via config parentURL anchor Adds a regression test that imports a non-`workflows` SDK subpath (`@agent-relay/sdk/github`) AND two `@agent-relay/config` subpaths (`/relay-config`, `/agent-config`) from a workflow file and asserts the run succeeds without consumer-repo node_modules. Test fails against the previous file-URL-concat implementation; passes against this one. 114/114 src/local/entrypoint.test.ts pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/local/entrypoint.test.ts | 63 ++++++++++++++++++++++++++++++++++++ src/local/entrypoint.ts | 23 ++++++++----- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/local/entrypoint.test.ts b/src/local/entrypoint.test.ts index c6e5fcd..8486371 100644 --- a/src/local/entrypoint.test.ts +++ b/src/local/entrypoint.test.ts @@ -3055,6 +3055,69 @@ describe('runLocal', () => { } }); + it('resolves @agent-relay/sdk subpaths and @agent-relay/config via the bundled package', async () => { + // Regression test for the loader bug fixed in PR #92: previously the + // generated sdk-runtime-loader only redirected `@agent-relay/sdk/workflows`, + // so workflow files importing other SDK subpaths (e.g. `/github`) or any + // `@agent-relay/config` subpath failed in consumer repos that hadn't + // `npm install`ed the SDK locally — defeating the point of bundling. + // + // This test imports a non-`workflows` SDK subpath AND an `@agent-relay/config` + // subpath, and asserts the workflow runs successfully without the consumer + // repo having any node_modules. + const { access, mkdir, mkdtemp, rm, writeFile } = await import('node:fs/promises'); + const { tmpdir } = await import('node:os'); + const { join } = await import('node:path'); + const repo = await mkdtemp(join(tmpdir(), 'ricky-sdk-subpaths-repo-')); + const stateHome = await mkdtemp(join(tmpdir(), 'ricky-sdk-subpaths-state-')); + const artifactPath = 'workflows/generated/sdk-subpaths.workflow.ts'; + const previousStateHome = process.env.RICKY_STATE_HOME; + + try { + process.env.RICKY_STATE_HOME = stateHome; + await mkdir(join(repo, 'workflows/generated'), { recursive: true }); + await writeFile( + join(repo, artifactPath), + [ + 'import { workflow } from "@agent-relay/sdk/workflows";', + 'import * as github from "@agent-relay/sdk/github";', + 'import * as relayConfig from "@agent-relay/config/relay-config";', + 'import * as agentConfig from "@agent-relay/config/agent-config";', + 'console.log("workflow=" + typeof workflow);', + 'console.log("sdk-subpath=" + typeof github);', + 'console.log("config-subpath-relay=" + typeof relayConfig);', + 'console.log("config-subpath-agent=" + typeof agentConfig);', + '', + ].join('\n'), + 'utf8', + ); + + const result = await runLocal( + { source: 'workflow-artifact', artifactPath, stageMode: 'run' }, + { + artifactReader: mockArtifactReader('import { workflow } from "@agent-relay/sdk/workflows";'), + localExecutor: { + cwd: repo, + timeoutMs: 10_000, + }, + }, + ); + + expect(result.ok).toBe(true); + expect(result.execution?.status).toBe('success'); + // No consumer-repo node_modules needed. + await expect(access(join(repo, 'node_modules'))).rejects.toMatchObject({ code: 'ENOENT' }); + } finally { + if (previousStateHome === undefined) { + delete process.env.RICKY_STATE_HOME; + } else { + process.env.RICKY_STATE_HOME = previousStateHome; + } + await rm(repo, { recursive: true, force: true }); + await rm(stateHome, { recursive: true, force: true }); + } + }); + it('kills the SDK workflow process tree when the local timeout fires', async () => { const { mkdir, mkdtemp, readFile, rm, writeFile } = await import('node:fs/promises'); const { tmpdir } = await import('node:os'); diff --git a/src/local/entrypoint.ts b/src/local/entrypoint.ts index 1fd0261..ee2c22c 100644 --- a/src/local/entrypoint.ts +++ b/src/local/entrypoint.ts @@ -935,18 +935,25 @@ async function workflowSdkLoaderNodeOption(cwd: string): Promise