From fbea8b8640fd413d134ad386dd8ffbc28d088bd0 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 21 Jan 2026 16:46:42 +0000 Subject: [PATCH 1/4] feat: add runtimeVersion to Job schema with validation - Add `runtimeVersion: RuntimeVersion` to Job schema - Add `minRuntimeVersion?: RuntimeVersion` to Expert schema - Runtime 0.x.y is treated as v1.0 for compatibility - Validate entire delegation chain before execution (fail fast) - Recursive delegate resolution for complete chain validation Closes #440 Co-Authored-By: Claude Opus 4.5 --- .changeset/runtime-version.md | 37 ++++ .../perstack/src/lib/log/data-fetcher.test.ts | 1 + apps/perstack/src/lib/log/data-fetcher.ts | 1 + apps/perstack/src/lib/log/formatter.test.ts | 1 + apps/runtime/src/cli/context.ts | 1 + apps/runtime/src/helpers/index.ts | 9 + apps/runtime/src/helpers/resolve-expert.ts | 4 +- .../src/helpers/runtime-version.test.ts | 198 ++++++++++++++++++ apps/runtime/src/helpers/runtime-version.ts | 56 +++++ .../runtime/src/helpers/setup-experts.test.ts | 77 +++++++ apps/runtime/src/helpers/setup-experts.ts | 23 +- .../orchestration/single-run-executor.test.ts | 1 + .../src/orchestration/single-run-executor.ts | 2 + apps/runtime/src/run.test.ts | 3 + apps/runtime/src/run.ts | 15 +- e2e/experts/runtime-version.toml | 99 +++++++++ e2e/perstack-runtime/runtime-version.test.ts | 95 +++++++++ packages/core/src/index.ts | 1 + packages/core/src/schemas/expert.ts | 5 + packages/core/src/schemas/job.ts | 4 + packages/core/src/schemas/perstack-toml.ts | 6 +- packages/core/src/schemas/runtime-version.ts | 5 + .../filesystem/src/filesystem-storage.test.ts | 1 + packages/storages/filesystem/src/job.ts | 1 + .../s3-compatible/src/s3-storage-base.test.ts | 1 + .../s3-compatible/src/serialization.test.ts | 1 + 26 files changed, 635 insertions(+), 13 deletions(-) create mode 100644 .changeset/runtime-version.md create mode 100644 apps/runtime/src/helpers/runtime-version.test.ts create mode 100644 apps/runtime/src/helpers/runtime-version.ts create mode 100644 e2e/experts/runtime-version.toml create mode 100644 e2e/perstack-runtime/runtime-version.test.ts create mode 100644 packages/core/src/schemas/runtime-version.ts diff --git a/.changeset/runtime-version.md b/.changeset/runtime-version.md new file mode 100644 index 00000000..8468cb83 --- /dev/null +++ b/.changeset/runtime-version.md @@ -0,0 +1,37 @@ +--- +"@perstack/core": minor +"@perstack/runtime": minor +"@perstack/base": minor +"@perstack/filesystem-storage": minor +"@perstack/s3-compatible-storage": minor +"@perstack/s3-storage": minor +"@perstack/r2-storage": minor +"@perstack/runner": minor +"@perstack/mock": minor +"@perstack/react": minor +"@perstack/anthropic-provider": minor +"@perstack/azure-openai-provider": minor +"@perstack/bedrock-provider": minor +"@perstack/deepseek-provider": minor +"@perstack/google-provider": minor +"@perstack/ollama-provider": minor +"@perstack/openai-provider": minor +"@perstack/vertex-provider": minor +"@perstack/provider-core": minor +"@perstack/adapter-base": minor +"@perstack/claude-code": minor +"@perstack/cursor": minor +"@perstack/docker": minor +"@perstack/gemini": minor +"@perstack/tui-components": minor +"create-expert": minor +"perstack": minor +--- + +Add runtime version tracking to Job schema and validation + +- Add `runtimeVersion` field to Job schema to track which runtime version executed the job +- Add `minRuntimeVersion` field to Expert schema for compatibility requirements +- Runtime version 0.x.y is treated as v1.0 for compatibility +- Validate entire delegation chain before execution (fail fast, no LLM calls if incompatible) +- Recursive delegate resolution ensures all experts in chain are checked diff --git a/apps/perstack/src/lib/log/data-fetcher.test.ts b/apps/perstack/src/lib/log/data-fetcher.test.ts index 798cdb09..a04446b4 100644 --- a/apps/perstack/src/lib/log/data-fetcher.test.ts +++ b/apps/perstack/src/lib/log/data-fetcher.test.ts @@ -6,6 +6,7 @@ const mockJob: Job = { id: "job-1", status: "completed", coordinatorExpertKey: "test-expert@1.0.0", + runtimeVersion: "v1.0", totalSteps: 5, usage: { inputTokens: 1000, diff --git a/apps/perstack/src/lib/log/data-fetcher.ts b/apps/perstack/src/lib/log/data-fetcher.ts index 6befc2d6..ce2fbe34 100644 --- a/apps/perstack/src/lib/log/data-fetcher.ts +++ b/apps/perstack/src/lib/log/data-fetcher.ts @@ -37,6 +37,7 @@ export function createLogDataFetcher(storage: StorageAdapter): LogDataFetcher { return { id: jobId, coordinatorExpertKey: firstCheckpoint.expert.key, + runtimeVersion: "v1.0", totalSteps: lastCheckpoint.stepNumber, usage: lastCheckpoint.usage, startedAt: getJobDirMtime(storage.getBasePath(), jobId), diff --git a/apps/perstack/src/lib/log/formatter.test.ts b/apps/perstack/src/lib/log/formatter.test.ts index c13c92fb..b19741fa 100644 --- a/apps/perstack/src/lib/log/formatter.test.ts +++ b/apps/perstack/src/lib/log/formatter.test.ts @@ -7,6 +7,7 @@ const mockJob: Job = { id: "job-1", status: "completed", coordinatorExpertKey: "test-expert@1.0.0", + runtimeVersion: "v1.0", totalSteps: 5, usage: { inputTokens: 1000, diff --git a/apps/runtime/src/cli/context.ts b/apps/runtime/src/cli/context.ts index 94d8ef87..db9b348a 100644 --- a/apps/runtime/src/cli/context.ts +++ b/apps/runtime/src/cli/context.ts @@ -49,6 +49,7 @@ export async function resolveRunContext(input: ResolveRunContextInput): Promise< skills: expert.skills, delegates: expert.delegates ?? [], tags: expert.tags ?? [], + minRuntimeVersion: expert.minRuntimeVersion, }, ] }), diff --git a/apps/runtime/src/helpers/index.ts b/apps/runtime/src/helpers/index.ts index d984c9d6..aac04612 100644 --- a/apps/runtime/src/helpers/index.ts +++ b/apps/runtime/src/helpers/index.ts @@ -12,6 +12,15 @@ export { loadLockfile, } from "./lockfile.js" export { calculateContextWindowUsage, getContextWindow, getModel } from "./model.js" +export { + compareRuntimeVersions, + determineJobRuntimeVersion, + getCurrentRuntimeVersion, + getMaxMinRuntimeVersion, + parseRuntimeVersion, + toRuntimeVersion, + validateRuntimeVersion, +} from "./runtime-version.js" export { type ResolveExpertToRunFn, type SetupExpertsResult, diff --git a/apps/runtime/src/helpers/resolve-expert.ts b/apps/runtime/src/helpers/resolve-expert.ts index 9ad7faee..a68d4740 100644 --- a/apps/runtime/src/helpers/resolve-expert.ts +++ b/apps/runtime/src/helpers/resolve-expert.ts @@ -1,5 +1,5 @@ import { createApiClient } from "@perstack/api-client" -import type { Expert, Skill } from "@perstack/core" +import type { Expert, RuntimeVersion, Skill } from "@perstack/core" export async function resolveExpertToRun( expertKey: string, @@ -35,6 +35,7 @@ function toRuntimeExpert( expert: { name: string version: string + minRuntimeVersion?: RuntimeVersion description?: string instruction: string skills?: Record< @@ -134,6 +135,7 @@ function toRuntimeExpert( key, name: expert.name, version: expert.version, + minRuntimeVersion: expert.minRuntimeVersion, description: expert.description ?? "", instruction: expert.instruction, skills, diff --git a/apps/runtime/src/helpers/runtime-version.test.ts b/apps/runtime/src/helpers/runtime-version.test.ts new file mode 100644 index 00000000..b1c5f24f --- /dev/null +++ b/apps/runtime/src/helpers/runtime-version.test.ts @@ -0,0 +1,198 @@ +import type { Expert, RuntimeVersion } from "@perstack/core" +import { describe, expect, it, vi } from "vitest" + +vi.mock("../../package.json", () => ({ + default: { version: "0.0.87" }, +})) + +import { + compareRuntimeVersions, + determineJobRuntimeVersion, + getCurrentRuntimeVersion, + getMaxMinRuntimeVersion, + parseRuntimeVersion, + toRuntimeVersion, + validateRuntimeVersion, +} from "./runtime-version.js" + +function createMockExpert( + overrides: Partial> & { + minRuntimeVersion?: RuntimeVersion | string + } = {}, +): Expert { + return { + key: "test-expert", + name: "Test Expert", + version: "1.0.0", + instruction: "Test instruction", + skills: {}, + delegates: [], + tags: [], + ...overrides, + } as Expert +} + +describe("@perstack/runtime: runtime-version", () => { + describe("parseRuntimeVersion()", () => { + it("parses v1.0 format", () => { + const result = parseRuntimeVersion("v1.0") + expect(result).toEqual({ major: 1, minor: 0 }) + }) + + it("parses 1.0 format without v prefix", () => { + const result = parseRuntimeVersion("1.0") + expect(result).toEqual({ major: 1, minor: 0 }) + }) + + it("parses v1.10 with two-digit minor", () => { + const result = parseRuntimeVersion("v1.10") + expect(result).toEqual({ major: 1, minor: 10 }) + }) + + it("returns null for undefined", () => { + const result = parseRuntimeVersion(undefined) + expect(result).toBeNull() + }) + + it("returns null for semver format 1.0.0", () => { + const result = parseRuntimeVersion("1.0.0") + expect(result).toBeNull() + }) + + it("returns null for invalid format", () => { + const result = parseRuntimeVersion("invalid") + expect(result).toBeNull() + }) + }) + + describe("compareRuntimeVersions()", () => { + it("returns positive when a > b (major)", () => { + expect(compareRuntimeVersions("v2.0", "v1.0")).toBeGreaterThan(0) + }) + + it("returns negative when a < b (major)", () => { + expect(compareRuntimeVersions("v1.0", "v2.0")).toBeLessThan(0) + }) + + it("returns positive when a > b (minor)", () => { + expect(compareRuntimeVersions("v1.5", "v1.2")).toBeGreaterThan(0) + }) + + it("returns zero when a equals b", () => { + expect(compareRuntimeVersions("v1.0", "v1.0")).toBe(0) + }) + + it("returns zero when either version is invalid", () => { + expect(compareRuntimeVersions("invalid", "v1.0")).toBe(0) + expect(compareRuntimeVersions("v1.0", "invalid")).toBe(0) + }) + }) + + describe("toRuntimeVersion()", () => { + it("converts semver to runtime version", () => { + expect(toRuntimeVersion("1.2.3")).toBe("v1.2") + }) + + it("converts 0.x.y to v1.0 (special case)", () => { + expect(toRuntimeVersion("0.0.87")).toBe("v1.0") + }) + + it("converts 0.5.10 to v1.0", () => { + expect(toRuntimeVersion("0.5.10")).toBe("v1.0") + }) + }) + + describe("getCurrentRuntimeVersion()", () => { + it("returns v1.0 for mocked 0.0.87 version", () => { + expect(getCurrentRuntimeVersion()).toBe("v1.0") + }) + }) + + describe("getMaxMinRuntimeVersion()", () => { + it("returns undefined for empty experts", () => { + const result = getMaxMinRuntimeVersion({}) + expect(result).toBeUndefined() + }) + + it("returns undefined when no experts have minRuntimeVersion", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1" }), + "expert-2": createMockExpert({ key: "expert-2" }), + } + const result = getMaxMinRuntimeVersion(experts) + expect(result).toBeUndefined() + }) + + it("returns the single minRuntimeVersion", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1", minRuntimeVersion: "v1.0" }), + } + const result = getMaxMinRuntimeVersion(experts) + expect(result).toBe("v1.0") + }) + + it("returns max minRuntimeVersion among multiple experts", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1", minRuntimeVersion: "v1.0" }), + "expert-2": createMockExpert({ key: "expert-2", minRuntimeVersion: "v1.5" }), + "expert-3": createMockExpert({ key: "expert-3", minRuntimeVersion: "v1.2" }), + } + const result = getMaxMinRuntimeVersion(experts) + expect(result).toBe("v1.5") + }) + + it("handles mixed undefined and defined minRuntimeVersion", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1" }), + "expert-2": createMockExpert({ key: "expert-2", minRuntimeVersion: "v1.0" }), + } + const result = getMaxMinRuntimeVersion(experts) + expect(result).toBe("v1.0") + }) + }) + + describe("validateRuntimeVersion()", () => { + it("does not throw when no minRuntimeVersion is set", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1" }), + } + expect(() => validateRuntimeVersion(experts)).not.toThrow() + }) + + it("does not throw when minRuntimeVersion <= current (v1.0)", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1", minRuntimeVersion: "v1.0" }), + } + expect(() => validateRuntimeVersion(experts)).not.toThrow() + }) + + it("throws when minRuntimeVersion > current", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1", minRuntimeVersion: "v99.0" }), + } + expect(() => validateRuntimeVersion(experts)).toThrow("v99.0") + }) + }) + + describe("determineJobRuntimeVersion()", () => { + it("returns v1.0 for 0.x.y runtime version", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1", minRuntimeVersion: "v1.0" }), + } + const result = determineJobRuntimeVersion(experts) + expect(result).toBe("v1.0") + }) + + it("throws on validation failure", () => { + const experts: Record = { + "expert-1": createMockExpert({ key: "expert-1", minRuntimeVersion: "v99.0" }), + } + expect(() => determineJobRuntimeVersion(experts)).toThrow("v99.0") + }) + + it("returns current version when no experts provided", () => { + const result = determineJobRuntimeVersion({}) + expect(result).toBe("v1.0") + }) + }) +}) diff --git a/apps/runtime/src/helpers/runtime-version.ts b/apps/runtime/src/helpers/runtime-version.ts new file mode 100644 index 00000000..a33ef30d --- /dev/null +++ b/apps/runtime/src/helpers/runtime-version.ts @@ -0,0 +1,56 @@ +import type { Expert, RuntimeVersion } from "@perstack/core" +import pkg from "../../package.json" with { type: "json" } + +export function parseRuntimeVersion( + version: string | undefined, +): { major: number; minor: number } | null { + if (!version) return null + const match = version.match(/^v?(\d+)\.(\d+)$/) + if (!match) return null + return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) } +} + +export function compareRuntimeVersions(a: string, b: string): number { + const parsedA = parseRuntimeVersion(a) + const parsedB = parseRuntimeVersion(b) + if (!parsedA || !parsedB) return 0 + if (parsedA.major !== parsedB.major) return parsedA.major - parsedB.major + return parsedA.minor - parsedB.minor +} + +export function toRuntimeVersion(semver: string): RuntimeVersion { + const parts = semver.split(".") + const major = parseInt(parts[0], 10) + if (major === 0) return "v1.0" + return `v${parts[0]}.${parts[1]}` as RuntimeVersion +} + +export function getCurrentRuntimeVersion(): RuntimeVersion { + return toRuntimeVersion(pkg.version) +} + +export function getMaxMinRuntimeVersion( + experts: Record, +): RuntimeVersion | undefined { + const versions = Object.values(experts) + .map((e) => e.minRuntimeVersion) + .filter((v): v is RuntimeVersion => v !== undefined && parseRuntimeVersion(v) !== null) + if (versions.length === 0) return undefined + return versions.reduce((max, v) => (compareRuntimeVersions(v, max) > 0 ? v : max), versions[0]) +} + +export function validateRuntimeVersion(experts: Record): void { + const currentVersion = getCurrentRuntimeVersion() + const maxMinVersion = getMaxMinRuntimeVersion(experts) + if (!maxMinVersion) return + if (compareRuntimeVersions(maxMinVersion, currentVersion) > 0) { + throw new Error( + `Runtime version ${currentVersion} does not meet minimum requirement ${maxMinVersion}`, + ) + } +} + +export function determineJobRuntimeVersion(experts: Record): RuntimeVersion { + validateRuntimeVersion(experts) + return getCurrentRuntimeVersion() +} diff --git a/apps/runtime/src/helpers/setup-experts.test.ts b/apps/runtime/src/helpers/setup-experts.test.ts index 30985478..3811cbc7 100644 --- a/apps/runtime/src/helpers/setup-experts.test.ts +++ b/apps/runtime/src/helpers/setup-experts.test.ts @@ -99,4 +99,81 @@ describe("@perstack/runtime: setupExperts", () => { perstackApiKey: "my-api-key", }) }) + + it("resolves 3-level delegation chain", async () => { + const rootExpert: Expert = { ...baseExpert, key: "root", name: "root", delegates: ["level1"] } + const level1Expert: Expert = { + ...baseExpert, + key: "level1", + name: "level1", + delegates: ["level2"], + } + const level2Expert: Expert = { ...baseExpert, key: "level2", name: "level2", delegates: [] } + const mockResolve = vi.fn().mockImplementation((key: string) => { + if (key === "root") return Promise.resolve(rootExpert) + if (key === "level1") return Promise.resolve(level1Expert) + if (key === "level2") return Promise.resolve(level2Expert) + return Promise.resolve(null) + }) + const setting = { ...baseSetting, expertKey: "root" } + const result = await setupExperts(setting, mockResolve) + expect(mockResolve).toHaveBeenCalledTimes(3) + expect(Object.keys(result.experts)).toHaveLength(3) + expect(result.experts["root"]).toEqual(rootExpert) + expect(result.experts["level1"]).toEqual(level1Expert) + expect(result.experts["level2"]).toEqual(level2Expert) + }) + + it("handles circular reference without infinite loop", async () => { + const rootExpert: Expert = { ...baseExpert, key: "root", name: "root", delegates: ["level1"] } + const level1Expert: Expert = { + ...baseExpert, + key: "level1", + name: "level1", + delegates: ["root"], + } + const mockResolve = vi.fn().mockImplementation((key: string) => { + if (key === "root") return Promise.resolve(rootExpert) + if (key === "level1") return Promise.resolve(level1Expert) + return Promise.resolve(null) + }) + const setting = { ...baseSetting, expertKey: "root" } + const result = await setupExperts(setting, mockResolve) + expect(mockResolve).toHaveBeenCalledTimes(2) + expect(Object.keys(result.experts)).toHaveLength(2) + }) + + it("handles diamond pattern - resolves shared delegate once", async () => { + const rootExpert: Expert = { + ...baseExpert, + key: "root", + name: "root", + delegates: ["branch-a", "branch-b"], + } + const branchAExpert: Expert = { + ...baseExpert, + key: "branch-a", + name: "branch-a", + delegates: ["shared"], + } + const branchBExpert: Expert = { + ...baseExpert, + key: "branch-b", + name: "branch-b", + delegates: ["shared"], + } + const sharedExpert: Expert = { ...baseExpert, key: "shared", name: "shared", delegates: [] } + const mockResolve = vi.fn().mockImplementation((key: string) => { + if (key === "root") return Promise.resolve(rootExpert) + if (key === "branch-a") return Promise.resolve(branchAExpert) + if (key === "branch-b") return Promise.resolve(branchBExpert) + if (key === "shared") return Promise.resolve(sharedExpert) + return Promise.resolve(null) + }) + const setting = { ...baseSetting, expertKey: "root" } + const result = await setupExperts(setting, mockResolve) + expect(mockResolve).toHaveBeenCalledTimes(4) + expect(Object.keys(result.experts)).toHaveLength(4) + expect(result.experts["shared"]).toEqual(sharedExpert) + }) }) diff --git a/apps/runtime/src/helpers/setup-experts.ts b/apps/runtime/src/helpers/setup-experts.ts index 77d12102..6ae29032 100644 --- a/apps/runtime/src/helpers/setup-experts.ts +++ b/apps/runtime/src/helpers/setup-experts.ts @@ -15,7 +15,6 @@ export async function setupExperts( setting: RunSetting, resolveExpertToRun?: ResolveExpertToRunFn, ): Promise { - // Lazy-load resolve-expert to avoid importing @perstack/api-client at module load time const resolveFn = resolveExpertToRun ?? (await import("./resolve-expert.js")).resolveExpertToRun const { expertKey } = setting const experts = { ...setting.experts } @@ -23,14 +22,22 @@ export async function setupExperts( perstackApiBaseUrl: setting.perstackApiBaseUrl, perstackApiKey: setting.perstackApiKey, } - const expertToRun = await resolveFn(expertKey, experts, clientOptions) - experts[expertKey] = expertToRun - for (const delegateName of expertToRun.delegates) { - const delegate = await resolveFn(delegateName, experts, clientOptions) - if (!delegate) { - throw new Error(`Delegate ${delegateName} not found`) + + async function resolveExpertWithDelegates(key: string, visited: Set): Promise { + if (visited.has(key)) return + visited.add(key) + const expert = await resolveFn(key, experts, clientOptions) + if (!expert) { + throw new Error(`Delegate ${key} not found`) + } + experts[key] = expert + for (const delegateKey of expert.delegates) { + await resolveExpertWithDelegates(delegateKey, visited) } - experts[delegateName] = delegate } + + const visited = new Set() + await resolveExpertWithDelegates(expertKey, visited) + const expertToRun = experts[expertKey] return { expertToRun, experts } } diff --git a/apps/runtime/src/orchestration/single-run-executor.test.ts b/apps/runtime/src/orchestration/single-run-executor.test.ts index e69e592d..291ddda2 100644 --- a/apps/runtime/src/orchestration/single-run-executor.test.ts +++ b/apps/runtime/src/orchestration/single-run-executor.test.ts @@ -21,6 +21,7 @@ vi.mock("../helpers/index.js", () => ({ }, }, }), + validateRuntimeVersion: vi.fn(), createInitialCheckpoint: vi.fn().mockImplementation((id, params) => ({ id, jobId: params.jobId, diff --git a/apps/runtime/src/orchestration/single-run-executor.ts b/apps/runtime/src/orchestration/single-run-executor.ts index b25ed93c..7f6e51b5 100644 --- a/apps/runtime/src/orchestration/single-run-executor.ts +++ b/apps/runtime/src/orchestration/single-run-executor.ts @@ -18,6 +18,7 @@ import { getLockfileExpertToolDefinitions, type ResolveExpertToRunFn, setupExperts, + validateRuntimeVersion, } from "../helpers/index.js" import { createProviderAdapter } from "../helpers/provider-adapter-factory.js" import "../helpers/register-providers.js" @@ -59,6 +60,7 @@ export class SingleRunExecutor { const contextWindow = getContextWindow(setting.providerConfig.providerName, setting.model) const { expertToRun, experts } = await setupExperts(setting, this.options.resolveExpertToRun) + validateRuntimeVersion(experts) this.emitInitEvent(setting, expertToRun, experts) diff --git a/apps/runtime/src/run.test.ts b/apps/runtime/src/run.test.ts index 642d2e17..a2ba4f8a 100644 --- a/apps/runtime/src/run.test.ts +++ b/apps/runtime/src/run.test.ts @@ -109,6 +109,7 @@ describe("@perstack/runtime: run", () => { const existingJob: Job = { id: "test-job-id", coordinatorExpertKey: "test-expert", + runtimeVersion: "v1.0", status: "running", totalSteps: 5, startedAt: Date.now(), @@ -135,6 +136,7 @@ describe("@perstack/runtime: run", () => { const pausedJob: Job = { id: "test-job-id", coordinatorExpertKey: "test-expert", + runtimeVersion: "v1.0", status: "stoppedByInteractiveTool", totalSteps: 5, startedAt: Date.now(), @@ -292,6 +294,7 @@ describe("@perstack/runtime: run", () => { const customJob: Job = { id: "custom-job-id", coordinatorExpertKey: "custom-expert", + runtimeVersion: "v1.0", status: "running", totalSteps: 0, startedAt: Date.now(), diff --git a/apps/runtime/src/run.ts b/apps/runtime/src/run.ts index 3a340ad6..fca33877 100755 --- a/apps/runtime/src/run.ts +++ b/apps/runtime/src/run.ts @@ -6,10 +6,15 @@ import type { RunParamsInput, RunSetting, RuntimeEvent, + RuntimeVersion, Step, } from "@perstack/core" import { runParamsSchema } from "@perstack/core" -import { createEmptyUsage, type ResolveExpertToRunFn } from "./helpers/index.js" +import { + createEmptyUsage, + getCurrentRuntimeVersion, + type ResolveExpertToRunFn, +} from "./helpers/index.js" import { buildReturnFromDelegation, extractDelegationContext, @@ -31,9 +36,15 @@ export type RunOptions = { lockfile?: Lockfile } -const defaultCreateJob = (jobId: string, expertKey: string, maxSteps?: number): Job => ({ +const defaultCreateJob = ( + jobId: string, + expertKey: string, + maxSteps?: number, + runtimeVersion: RuntimeVersion = getCurrentRuntimeVersion(), +): Job => ({ id: jobId, coordinatorExpertKey: expertKey, + runtimeVersion, status: "running", totalSteps: 0, startedAt: Date.now(), diff --git a/e2e/experts/runtime-version.toml b/e2e/experts/runtime-version.toml new file mode 100644 index 00000000..dd733bf3 --- /dev/null +++ b/e2e/experts/runtime-version.toml @@ -0,0 +1,99 @@ +model = "claude-sonnet-4-5" + +[provider] +providerName = "anthropic" + +envPath = [".env", ".env.local"] + +# Expert with v1.0 minRuntimeVersion (compatible with 0.x.y) +[experts."e2e-runtime-v1"] +version = "1.0.0" +minRuntimeVersion = "v1.0" +instruction = "Call attemptCompletion with result 'OK'" + +[experts."e2e-runtime-v1".skills."@perstack/base"] +type = "mcpStdioSkill" +command = "npx" +packageName = "@perstack/base" +pick = ["attemptCompletion"] + +# Expert without minRuntimeVersion (default) +[experts."e2e-runtime-default"] +version = "1.0.0" +instruction = "Call attemptCompletion with result 'OK'" + +[experts."e2e-runtime-default".skills."@perstack/base"] +type = "mcpStdioSkill" +command = "npx" +packageName = "@perstack/base" +pick = ["attemptCompletion"] + +# Expert requiring future version (validation failure) +[experts."e2e-runtime-future"] +version = "1.0.0" +minRuntimeVersion = "v99.0" +instruction = "Call attemptCompletion with result 'OK'" + +[experts."e2e-runtime-future".skills."@perstack/base"] +type = "mcpStdioSkill" +command = "npx" +packageName = "@perstack/base" +pick = ["attemptCompletion"] + +# 3-level chain: root(v1.0) -> level1(v1.0) -> level2(v1.0) +[experts."e2e-runtime-chain-ok"] +version = "1.0.0" +minRuntimeVersion = "v1.0" +instruction = """ +1. Delegate to "e2e-runtime-chain-ok-l1" with "test" +2. When done, call attemptCompletion with result "OK" +""" +delegates = ["e2e-runtime-chain-ok-l1"] + +[experts."e2e-runtime-chain-ok".skills."@perstack/base"] +type = "mcpStdioSkill" +command = "npx" +packageName = "@perstack/base" +pick = ["attemptCompletion"] + +[experts."e2e-runtime-chain-ok-l1"] +version = "1.0.0" +minRuntimeVersion = "v1.0" +instruction = """ +1. Delegate to "e2e-runtime-chain-ok-l2" with "test" +2. When done, call attemptCompletion with result "OK" +""" +delegates = ["e2e-runtime-chain-ok-l2"] + +[experts."e2e-runtime-chain-ok-l1".skills."@perstack/base"] +type = "mcpStdioSkill" +command = "npx" +packageName = "@perstack/base" +pick = ["attemptCompletion"] + +[experts."e2e-runtime-chain-ok-l2"] +version = "1.0.0" +minRuntimeVersion = "v1.0" +instruction = "Call attemptCompletion with result 'OK'" + +[experts."e2e-runtime-chain-ok-l2".skills."@perstack/base"] +type = "mcpStdioSkill" +command = "npx" +packageName = "@perstack/base" +pick = ["attemptCompletion"] + +# Chain with nested future version: root(v1.0) -> nested(v99.0) +[experts."e2e-runtime-chain-future"] +version = "1.0.0" +minRuntimeVersion = "v1.0" +instruction = """ +1. Delegate to "e2e-runtime-future" with "test" +2. When done, call attemptCompletion with result "OK" +""" +delegates = ["e2e-runtime-future"] + +[experts."e2e-runtime-chain-future".skills."@perstack/base"] +type = "mcpStdioSkill" +command = "npx" +packageName = "@perstack/base" +pick = ["attemptCompletion"] diff --git a/e2e/perstack-runtime/runtime-version.test.ts b/e2e/perstack-runtime/runtime-version.test.ts new file mode 100644 index 00000000..c2d0b313 --- /dev/null +++ b/e2e/perstack-runtime/runtime-version.test.ts @@ -0,0 +1,95 @@ +/** + * Runtime Version E2E Tests + * + * Tests runtime version validation in perstack-runtime: + * - v1.0 minRuntimeVersion with 0.x.y runtime (special case) + * - No minRuntimeVersion (default) + * - Future version requirement (validation failure) + * - 3-level delegation chain with all v1.0 + * - Nested delegate with future version requirement + * + * TOML: e2e/experts/runtime-version.toml + */ +import { describe, expect, it } from "vitest" +import { assertEventSequenceContains } from "../lib/assertions.js" +import { filterEventsByType } from "../lib/event-parser.js" +import { runRuntimeCli, withEventParsing } from "../lib/runner.js" + +const RUNTIME_VERSION_CONFIG = "./e2e/experts/runtime-version.toml" +const LLM_TIMEOUT = 120000 +const LLM_EXTENDED_TIMEOUT = 300000 + +describe.concurrent("Runtime Version Validation", () => { + it( + "should succeed with v1.0 minRuntimeVersion on 0.x.y runtime", + async () => { + const cmdResult = await runRuntimeCli( + ["run", "--config", RUNTIME_VERSION_CONFIG, "e2e-runtime-v1", "test"], + { timeout: LLM_TIMEOUT }, + ) + const result = withEventParsing(cmdResult) + expect(result.exitCode).toBe(0) + expect(assertEventSequenceContains(result.events, ["startRun", "completeRun"]).passed).toBe( + true, + ) + }, + LLM_TIMEOUT, + ) + + it( + "should succeed with no minRuntimeVersion (default)", + async () => { + const cmdResult = await runRuntimeCli( + ["run", "--config", RUNTIME_VERSION_CONFIG, "e2e-runtime-default", "test"], + { timeout: LLM_TIMEOUT }, + ) + const result = withEventParsing(cmdResult) + expect(result.exitCode).toBe(0) + expect(assertEventSequenceContains(result.events, ["startRun", "completeRun"]).passed).toBe( + true, + ) + }, + LLM_TIMEOUT, + ) + + it( + "should fail when expert requires future version", + async () => { + const cmdResult = await runRuntimeCli( + ["run", "--config", RUNTIME_VERSION_CONFIG, "e2e-runtime-future", "test"], + { timeout: LLM_TIMEOUT }, + ) + expect(cmdResult.exitCode).toBe(1) + expect(cmdResult.stderr).toContain("v99.0") + }, + LLM_TIMEOUT, + ) + + it( + "should succeed with 3-level delegation chain all requiring v1.0", + async () => { + const cmdResult = await runRuntimeCli( + ["run", "--config", RUNTIME_VERSION_CONFIG, "e2e-runtime-chain-ok", "test"], + { timeout: LLM_EXTENDED_TIMEOUT }, + ) + const result = withEventParsing(cmdResult) + expect(result.exitCode).toBe(0) + const completeRunEvents = filterEventsByType(result.events, "completeRun") + expect(completeRunEvents.length).toBe(3) + }, + LLM_EXTENDED_TIMEOUT, + ) + + it( + "should fail when nested delegate requires future version", + async () => { + const cmdResult = await runRuntimeCli( + ["run", "--config", RUNTIME_VERSION_CONFIG, "e2e-runtime-chain-future", "test"], + { timeout: LLM_TIMEOUT }, + ) + expect(cmdResult.exitCode).toBe(1) + expect(cmdResult.stderr).toContain("v99.0") + }, + LLM_TIMEOUT, + ) +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a1ef38ea..13f5b9e8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,6 +14,7 @@ export * from "./schemas/provider-tools.js" export * from "./schemas/run-command.js" export * from "./schemas/runtime.js" export * from "./schemas/runtime-name.js" +export * from "./schemas/runtime-version.js" export * from "./schemas/skill.js" export * from "./schemas/skill-manager.js" export * from "./schemas/step.js" diff --git a/packages/core/src/schemas/expert.ts b/packages/core/src/schemas/expert.ts index 8dc71248..9c2ef6ec 100644 --- a/packages/core/src/schemas/expert.ts +++ b/packages/core/src/schemas/expert.ts @@ -8,6 +8,8 @@ import { } from "../constants/constants.js" import type { AnthropicProviderSkill, ProviderToolOptions } from "./provider-tools.js" import { anthropicProviderSkillSchema, providerToolOptionsSchema } from "./provider-tools.js" +import type { RuntimeVersion } from "./runtime-version.js" +import { runtimeVersionSchema } from "./runtime-version.js" import type { InteractiveSkill, McpSseSkill, McpStdioSkill, Skill } from "./skill.js" import { interactiveSkillSchema, mcpSseSkillSchema, mcpStdioSkillSchema } from "./skill.js" @@ -32,6 +34,8 @@ export interface Expert { delegates: string[] /** Tags for categorization and discovery */ tags: string[] + /** Minimum runtime version required to run this Expert */ + minRuntimeVersion?: RuntimeVersion /** Provider-specific tool names to enable (e.g., "webSearch", "codeExecution") */ providerTools?: string[] /** Anthropic Agent Skills configuration */ @@ -95,6 +99,7 @@ export const expertSchema = z.object({ }), delegates: z.array(z.string().regex(expertKeyRegex).min(1)).optional().default([]), tags: z.array(z.string().regex(tagNameRegex).min(1)).optional().default([]), + minRuntimeVersion: runtimeVersionSchema.optional(), providerTools: z.array(z.string()).optional(), providerSkills: z.array(anthropicProviderSkillSchema).optional(), providerToolOptions: providerToolOptionsSchema, diff --git a/packages/core/src/schemas/job.ts b/packages/core/src/schemas/job.ts index 44359dd7..8508f7f2 100644 --- a/packages/core/src/schemas/job.ts +++ b/packages/core/src/schemas/job.ts @@ -1,4 +1,6 @@ import { z } from "zod" +import type { RuntimeVersion } from "./runtime-version.js" +import { runtimeVersionSchema } from "./runtime-version.js" import type { Usage } from "./usage.js" import { usageSchema } from "./usage.js" @@ -21,6 +23,7 @@ export interface Job { id: string status: JobStatus coordinatorExpertKey: string + runtimeVersion: RuntimeVersion totalSteps: number maxSteps?: number usage: Usage @@ -32,6 +35,7 @@ export const jobSchema = z.object({ id: z.string(), status: jobStatusSchema, coordinatorExpertKey: z.string(), + runtimeVersion: runtimeVersionSchema, totalSteps: z.number(), maxSteps: z.number().optional(), usage: usageSchema, diff --git a/packages/core/src/schemas/perstack-toml.ts b/packages/core/src/schemas/perstack-toml.ts index b06f45a2..c22ce176 100644 --- a/packages/core/src/schemas/perstack-toml.ts +++ b/packages/core/src/schemas/perstack-toml.ts @@ -3,6 +3,8 @@ import { headersSchema } from "./provider-config.js" import { anthropicProviderSkillSchema, providerToolOptionsSchema } from "./provider-tools.js" import type { RuntimeName } from "./runtime-name.js" import { runtimeNameSchema } from "./runtime-name.js" +import type { RuntimeVersion } from "./runtime-version.js" +import { runtimeVersionSchema } from "./runtime-version.js" /** Reasoning budget for native LLM reasoning (extended thinking / test-time scaling) */ export type ReasoningBudget = "none" | "minimal" | "low" | "medium" | "high" | number @@ -216,7 +218,7 @@ export interface PerstackConfigExpert { /** Semantic version */ version?: string /** Minimum runtime version required */ - minRuntimeVersion?: string + minRuntimeVersion?: RuntimeVersion /** Description of the Expert */ description?: string /** System instruction */ @@ -284,7 +286,7 @@ export const perstackConfigSchema = z.object({ z.string(), z.object({ version: z.string().optional(), - minRuntimeVersion: z.string().optional(), + minRuntimeVersion: runtimeVersionSchema.optional(), description: z.string().optional(), instruction: z.string(), skills: z diff --git a/packages/core/src/schemas/runtime-version.ts b/packages/core/src/schemas/runtime-version.ts new file mode 100644 index 00000000..469d12af --- /dev/null +++ b/packages/core/src/schemas/runtime-version.ts @@ -0,0 +1,5 @@ +import { z } from "zod" + +export const runtimeVersions = ["v1.0"] as const +export type RuntimeVersion = (typeof runtimeVersions)[number] +export const runtimeVersionSchema = z.enum(runtimeVersions) diff --git a/packages/storages/filesystem/src/filesystem-storage.test.ts b/packages/storages/filesystem/src/filesystem-storage.test.ts index 919de5a2..7bb5fcb2 100644 --- a/packages/storages/filesystem/src/filesystem-storage.test.ts +++ b/packages/storages/filesystem/src/filesystem-storage.test.ts @@ -39,6 +39,7 @@ function createTestJob(overrides: Partial = {}): Job { id: overrides.id ?? createId(), status: "running", coordinatorExpertKey: "test-expert", + runtimeVersion: "v1.0", totalSteps: 0, usage: createEmptyUsage(), startedAt: overrides.startedAt ?? Date.now(), diff --git a/packages/storages/filesystem/src/job.ts b/packages/storages/filesystem/src/job.ts index 12ec7d50..8e0aea79 100644 --- a/packages/storages/filesystem/src/job.ts +++ b/packages/storages/filesystem/src/job.ts @@ -61,6 +61,7 @@ export function createInitialJob(jobId: string, expertKey: string, maxSteps?: nu id: jobId, status: "running", coordinatorExpertKey: expertKey, + runtimeVersion: "v1.0", totalSteps: 0, maxSteps, usage: { diff --git a/packages/storages/s3-compatible/src/s3-storage-base.test.ts b/packages/storages/s3-compatible/src/s3-storage-base.test.ts index 1a07f215..0cade9fc 100644 --- a/packages/storages/s3-compatible/src/s3-storage-base.test.ts +++ b/packages/storages/s3-compatible/src/s3-storage-base.test.ts @@ -93,6 +93,7 @@ function createTestJob(overrides: Partial = {}): Job { id: overrides.id ?? createId(), status: "running", coordinatorExpertKey: "test-expert", + runtimeVersion: "v1.0", totalSteps: 0, usage: createEmptyUsage(), startedAt: overrides.startedAt ?? Date.now(), diff --git a/packages/storages/s3-compatible/src/serialization.test.ts b/packages/storages/s3-compatible/src/serialization.test.ts index a4594333..3c05146a 100644 --- a/packages/storages/s3-compatible/src/serialization.test.ts +++ b/packages/storages/s3-compatible/src/serialization.test.ts @@ -52,6 +52,7 @@ describe("serialization", () => { id: createId(), status: "running", coordinatorExpertKey: "test-expert", + runtimeVersion: "v1.0", totalSteps: 5, usage: createEmptyUsage(), startedAt: Date.now(), From 57babf80184554c66d76ee47c843a40788ce1375 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 21 Jan 2026 16:51:48 +0000 Subject: [PATCH 2/4] chore: change changeset to patch bump Co-Authored-By: Claude Opus 4.5 --- .changeset/runtime-version.md | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.changeset/runtime-version.md b/.changeset/runtime-version.md index 8468cb83..c1e807a3 100644 --- a/.changeset/runtime-version.md +++ b/.changeset/runtime-version.md @@ -1,31 +1,31 @@ --- -"@perstack/core": minor -"@perstack/runtime": minor -"@perstack/base": minor -"@perstack/filesystem-storage": minor -"@perstack/s3-compatible-storage": minor -"@perstack/s3-storage": minor -"@perstack/r2-storage": minor -"@perstack/runner": minor -"@perstack/mock": minor -"@perstack/react": minor -"@perstack/anthropic-provider": minor -"@perstack/azure-openai-provider": minor -"@perstack/bedrock-provider": minor -"@perstack/deepseek-provider": minor -"@perstack/google-provider": minor -"@perstack/ollama-provider": minor -"@perstack/openai-provider": minor -"@perstack/vertex-provider": minor -"@perstack/provider-core": minor -"@perstack/adapter-base": minor -"@perstack/claude-code": minor -"@perstack/cursor": minor -"@perstack/docker": minor -"@perstack/gemini": minor -"@perstack/tui-components": minor -"create-expert": minor -"perstack": minor +"@perstack/core": patch +"@perstack/runtime": patch +"@perstack/base": patch +"@perstack/filesystem-storage": patch +"@perstack/s3-compatible-storage": patch +"@perstack/s3-storage": patch +"@perstack/r2-storage": patch +"@perstack/runner": patch +"@perstack/mock": patch +"@perstack/react": patch +"@perstack/anthropic-provider": patch +"@perstack/azure-openai-provider": patch +"@perstack/bedrock-provider": patch +"@perstack/deepseek-provider": patch +"@perstack/google-provider": patch +"@perstack/ollama-provider": patch +"@perstack/openai-provider": patch +"@perstack/vertex-provider": patch +"@perstack/provider-core": patch +"@perstack/adapter-base": patch +"@perstack/claude-code": patch +"@perstack/cursor": patch +"@perstack/docker": patch +"@perstack/gemini": patch +"@perstack/tui-components": patch +"create-expert": patch +"perstack": patch --- Add runtime version tracking to Job schema and validation From 6fd6401a264a0c3038bf5ab9ebd03c60e45cbb3b Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 21 Jan 2026 17:05:00 +0000 Subject: [PATCH 3/4] fix: make Expert.minRuntimeVersion required with default value - Changed minRuntimeVersion from optional to required in Expert interface - Added default value "v1.0" in schema - Updated all test files and helper functions to include minRuntimeVersion Co-Authored-By: Claude Opus 4.5 --- apps/perstack/src/install.ts | 3 +++ apps/runtime/src/helpers/resolve-expert.test.ts | 1 + apps/runtime/src/helpers/resolve-expert.ts | 2 +- apps/runtime/src/helpers/setup-experts.test.ts | 1 + apps/runtime/src/messages/message.test.ts | 7 +++++++ apps/runtime/src/perstack-adapter.test.ts | 1 + apps/runtime/src/skill-manager/delegate.test.ts | 1 + apps/runtime/src/skill-manager/helpers.test.ts | 2 ++ .../src/skill-manager/skill-manager-factory.test.ts | 1 + packages/core/src/schemas/expert.ts | 4 ++-- packages/runtimes/docker/src/docker-adapter.test.ts | 1 + 11 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/perstack/src/install.ts b/apps/perstack/src/install.ts index 1014c4cd..240b83b1 100644 --- a/apps/perstack/src/install.ts +++ b/apps/perstack/src/install.ts @@ -8,6 +8,7 @@ import { type Lockfile, type LockfileExpert, type PerstackConfig, + type RuntimeVersion, type Skill, } from "@perstack/core" import { collectToolDefinitionsForExpert } from "@perstack/runtime" @@ -41,6 +42,7 @@ type PublishedExpertData = { version: string description?: string instruction: string + minRuntimeVersion?: RuntimeVersion skills?: Record< string, | { @@ -139,6 +141,7 @@ function toRuntimeExpert(key: string, expert: PublishedExpertData): Expert { key, name: expert.name, version: expert.version, + minRuntimeVersion: expert.minRuntimeVersion ?? "v1.0", description: expert.description ?? "", instruction: expert.instruction, skills, diff --git a/apps/runtime/src/helpers/resolve-expert.test.ts b/apps/runtime/src/helpers/resolve-expert.test.ts index 94e2903d..7c1619c9 100644 --- a/apps/runtime/src/helpers/resolve-expert.test.ts +++ b/apps/runtime/src/helpers/resolve-expert.test.ts @@ -70,6 +70,7 @@ function createTestExpert(overrides: Partial = {}): Expert { }, delegates: [], tags: [], + minRuntimeVersion: "v1.0", ...overrides, } } diff --git a/apps/runtime/src/helpers/resolve-expert.ts b/apps/runtime/src/helpers/resolve-expert.ts index a68d4740..394823c8 100644 --- a/apps/runtime/src/helpers/resolve-expert.ts +++ b/apps/runtime/src/helpers/resolve-expert.ts @@ -135,7 +135,7 @@ function toRuntimeExpert( key, name: expert.name, version: expert.version, - minRuntimeVersion: expert.minRuntimeVersion, + minRuntimeVersion: expert.minRuntimeVersion ?? "v1.0", description: expert.description ?? "", instruction: expert.instruction, skills, diff --git a/apps/runtime/src/helpers/setup-experts.test.ts b/apps/runtime/src/helpers/setup-experts.test.ts index 3811cbc7..aef2c336 100644 --- a/apps/runtime/src/helpers/setup-experts.test.ts +++ b/apps/runtime/src/helpers/setup-experts.test.ts @@ -11,6 +11,7 @@ describe("@perstack/runtime: setupExperts", () => { skills: {}, delegates: [], tags: [], + minRuntimeVersion: "v1.0", } const baseSetting: RunSetting = { diff --git a/apps/runtime/src/messages/message.test.ts b/apps/runtime/src/messages/message.test.ts index e169d597..54b8d4ca 100644 --- a/apps/runtime/src/messages/message.test.ts +++ b/apps/runtime/src/messages/message.test.ts @@ -512,6 +512,7 @@ describe("@perstack/messages: instruction-message", () => { delegates: [], tags: [], runtime: ["local" as const], + minRuntimeVersion: "v1.0" as const, } const result = createInstructionMessage(expert, {}, startedAt) expect(result.type).toBe("instructionMessage") @@ -542,6 +543,7 @@ describe("@perstack/messages: instruction-message", () => { delegates: [], tags: [], runtime: ["local" as const], + minRuntimeVersion: "v1.0" as const, } const result = createInstructionMessage(expert, {}, startedAt) expect(result.contents[0].text).toContain("Always use this skill carefully.") @@ -569,6 +571,7 @@ describe("@perstack/messages: instruction-message", () => { delegates: [], tags: [], runtime: ["local" as const], + minRuntimeVersion: "v1.0" as const, } const result = createInstructionMessage(expert, {}, startedAt) expect(result.contents[0].text).not.toContain('"test-skill" skill rules:') @@ -584,6 +587,7 @@ describe("@perstack/messages: instruction-message", () => { delegates: ["delegate-expert"], tags: [], runtime: ["local" as const], + minRuntimeVersion: "v1.0" as const, } const experts = { "test-expert": expert, @@ -597,6 +601,7 @@ describe("@perstack/messages: instruction-message", () => { delegates: [], tags: [], runtime: ["local" as const], + minRuntimeVersion: "v1.0" as const, }, } const result = createInstructionMessage(expert, experts, startedAt) @@ -614,6 +619,7 @@ describe("@perstack/messages: instruction-message", () => { delegates: ["nonexistent-delegate"], tags: [], runtime: ["local" as const], + minRuntimeVersion: "v1.0" as const, } const result = createInstructionMessage(expert, {}, startedAt) expect(result.contents[0].text).not.toContain('About "') @@ -629,6 +635,7 @@ describe("@perstack/messages: instruction-message", () => { delegates: [], tags: [], runtime: ["local" as const], + minRuntimeVersion: "v1.0" as const, } const result = createInstructionMessage(expert, {}, startedAt) expect(result.contents[0].text).toContain("2023-11-14T22:13:20.000Z") diff --git a/apps/runtime/src/perstack-adapter.test.ts b/apps/runtime/src/perstack-adapter.test.ts index 8aed134c..ea501e63 100644 --- a/apps/runtime/src/perstack-adapter.test.ts +++ b/apps/runtime/src/perstack-adapter.test.ts @@ -98,6 +98,7 @@ describe("@perstack/runtime: PerstackAdapter", () => { skills: {}, delegates: [], tags: [], + minRuntimeVersion: "v1.0", } const config = adapter.convertExpert(expert) expect(config.instruction).toBe("Test instruction") diff --git a/apps/runtime/src/skill-manager/delegate.test.ts b/apps/runtime/src/skill-manager/delegate.test.ts index c61dbc17..024bdc9d 100644 --- a/apps/runtime/src/skill-manager/delegate.test.ts +++ b/apps/runtime/src/skill-manager/delegate.test.ts @@ -12,6 +12,7 @@ function createDelegateExpert(overrides: Partial = {}): Expert { skills: {}, delegates: [], tags: [], + minRuntimeVersion: "v1.0", ...overrides, } } diff --git a/apps/runtime/src/skill-manager/helpers.test.ts b/apps/runtime/src/skill-manager/helpers.test.ts index fc986f98..26a57af0 100644 --- a/apps/runtime/src/skill-manager/helpers.test.ts +++ b/apps/runtime/src/skill-manager/helpers.test.ts @@ -358,6 +358,7 @@ describe("skill-manager helpers", () => { }, delegates: [], tags: [], + minRuntimeVersion: "v1.0", ...overrides, }) @@ -475,6 +476,7 @@ describe("skill-manager helpers", () => { }, delegates: [], tags: [], + minRuntimeVersion: "v1.0", ...overrides, }) diff --git a/apps/runtime/src/skill-manager/skill-manager-factory.test.ts b/apps/runtime/src/skill-manager/skill-manager-factory.test.ts index 264c32e2..7aabc5a2 100644 --- a/apps/runtime/src/skill-manager/skill-manager-factory.test.ts +++ b/apps/runtime/src/skill-manager/skill-manager-factory.test.ts @@ -45,6 +45,7 @@ function createExpert(overrides: Partial = {}): Expert { skills: {}, delegates: [], tags: [], + minRuntimeVersion: "v1.0", ...overrides, } } diff --git a/packages/core/src/schemas/expert.ts b/packages/core/src/schemas/expert.ts index 9c2ef6ec..88f6809a 100644 --- a/packages/core/src/schemas/expert.ts +++ b/packages/core/src/schemas/expert.ts @@ -35,7 +35,7 @@ export interface Expert { /** Tags for categorization and discovery */ tags: string[] /** Minimum runtime version required to run this Expert */ - minRuntimeVersion?: RuntimeVersion + minRuntimeVersion: RuntimeVersion /** Provider-specific tool names to enable (e.g., "webSearch", "codeExecution") */ providerTools?: string[] /** Anthropic Agent Skills configuration */ @@ -99,7 +99,7 @@ export const expertSchema = z.object({ }), delegates: z.array(z.string().regex(expertKeyRegex).min(1)).optional().default([]), tags: z.array(z.string().regex(tagNameRegex).min(1)).optional().default([]), - minRuntimeVersion: runtimeVersionSchema.optional(), + minRuntimeVersion: runtimeVersionSchema.default("v1.0"), providerTools: z.array(z.string()).optional(), providerSkills: z.array(anthropicProviderSkillSchema).optional(), providerToolOptions: providerToolOptionsSchema, diff --git a/packages/runtimes/docker/src/docker-adapter.test.ts b/packages/runtimes/docker/src/docker-adapter.test.ts index d7e6c3bb..1083b3eb 100644 --- a/packages/runtimes/docker/src/docker-adapter.test.ts +++ b/packages/runtimes/docker/src/docker-adapter.test.ts @@ -81,6 +81,7 @@ describe("DockerAdapter", () => { skills: {}, delegates: [], tags: [], + minRuntimeVersion: "v1.0" as const, } const config = adapter.convertExpert(expert) expect(config.instruction).toBe("You are a test expert.") From 2618d434959a9a9e50815ad401fb70deee65e317 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 21 Jan 2026 17:06:19 +0000 Subject: [PATCH 4/4] refactor: align RuntimeVersion enum definition with codebase conventions Co-Authored-By: Claude Opus 4.5 --- packages/core/src/schemas/runtime-version.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/schemas/runtime-version.ts b/packages/core/src/schemas/runtime-version.ts index 469d12af..c20e4327 100644 --- a/packages/core/src/schemas/runtime-version.ts +++ b/packages/core/src/schemas/runtime-version.ts @@ -1,5 +1,5 @@ import { z } from "zod" -export const runtimeVersions = ["v1.0"] as const -export type RuntimeVersion = (typeof runtimeVersions)[number] -export const runtimeVersionSchema = z.enum(runtimeVersions) +export type RuntimeVersion = "v1.0" + +export const runtimeVersionSchema = z.enum(["v1.0"])