From e122918ea6c6b7a7fc64438fe24ef10aba277545 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Thu, 26 Feb 2026 05:52:10 +0000 Subject: [PATCH 1/2] fix: store all scope experts on resolve to support delegate lookup When fetching a published expert from the API, store all experts from the scope definition (coordinator + delegates) instead of only the requested one. This allows delegates to be resolved from the local cache without separate API calls that would fail with 404. Co-Authored-By: Claude Opus 4.6 --- packages/installer/src/expert-resolver.ts | 17 +++++++---- .../src/helpers/resolve-expert.test.ts | 29 +++++++++++++++++-- .../runtime/src/helpers/resolve-expert.ts | 12 ++++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/packages/installer/src/expert-resolver.ts b/packages/installer/src/expert-resolver.ts index 9815eba0..fd36ff56 100644 --- a/packages/installer/src/expert-resolver.ts +++ b/packages/installer/src/expert-resolver.ts @@ -173,14 +173,19 @@ export async function resolveAllExperts( `Failed to resolve delegate "${delegateKey}": ${result.error.message}`, ) } - const publishedExpert = result.data.data.definition.experts[delegateKey] - if (!publishedExpert) { + const allExperts = result.data.data.definition.experts + if (!allExperts[delegateKey]) { throw new PerstackError(`Expert "${delegateKey}" not found in API response`) } - experts[delegateKey] = toRuntimeExpert(delegateKey, publishedExpert) - for (const nestedDelegate of publishedExpert.delegates ?? []) { - if (!experts[nestedDelegate]) { - toResolve.add(nestedDelegate) + // Store all experts from the definition (coordinator + delegates) + for (const [key, expert] of Object.entries(allExperts)) { + if (!experts[key]) { + experts[key] = toRuntimeExpert(key, expert) + for (const nestedDelegate of expert.delegates ?? []) { + if (!experts[nestedDelegate]) { + toResolve.add(nestedDelegate) + } + } } } } diff --git a/packages/runtime/src/helpers/resolve-expert.test.ts b/packages/runtime/src/helpers/resolve-expert.test.ts index d97875f6..a30f3405 100644 --- a/packages/runtime/src/helpers/resolve-expert.test.ts +++ b/packages/runtime/src/helpers/resolve-expert.test.ts @@ -35,6 +35,15 @@ mock.module("@perstack/api-client", () => ({ omit: [], }, }, + delegates: [`@${expertKey}/delegate`], + tags: [], + }, + [`@${expertKey}/delegate`]: { + name: "Delegate Expert", + version: "1.0.0", + description: "A delegate expert", + instruction: "Delegate instruction", + skills: {}, delegates: [], tags: [], }, @@ -107,13 +116,16 @@ describe("@perstack/runtime: resolveExpertToRun", () => { expect(result.skills["@perstack/base"].name).toBe("@perstack/base") }) - it("does not mutate experts record", async () => { + it("populates experts record with all experts from definition", async () => { const experts: Record = {} await resolveExpertToRun("remote-expert", experts, { perstackApiBaseUrl: "https://api.test.com", perstackApiKey: "test-key", }) - expect(experts["remote-expert"]).toBeUndefined() + expect(experts["remote-expert"]).toBeDefined() + expect(experts["remote-expert"].name).toBe("Remote Expert") + expect(experts["@remote-expert/delegate"]).toBeDefined() + expect(experts["@remote-expert/delegate"].name).toBe("Delegate Expert") }) it("fetches public expert from API without API key", async () => { @@ -123,4 +135,17 @@ describe("@perstack/runtime: resolveExpertToRun", () => { }) expect(result.key).toBe("published-expert") }) + + it("resolves delegate from already-populated experts without API call", async () => { + const experts: Record = {} + await resolveExpertToRun("remote-expert", experts, { + perstackApiBaseUrl: "https://api.test.com", + }) + // Delegate should already be in the experts record + const delegate = await resolveExpertToRun("@remote-expert/delegate", experts, { + perstackApiBaseUrl: "https://api.test.com", + }) + expect(delegate.key).toBe("@remote-expert/delegate") + expect(delegate.name).toBe("Delegate Expert") + }) }) diff --git a/packages/runtime/src/helpers/resolve-expert.ts b/packages/runtime/src/helpers/resolve-expert.ts index dcd6e329..e36d11d3 100644 --- a/packages/runtime/src/helpers/resolve-expert.ts +++ b/packages/runtime/src/helpers/resolve-expert.ts @@ -20,11 +20,17 @@ export async function resolveExpertToRun( if (!result.ok) { throw new PerstackError(`Failed to resolve expert "${expertKey}": ${result.error.message}`) } - const publishedExpert = result.data.data.definition.experts[expertKey] - if (!publishedExpert) { + const allExperts = result.data.data.definition.experts + if (!allExperts[expertKey]) { throw new PerstackError(`Expert "${expertKey}" not found in API response`) } - return toRuntimeExpert(expertKey, publishedExpert) + // Store all experts from the definition (coordinator + delegates) + for (const [key, expert] of Object.entries(allExperts)) { + if (!experts[key]) { + experts[key] = toRuntimeExpert(key, expert) + } + } + return experts[expertKey] } function toRuntimeExpert( From cff93bda313a69095701c199844e6dcbbade33a3 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Thu, 26 Feb 2026 13:32:47 +0000 Subject: [PATCH 2/2] chore: add changeset for scope delegate resolution fix Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-scope-delegate-resolution.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fix-scope-delegate-resolution.md diff --git a/.changeset/fix-scope-delegate-resolution.md b/.changeset/fix-scope-delegate-resolution.md new file mode 100644 index 00000000..6e993ec3 --- /dev/null +++ b/.changeset/fix-scope-delegate-resolution.md @@ -0,0 +1,7 @@ +--- +"@perstack/runtime": patch +"@perstack/installer": patch +"perstack": patch +--- + +Store all experts from a published scope definition (coordinator + delegates) so delegates resolve from cache without separate API calls.