@relay-harness/coding-agent 1.0.2 → 1.0.4
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/CHANGELOG.md +27 -6
- package/LICENSE +22 -0
- package/README.md +14 -16
- package/dist/bundle/chunks/{chunk-H6S6DQTM.js → chunk-6AEKWHDP.js} +1 -1
- package/dist/bundle/chunks/{chunk-3QND5D7W.js → chunk-7ZBTLURV.js} +1 -1
- package/dist/bundle/chunks/{chunk-WMTXXI7H.js → chunk-FUPU6X73.js} +1 -1
- package/dist/bundle/chunks/{chunk-XDAWU2J5.js → chunk-IDUCLYG3.js} +1 -1
- package/dist/bundle/chunks/{chunk-XSLAMYCK.js → chunk-LVFZRSZQ.js} +556 -62
- package/dist/bundle/chunks/{chunk-ISNO4JTR.js → chunk-PJWDW5WR.js} +2 -2
- package/dist/bundle/chunks/{chunk-3XNI2SB6.js → chunk-TZ2ZVU7E.js} +1 -1
- package/dist/bundle/chunks/{chunk-MP5FU7MG.js → chunk-UVFWV6YH.js} +1 -1
- package/dist/bundle/chunks/{chunk-PTRF7IZB.js → chunk-WU7MEKPF.js} +1 -1
- package/dist/bundle/chunks/{cli-K4CNHWOI.js → cli-2LN4J4IN.js} +1 -1
- package/dist/bundle/chunks/{easter-egg-3d-TGRCIHA4.js → easter-egg-3d-URCOHV3B.js} +1 -1
- package/dist/bundle/chunks/{execute-6E5DA2QI.js → execute-JAHDBPR2.js} +1 -1
- package/dist/bundle/chunks/{runtime-O5HLCH3V.js → runtime-TTSK6Y5T.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-FNVEVB65.js → virtual-modules-2G2P7CK2.js} +1 -1
- package/dist/bundle/cli-runtime.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/settings-manager.d.ts +5 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/extensions/laya/assessment.d.ts +7 -1
- package/dist/extensions/laya/assessment.d.ts.map +1 -1
- package/dist/extensions/laya/assessment.js +21 -1
- package/dist/extensions/laya/assessment.js.map +1 -1
- package/dist/extensions/laya/docker.d.ts +19 -0
- package/dist/extensions/laya/docker.d.ts.map +1 -0
- package/dist/extensions/laya/docker.js +44 -0
- package/dist/extensions/laya/docker.js.map +1 -0
- package/dist/extensions/laya/index.d.ts +12 -3
- package/dist/extensions/laya/index.d.ts.map +1 -1
- package/dist/extensions/laya/index.js +331 -67
- package/dist/extensions/laya/index.js.map +1 -1
- package/dist/extensions/laya/learn.d.ts +67 -0
- package/dist/extensions/laya/learn.d.ts.map +1 -0
- package/dist/extensions/laya/learn.js +263 -0
- package/dist/extensions/laya/learn.js.map +1 -0
- package/dist/extensions/laya/memory.d.ts +48 -0
- package/dist/extensions/laya/memory.d.ts.map +1 -0
- package/dist/extensions/laya/memory.js +107 -0
- package/dist/extensions/laya/memory.js.map +1 -0
- package/dist/extensions/laya/model-manifest.d.ts +1 -1
- package/dist/extensions/laya/model-manifest.js +1 -1
- package/dist/extensions/laya/model-manifest.js.map +1 -1
- package/dist/extensions/laya/routers.d.ts +8 -0
- package/dist/extensions/laya/routers.d.ts.map +1 -1
- package/dist/extensions/laya/routers.js +2 -0
- package/dist/extensions/laya/routers.js.map +1 -1
- package/dist/extensions/laya/runtime.d.ts +39 -49
- package/dist/extensions/laya/runtime.d.ts.map +1 -1
- package/dist/extensions/laya/runtime.js +93 -145
- package/dist/extensions/laya/runtime.js.map +1 -1
- package/dist/extensions/laya/server.d.ts +39 -15
- package/dist/extensions/laya/server.d.ts.map +1 -1
- package/dist/extensions/laya/server.js +219 -78
- package/dist/extensions/laya/server.js.map +1 -1
- package/dist/extensions/laya/train-script.d.ts +8 -0
- package/dist/extensions/laya/train-script.d.ts.map +1 -0
- package/dist/extensions/laya/train-script.js +422 -0
- package/dist/extensions/laya/train-script.js.map +1 -0
- package/dist/extensions/laya/training.d.ts +176 -0
- package/dist/extensions/laya/training.d.ts.map +1 -0
- package/dist/extensions/laya/training.js +337 -0
- package/dist/extensions/laya/training.js.map +1 -0
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +3 -2
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +1 -1
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +9 -9
- package/dist/modes/interactive/theme/light.json +9 -9
- package/dist/modes/interactive/theme/system-theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/system-theme.js +11 -10
- package/dist/modes/interactive/theme/system-theme.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +10 -6
- package/dist/utils/version-check.js.map +1 -1
- package/docs/laya.md +59 -19
- package/docs/quickstart.md +19 -6
- package/docs/settings.md +4 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +15 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routers.js","sourceRoot":"","sources":["../../../src/extensions/laya/routers.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TaskAssessment } from \"./assessment.ts\";\nimport type { PolicyDecision } from \"./policy.ts\";\nimport { AGENTS, type AgentId, type ToolRequirement, type ValidationLevel } from \"./questions.ts\";\n\n/**\n * Agent, skill and tool routing. Relay has no agent registry, so the agent is a role the plan\n * message asks the model to take. Skills are reduced hierarchically: the agent picks a keyword\n * family, and only skills matching it are named. Tools are advised by default; with\n * `laya.toolRouting: \"enforce\"` the tools a task clearly does not need are deactivated.\n */\n\nconst AGENT_SKILL_KEYWORDS: Record<AgentId, string[]> = {\n\t\"software-engineer\": [\n\t\t\"code\",\n\t\t\"implement\",\n\t\t\"refactor\",\n\t\t\"typescript\",\n\t\t\"javascript\",\n\t\t\"python\",\n\t\t\"java\",\n\t\t\"golang\",\n\t\t\"rust\",\n\t],\n\t\"backend-engineer\": [\"api\", \"backend\", \"server\", \"service\", \"rest\", \"graphql\", \"queue\", \"spring\", \"node\"],\n\t\"frontend-engineer\": [\n\t\t\"frontend\",\n\t\t\"ui\",\n\t\t\"ux\",\n\t\t\"css\",\n\t\t\"react\",\n\t\t\"vue\",\n\t\t\"angular\",\n\t\t\"component\",\n\t\t\"accessibility\",\n\t\t\"design\",\n\t],\n\t\"mobile-engineer\": [\"mobile\", \"android\", \"ios\", \"swift\", \"kotlin\", \"flutter\", \"react-native\"],\n\t\"devops-engineer\": [\"devops\", \"ci\", \"deploy\", \"docker\", \"kubernetes\", \"infra\", \"terraform\", \"release\"],\n\t\"database-engineer\": [\"database\", \"sql\", \"postgres\", \"mysql\", \"migration\", \"schema\", \"query\", \"cockroach\"],\n\t\"security-engineer\": [\"security\", \"auth\", \"oauth\", \"vulnerab\", \"secret\", \"crypto\", \"audit\", \"compliance\"],\n\t\"software-architect\": [\"architecture\", \"system-design\", \"design\", \"ddd\", \"distributed\", \"event\", \"adr\"],\n\t\"qa-engineer\": [\"test\", \"qa\", \"testing\", \"coverage\", \"e2e\", \"playwright\", \"vitest\", \"jest\"],\n\tresearcher: [\"research\", \"explain\", \"analysis\", \"docs\", \"compare\", \"documentation\"],\n};\n\nexport interface SkillInfo {\n\tname: string;\n\tdescription: string;\n}\n\n/** Up to `limit` skills whose name or description matches the agent's keyword family or the request. */\nexport function selectSkills(skills: readonly SkillInfo[], agent: AgentId, request: string, limit = 3): string[] {\n\tconst keywords = AGENT_SKILL_KEYWORDS[agent];\n\tconst requestWords = new Set(request.toLowerCase().match(/[a-z0-9-]{4,}/g) ?? []);\n\treturn skills\n\t\t.map((skill) => {\n\t\t\tconst name = skill.name.toLowerCase();\n\t\t\tconst text = `${name} ${skill.description.toLowerCase()}`;\n\t\t\tconst keywordHits = keywords.filter((keyword) => text.includes(keyword)).length;\n\t\t\tconst requestHits = name.split(/[^a-z0-9]+/).filter((part) => requestWords.has(part)).length;\n\t\t\treturn { name: skill.name, score: keywordHits + 2 * requestHits };\n\t\t})\n\t\t.filter((skill) => skill.score > 0)\n\t\t.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))\n\t\t.slice(0, limit)\n\t\t.map((skill) => skill.name);\n}\n\n/** Tool name patterns per requirement. Tools matching none of them are never deactivated. */\nconst TOOL_PATTERNS: Record<ToolRequirement, RegExp> = {\n\trequires_write: /^(edit|write|multi_?edit|apply_patch)$/,\n\trequires_shell: /^bash$/,\n\trequires_tests: /^bash$/,\n\trequires_git: /^bash$/,\n\trequires_web: /web|fetch|search_web|http/,\n\trequires_browser: /browser|playwright|puppeteer/,\n\trequires_database: /sql|database|\\bdb\\b|postgres|mysql/,\n};\n\n/**\n * Tools to deactivate for this task: tools that match only requirements the task does not have.\n * `bash` serves shell, tests and git, so it stays when any of them is needed.\n */\nexport function unneededTools(activeTools: readonly string[], tools: Record<ToolRequirement, boolean>): string[] {\n\treturn activeTools.filter((name) => {\n\t\tconst matched = (Object.keys(TOOL_PATTERNS) as ToolRequirement[]).filter((id) => TOOL_PATTERNS[id].test(name));\n\t\treturn matched.length > 0 && matched.every((id) => !tools[id]);\n\t});\n}\n\nconst VALIDATION_HINTS: Record<ValidationLevel, string> = {\n\tnone: \"no check needed\",\n\tsyntax: \"check formatting or syntax of what changed\",\n\tcompile: \"run the type check or build\",\n\tunit_test: \"run the unit tests that cover the change\",\n\tintegration_test: \"run the integration or end-to-end tests of the affected flow\",\n\treview: \"review the change for security, data and architectural impact, then run the relevant tests\",\n};\n\nconst TOOL_LABELS: Record<ToolRequirement, string> = {\n\trequires_write: \"file edits\",\n\trequires_shell: \"shell\",\n\trequires_tests: \"tests\",\n\trequires_web: \"web\",\n\trequires_browser: \"browser\",\n\trequires_database: \"database\",\n\trequires_git: \"git\",\n};\n\n/** The advisory plan sent to the model next to the user's message. */\nexport function renderPlanMessage(\n\tassessment: TaskAssessment,\n\tpolicy: PolicyDecision,\n\tskills: readonly string[],\n\tenforcedTools: readonly string[],\n): string {\n\tconst percent = (value: number) => `${Math.round(value * 100)}%`;\n\tconst needed = (Object.keys(TOOL_LABELS) as ToolRequirement[]).filter((id) => assessment.tools[id]);\n\tconst notNeeded = (Object.keys(TOOL_LABELS) as ToolRequirement[]).filter((id) => !assessment.tools[id]);\n\tconst lines = [\n\t\t\"[laya:plan] Execution plan from the harness router for this request (guidance, not a constraint):\",\n\t\t`- Task: ${assessment.task.type.replaceAll(\"_\", \" \")}, scope ${assessment.task.scope.replaceAll(\"_\", \" \")}, complexity ${percent(assessment.task.complexity)}, risk ${percent(assessment.task.risk)}.`,\n\t\t`- Role: act as ${assessment.agent} (${AGENTS[assessment.agent]}).`,\n\t];\n\tif (skills.length > 0)\n\t\tlines.push(`- Skills likely relevant: ${skills.join(\", \")}. Load one only when a step needs it.`);\n\tlines.push(\n\t\t`- Tools: ${needed.length > 0 ? `needs ${needed.map((id) => TOOL_LABELS[id]).join(\", \")}` : \"read-only\"}${\n\t\t\tnotNeeded.length > 0 ? `; not expected: ${notNeeded.map((id) => TOOL_LABELS[id]).join(\", \")}` : \"\"\n\t\t}.`,\n\t);\n\tif (enforcedTools.length > 0) lines.push(`- Deactivated for this request: ${enforcedTools.join(\", \")}.`);\n\tif (policy.validation.level !== \"none\") {\n\t\tlines.push(\n\t\t\t`- Validation${policy.validation.required ? \" (required)\" : \"\"}: ${VALIDATION_HINTS[policy.validation.level]} before reporting completion.`,\n\t\t);\n\t}\n\tif (policy.rules.length > 0)\n\t\tlines.push(`- Sensitive area: ${policy.rules.join(\", \")}. Prefer the safest correct change.`);\n\tif (assessment.task.ambiguity >= 0.75) lines.push(\"- The request is open: confirm the goal before large changes.\");\n\treturn lines.join(\"\\n\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"routers.js","sourceRoot":"","sources":["../../../src/extensions/laya/routers.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TaskAssessment } from \"./assessment.ts\";\nimport type { PolicyDecision } from \"./policy.ts\";\nimport { AGENTS, type AgentId, type ToolRequirement, type ValidationLevel } from \"./questions.ts\";\n\n/**\n * Agent, skill and tool routing. Relay has no agent registry, so the agent is a role the plan\n * message asks the model to take. Skills are reduced hierarchically: the agent picks a keyword\n * family, and only skills matching it are named. Tools are advised by default; with\n * `laya.toolRouting: \"enforce\"` the tools a task clearly does not need are deactivated.\n */\n\n/** Custom message type of the plan sent with each routed request. */\nexport const LAYA_PLAN_MESSAGE = \"laya.plan\";\n\nconst AGENT_SKILL_KEYWORDS: Record<AgentId, string[]> = {\n\t\"software-engineer\": [\n\t\t\"code\",\n\t\t\"implement\",\n\t\t\"refactor\",\n\t\t\"typescript\",\n\t\t\"javascript\",\n\t\t\"python\",\n\t\t\"java\",\n\t\t\"golang\",\n\t\t\"rust\",\n\t],\n\t\"backend-engineer\": [\"api\", \"backend\", \"server\", \"service\", \"rest\", \"graphql\", \"queue\", \"spring\", \"node\"],\n\t\"frontend-engineer\": [\n\t\t\"frontend\",\n\t\t\"ui\",\n\t\t\"ux\",\n\t\t\"css\",\n\t\t\"react\",\n\t\t\"vue\",\n\t\t\"angular\",\n\t\t\"component\",\n\t\t\"accessibility\",\n\t\t\"design\",\n\t],\n\t\"mobile-engineer\": [\"mobile\", \"android\", \"ios\", \"swift\", \"kotlin\", \"flutter\", \"react-native\"],\n\t\"devops-engineer\": [\"devops\", \"ci\", \"deploy\", \"docker\", \"kubernetes\", \"infra\", \"terraform\", \"release\"],\n\t\"database-engineer\": [\"database\", \"sql\", \"postgres\", \"mysql\", \"migration\", \"schema\", \"query\", \"cockroach\"],\n\t\"security-engineer\": [\"security\", \"auth\", \"oauth\", \"vulnerab\", \"secret\", \"crypto\", \"audit\", \"compliance\"],\n\t\"software-architect\": [\"architecture\", \"system-design\", \"design\", \"ddd\", \"distributed\", \"event\", \"adr\"],\n\t\"qa-engineer\": [\"test\", \"qa\", \"testing\", \"coverage\", \"e2e\", \"playwright\", \"vitest\", \"jest\"],\n\tresearcher: [\"research\", \"explain\", \"analysis\", \"docs\", \"compare\", \"documentation\"],\n};\n\nexport interface SkillInfo {\n\tname: string;\n\tdescription: string;\n}\n\n/** Up to `limit` skills whose name or description matches the agent's keyword family or the request. */\nexport function selectSkills(skills: readonly SkillInfo[], agent: AgentId, request: string, limit = 3): string[] {\n\tconst keywords = AGENT_SKILL_KEYWORDS[agent];\n\tconst requestWords = new Set(request.toLowerCase().match(/[a-z0-9-]{4,}/g) ?? []);\n\treturn skills\n\t\t.map((skill) => {\n\t\t\tconst name = skill.name.toLowerCase();\n\t\t\tconst text = `${name} ${skill.description.toLowerCase()}`;\n\t\t\tconst keywordHits = keywords.filter((keyword) => text.includes(keyword)).length;\n\t\t\tconst requestHits = name.split(/[^a-z0-9]+/).filter((part) => requestWords.has(part)).length;\n\t\t\treturn { name: skill.name, score: keywordHits + 2 * requestHits };\n\t\t})\n\t\t.filter((skill) => skill.score > 0)\n\t\t.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))\n\t\t.slice(0, limit)\n\t\t.map((skill) => skill.name);\n}\n\n/** Tool name patterns per requirement. Tools matching none of them are never deactivated. */\nconst TOOL_PATTERNS: Record<ToolRequirement, RegExp> = {\n\trequires_write: /^(edit|write|multi_?edit|apply_patch)$/,\n\trequires_shell: /^bash$/,\n\trequires_tests: /^bash$/,\n\trequires_git: /^bash$/,\n\trequires_web: /web|fetch|search_web|http/,\n\trequires_browser: /browser|playwright|puppeteer/,\n\trequires_database: /sql|database|\\bdb\\b|postgres|mysql/,\n};\n\n/**\n * Tools to deactivate for this task: tools that match only requirements the task does not have.\n * `bash` serves shell, tests and git, so it stays when any of them is needed.\n */\nexport function unneededTools(activeTools: readonly string[], tools: Record<ToolRequirement, boolean>): string[] {\n\treturn activeTools.filter((name) => {\n\t\tconst matched = (Object.keys(TOOL_PATTERNS) as ToolRequirement[]).filter((id) => TOOL_PATTERNS[id].test(name));\n\t\treturn matched.length > 0 && matched.every((id) => !tools[id]);\n\t});\n}\n\nconst VALIDATION_HINTS: Record<ValidationLevel, string> = {\n\tnone: \"no check needed\",\n\tsyntax: \"check formatting or syntax of what changed\",\n\tcompile: \"run the type check or build\",\n\tunit_test: \"run the unit tests that cover the change\",\n\tintegration_test: \"run the integration or end-to-end tests of the affected flow\",\n\treview: \"review the change for security, data and architectural impact, then run the relevant tests\",\n};\n\nconst TOOL_LABELS: Record<ToolRequirement, string> = {\n\trequires_write: \"file edits\",\n\trequires_shell: \"shell\",\n\trequires_tests: \"tests\",\n\trequires_web: \"web\",\n\trequires_browser: \"browser\",\n\trequires_database: \"database\",\n\trequires_git: \"git\",\n};\n\n/** The advisory plan sent to the model next to the user's message. */\nexport function renderPlanMessage(\n\tassessment: TaskAssessment,\n\tpolicy: PolicyDecision,\n\tskills: readonly string[],\n\tenforcedTools: readonly string[],\n): string {\n\tconst percent = (value: number) => `${Math.round(value * 100)}%`;\n\tconst needed = (Object.keys(TOOL_LABELS) as ToolRequirement[]).filter((id) => assessment.tools[id]);\n\tconst notNeeded = (Object.keys(TOOL_LABELS) as ToolRequirement[]).filter((id) => !assessment.tools[id]);\n\tconst lines = [\n\t\t\"[laya:plan] Execution plan from the harness router for this request (guidance, not a constraint):\",\n\t\t`- Task: ${assessment.task.type.replaceAll(\"_\", \" \")}, scope ${assessment.task.scope.replaceAll(\"_\", \" \")}, complexity ${percent(assessment.task.complexity)}, risk ${percent(assessment.task.risk)}.`,\n\t\t`- Role: act as ${assessment.agent} (${AGENTS[assessment.agent]}).`,\n\t];\n\tif (skills.length > 0)\n\t\tlines.push(`- Skills likely relevant: ${skills.join(\", \")}. Load one only when a step needs it.`);\n\tlines.push(\n\t\t`- Tools: ${needed.length > 0 ? `needs ${needed.map((id) => TOOL_LABELS[id]).join(\", \")}` : \"read-only\"}${\n\t\t\tnotNeeded.length > 0 ? `; not expected: ${notNeeded.map((id) => TOOL_LABELS[id]).join(\", \")}` : \"\"\n\t\t}.`,\n\t);\n\tif (enforcedTools.length > 0) lines.push(`- Deactivated for this request: ${enforcedTools.join(\", \")}.`);\n\tif (policy.validation.level !== \"none\") {\n\t\tlines.push(\n\t\t\t`- Validation${policy.validation.required ? \" (required)\" : \"\"}: ${VALIDATION_HINTS[policy.validation.level]} before reporting completion.`,\n\t\t);\n\t}\n\tif (policy.rules.length > 0)\n\t\tlines.push(`- Sensitive area: ${policy.rules.join(\", \")}. Prefer the safest correct change.`);\n\tif (assessment.task.ambiguity >= 0.75) lines.push(\"- The request is open: confirm the goal before large changes.\");\n\treturn lines.join(\"\\n\");\n}\n"]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Laya runs in Docker, never on the host: an image carries Python, torch, the `laya` package, the
|
|
3
|
+
* shipped model and the scripts that serve and train it. Relay pulls the image, keeps one server
|
|
4
|
+
* container running and starts a short-lived container for each training. The host only keeps
|
|
5
|
+
* Relay's own data (training exercises, the model registry, telemetry).
|
|
5
6
|
*/
|
|
6
7
|
export interface LayaModelFile {
|
|
7
8
|
/** Path inside the model directory, with forward slashes. */
|
|
8
9
|
path: string;
|
|
9
|
-
/**
|
|
10
|
+
/** File name in the model repository. */
|
|
10
11
|
asset: string;
|
|
11
12
|
size: number;
|
|
12
13
|
sha256: string;
|
|
@@ -15,54 +16,43 @@ export interface LayaModelManifest {
|
|
|
15
16
|
version: string;
|
|
16
17
|
/** pip requirement for the Laya runtime. */
|
|
17
18
|
layaPackage: string;
|
|
18
|
-
/** Directory URL the
|
|
19
|
+
/** Directory URL the model files are downloaded from when the image is built, ending in a slash. */
|
|
19
20
|
baseUrl: string;
|
|
20
21
|
files: LayaModelFile[];
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
export
|
|
42
|
-
export declare function runtimeStatus(manifest: LayaModelManifest, paths: LayaPaths): LayaRuntimeStatus;
|
|
43
|
-
export interface DownloadOptions {
|
|
44
|
-
fetch?: typeof fetch;
|
|
45
|
-
signal?: AbortSignal;
|
|
46
|
-
onProgress?: (file: LayaModelFile, received: number) => void;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Downloads the files that are missing or wrong, verifying each against its manifest hash before it
|
|
50
|
-
* becomes visible: a download is written to a `.part` file and renamed only when the hash matches.
|
|
51
|
-
*/
|
|
52
|
-
export declare function downloadModel(manifest: LayaModelManifest, paths: LayaPaths, options?: DownloadOptions): Promise<void>;
|
|
53
|
-
/** A Python 3.10 to 3.13 interpreter (the range torch publishes wheels for), as command plus leading args. */
|
|
54
|
-
export declare function findPython(exec: ExecFunction, override?: string): Promise<[string, string[]] | undefined>;
|
|
55
|
-
export interface SetupOptions {
|
|
56
|
-
exec: ExecFunction;
|
|
57
|
-
home: string;
|
|
58
|
-
manifest: LayaModelManifest;
|
|
59
|
-
python?: string;
|
|
60
|
-
fetch?: typeof fetch;
|
|
61
|
-
signal?: AbortSignal;
|
|
62
|
-
report: (message: string) => void;
|
|
63
|
-
}
|
|
64
|
-
/** Creates the environment, installs torch and Laya, writes the server script and downloads the model. */
|
|
65
|
-
export declare function setupLayaRuntime(options: SetupOptions): Promise<LayaPaths>;
|
|
23
|
+
/** Registry the images are published to. */
|
|
24
|
+
export declare const LAYA_IMAGE_REPOSITORY = "ghcr.io/eltonssouza/relay-laya";
|
|
25
|
+
/** The server container, kept running between Relay sessions. */
|
|
26
|
+
export declare const LAYA_CONTAINER = "relay-laya";
|
|
27
|
+
/** The container of a running training; removed when it ends. */
|
|
28
|
+
export declare const LAYA_TRAIN_CONTAINER = "relay-laya-train";
|
|
29
|
+
/** Volume with the models trained from sessions. */
|
|
30
|
+
export declare const LAYA_MODELS_VOLUME = "relay-laya-models";
|
|
31
|
+
/** Port the server listens on inside its container. */
|
|
32
|
+
export declare const LAYA_CONTAINER_PORT = 8000;
|
|
33
|
+
/** Paths inside the image and its containers. */
|
|
34
|
+
export declare const LAYA_IMAGE_PATHS: {
|
|
35
|
+
readonly shippedModel: "/opt/laya/model";
|
|
36
|
+
readonly trainedModels: "/data/models";
|
|
37
|
+
readonly serveScript: "/opt/laya/serve.py";
|
|
38
|
+
readonly trainScript: "/opt/laya/train.py";
|
|
39
|
+
readonly workspace: "/workspace";
|
|
40
|
+
};
|
|
41
|
+
/** `cpu` runs everywhere; `cuda` carries the CUDA build of torch for machines with an NVIDIA GPU. */
|
|
42
|
+
export type LayaImageVariant = "cpu" | "cuda";
|
|
66
43
|
/** Serves the model with the System One protocol Relay's `typesafe-system-one` classifier speaks. */
|
|
67
44
|
export declare const SERVE_SCRIPT = "import argparse\n\nimport laya\nimport torch\nimport uvicorn\nfrom fastapi import FastAPI, HTTPException\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--model\", required=True)\nparser.add_argument(\"--host\", default=\"127.0.0.1\")\nparser.add_argument(\"--port\", type=int, default=8000)\nargs = parser.parse_args()\n\nagent = laya.load(args.model, device=\"cuda\" if torch.cuda.is_available() else \"cpu\")\napp = FastAPI(title=\"relay-laya\")\n\n\n@app.get(\"/health\")\ndef health():\n return {\"ok\": True, \"model\": args.model}\n\n\n@app.post(\"/v1/systemone\")\ndef system_one(body: dict):\n if \"state\" not in body or not body.get(\"questions\"):\n raise HTTPException(422, \"send 'state' and 'questions'\")\n return agent.predict(body[\"state\"], body[\"questions\"])\n\n\nuvicorn.run(app, host=args.host, port=args.port, log_level=\"warning\")\n";
|
|
45
|
+
/** Downloads the shipped model while the image is built, checking every file against the manifest. */
|
|
46
|
+
export declare const FETCH_MODEL_SCRIPT = "import hashlib\nimport json\nimport pathlib\nimport sys\nimport urllib.request\n\nmanifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding=\"utf-8\"))\ntarget = pathlib.Path(sys.argv[2])\nfor file in manifest[\"files\"]:\n path = target / file[\"path\"]\n path.parent.mkdir(parents=True, exist_ok=True)\n digest = hashlib.sha256()\n size = 0\n with urllib.request.urlopen(manifest[\"baseUrl\"] + file[\"asset\"]) as response, open(path, \"wb\") as out:\n while chunk := response.read(1 << 20):\n digest.update(chunk)\n size += len(chunk)\n out.write(chunk)\n if size != file[\"size\"] or digest.hexdigest() != file[\"sha256\"]:\n sys.exit(f\"{file['asset']} does not match the manifest (size {size}, sha256 {digest.hexdigest()})\")\n print(f\"{file['path']} ok\", flush=True)\n";
|
|
47
|
+
/** Files of the image's build context, by name. */
|
|
48
|
+
export declare function layaImageContext(manifest: LayaModelManifest, variant: LayaImageVariant): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Image tag: the model version, the variant and a hash of the build context. Changing a script, a
|
|
51
|
+
* dependency or the model gives a new tag, so a Relay version always runs the image built from its
|
|
52
|
+
* own sources, and an unchanged context reuses the published image.
|
|
53
|
+
*/
|
|
54
|
+
export declare function layaImageTag(manifest: LayaModelManifest, variant: LayaImageVariant): string;
|
|
55
|
+
export declare function layaImage(manifest: LayaModelManifest, variant: LayaImageVariant): string;
|
|
56
|
+
/** Container path of a model trained from sessions. */
|
|
57
|
+
export declare function trainedModelPath(name: string): string;
|
|
68
58
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/extensions/laya/runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/extensions/laya/runtime.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC7B,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,oGAAoG;IACpG,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,aAAa,EAAE,CAAC;CACvB;AAED,4CAA4C;AAC5C,eAAO,MAAM,qBAAqB,mCAAmC,CAAC;AACtE,iEAAiE;AACjE,eAAO,MAAM,cAAc,eAAe,CAAC;AAC3C,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,qBAAqB,CAAC;AACvD,oDAAoD;AACpD,eAAO,MAAM,kBAAkB,sBAAsB,CAAC;AACtD,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,iDAAiD;AACjD,eAAO,MAAM,gBAAgB;aAC5B,YAAY,EAAE,iBAAiB;aAC/B,aAAa,EAAE,cAAc;aAC7B,WAAW,EAAE,oBAAoB;aACjC,WAAW,EAAE,oBAAoB;aACjC,SAAS,EAAE,YAAY;CACd,CAAC;AAEX,qGAAqG;AACrG,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,CAAC;AAO9C,qGAAqG;AACrG,eAAO,MAAM,YAAY,83BA8BxB,CAAC;AAEF,sGAAsG;AACtG,eAAO,MAAM,kBAAkB,41BAqB9B,CAAC;AAqBF,mDAAmD;AACnD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ/G;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAQ3F;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAExF;AAED,uDAAuD;AACvD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD"}
|
|
@@ -1,149 +1,27 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
export function runtimeStatus(manifest, paths) {
|
|
30
|
-
if (!existsSync(paths.venvPython) || !existsSync(paths.serveScript))
|
|
31
|
-
return "missing-environment";
|
|
32
|
-
return missingModelFiles(manifest, paths).length > 0 ? "missing-model" : "ready";
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Downloads the files that are missing or wrong, verifying each against its manifest hash before it
|
|
36
|
-
* becomes visible: a download is written to a `.part` file and renamed only when the hash matches.
|
|
37
|
-
*/
|
|
38
|
-
export async function downloadModel(manifest, paths, options = {}) {
|
|
39
|
-
const request = options.fetch ?? globalThis.fetch;
|
|
40
|
-
for (const file of missingModelFiles(manifest, paths)) {
|
|
41
|
-
const target = join(paths.model, ...file.path.split("/"));
|
|
42
|
-
const partial = `${target}.part`;
|
|
43
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
44
|
-
const response = await request(new URL(file.asset, manifest.baseUrl), { signal: options.signal });
|
|
45
|
-
if (!response.ok || !response.body) {
|
|
46
|
-
throw new Error(`Downloading ${file.asset} failed: HTTP ${response.status}`);
|
|
47
|
-
}
|
|
48
|
-
const hash = createHash("sha256");
|
|
49
|
-
let received = 0;
|
|
50
|
-
const meter = new Transform({
|
|
51
|
-
transform(chunk, _encoding, callback) {
|
|
52
|
-
hash.update(chunk);
|
|
53
|
-
received += chunk.length;
|
|
54
|
-
options.onProgress?.(file, received);
|
|
55
|
-
callback(null, chunk);
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
try {
|
|
59
|
-
await pipeline(Readable.fromWeb(response.body), meter, createWriteStream(partial), { signal: options.signal });
|
|
60
|
-
const digest = hash.digest("hex");
|
|
61
|
-
if (received !== file.size || digest !== file.sha256) {
|
|
62
|
-
throw new Error(`${file.asset} does not match the manifest (size ${received}, sha256 ${digest})`);
|
|
63
|
-
}
|
|
64
|
-
renameSync(partial, target);
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
rmSync(partial, { force: true });
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const PYTHON_CANDIDATES = [
|
|
73
|
-
["python3", []],
|
|
74
|
-
["python", []],
|
|
75
|
-
["py", ["-3"]],
|
|
76
|
-
];
|
|
77
|
-
/** A Python 3.10 to 3.13 interpreter (the range torch publishes wheels for), as command plus leading args. */
|
|
78
|
-
export async function findPython(exec, override) {
|
|
79
|
-
const candidates = override ? [[override, []]] : PYTHON_CANDIDATES;
|
|
80
|
-
for (const [command, leading] of candidates) {
|
|
81
|
-
try {
|
|
82
|
-
const result = await exec(command, [...leading, "-c", "import sys; print(sys.version_info[0], sys.version_info[1])"], { timeout: 15_000 });
|
|
83
|
-
const [major, minor] = result.stdout.trim().split(" ").map(Number);
|
|
84
|
-
if (result.code === 0 && major === 3 && minor >= 10 && minor <= 13)
|
|
85
|
-
return [command, leading];
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// Not installed.
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
const PIP_TIMEOUT_MS = 30 * 60_000;
|
|
94
|
-
async function run(exec, command, args, options, failure) {
|
|
95
|
-
const result = await exec(command, args, options);
|
|
96
|
-
if (result.code !== 0) {
|
|
97
|
-
throw new Error(`${failure}: ${(result.stderr || result.stdout).trim().split("\n").slice(-4).join(" ")}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/** Torch wheel index: a CUDA build only where an NVIDIA GPU exists, the small CPU build otherwise. */
|
|
101
|
-
async function torchArgs(exec) {
|
|
102
|
-
if (process.platform === "darwin")
|
|
103
|
-
return ["torch"];
|
|
104
|
-
let gpu = false;
|
|
105
|
-
try {
|
|
106
|
-
gpu = (await exec("nvidia-smi", ["-L"], { timeout: 10_000 })).code === 0;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// No NVIDIA driver.
|
|
110
|
-
}
|
|
111
|
-
return ["torch", "--index-url", `https://download.pytorch.org/whl/${gpu ? "cu128" : "cpu"}`];
|
|
112
|
-
}
|
|
113
|
-
/** Creates the environment, installs torch and Laya, writes the server script and downloads the model. */
|
|
114
|
-
export async function setupLayaRuntime(options) {
|
|
115
|
-
const { exec, report, signal } = options;
|
|
116
|
-
const paths = layaPaths(options.home, options.manifest);
|
|
117
|
-
mkdirSync(paths.home, { recursive: true });
|
|
118
|
-
const importable = async () => existsSync(paths.venvPython) &&
|
|
119
|
-
(await exec(paths.venvPython, ["-c", "import laya, torch, fastapi, uvicorn"], { timeout: 120_000 })).code === 0;
|
|
120
|
-
if (!(await importable())) {
|
|
121
|
-
const python = await findPython(exec, options.python);
|
|
122
|
-
if (!python) {
|
|
123
|
-
throw new Error("Laya needs Python 3.10 to 3.13. Install it, or set laya.python to its path.");
|
|
124
|
-
}
|
|
125
|
-
report("Creating the Python environment");
|
|
126
|
-
await run(exec, python[0], [...python[1], "-m", "venv", paths.venv], { signal, timeout: 120_000 }, "Creating the environment failed");
|
|
127
|
-
const pip = [paths.venvPython, "-m", "pip", "install", "-q", "--disable-pip-version-check"];
|
|
128
|
-
report("Installing torch (large download)");
|
|
129
|
-
await run(exec, pip[0], [...pip.slice(1), ...(await torchArgs(exec))], { signal, timeout: PIP_TIMEOUT_MS }, "Installing torch failed");
|
|
130
|
-
report("Installing Laya");
|
|
131
|
-
await run(exec, pip[0], [...pip.slice(1), options.manifest.layaPackage, "fastapi", "uvicorn"], { signal, timeout: PIP_TIMEOUT_MS }, "Installing Laya failed");
|
|
132
|
-
}
|
|
133
|
-
writeFileSync(paths.serveScript, SERVE_SCRIPT, "utf8");
|
|
134
|
-
let lastReported = 0;
|
|
135
|
-
await downloadModel(options.manifest, paths, {
|
|
136
|
-
fetch: options.fetch,
|
|
137
|
-
signal,
|
|
138
|
-
onProgress: (file, received) => {
|
|
139
|
-
if (file.size < 10_000_000 || received - lastReported < 25_000_000)
|
|
140
|
-
return;
|
|
141
|
-
lastReported = received;
|
|
142
|
-
report(`Downloading the Laya model: ${file.asset} ${Math.round((received / file.size) * 100)}%`);
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
return paths;
|
|
146
|
-
}
|
|
2
|
+
import { TRAIN_SCRIPT } from "./train-script.js";
|
|
3
|
+
/** Registry the images are published to. */
|
|
4
|
+
export const LAYA_IMAGE_REPOSITORY = "ghcr.io/eltonssouza/relay-laya";
|
|
5
|
+
/** The server container, kept running between Relay sessions. */
|
|
6
|
+
export const LAYA_CONTAINER = "relay-laya";
|
|
7
|
+
/** The container of a running training; removed when it ends. */
|
|
8
|
+
export const LAYA_TRAIN_CONTAINER = "relay-laya-train";
|
|
9
|
+
/** Volume with the models trained from sessions. */
|
|
10
|
+
export const LAYA_MODELS_VOLUME = "relay-laya-models";
|
|
11
|
+
/** Port the server listens on inside its container. */
|
|
12
|
+
export const LAYA_CONTAINER_PORT = 8000;
|
|
13
|
+
/** Paths inside the image and its containers. */
|
|
14
|
+
export const LAYA_IMAGE_PATHS = {
|
|
15
|
+
shippedModel: "/opt/laya/model",
|
|
16
|
+
trainedModels: "/data/models",
|
|
17
|
+
serveScript: "/opt/laya/serve.py",
|
|
18
|
+
trainScript: "/opt/laya/train.py",
|
|
19
|
+
workspace: "/workspace",
|
|
20
|
+
};
|
|
21
|
+
const TORCH_INDEX = {
|
|
22
|
+
cpu: "https://download.pytorch.org/whl/cpu",
|
|
23
|
+
cuda: "https://download.pytorch.org/whl/cu128",
|
|
24
|
+
};
|
|
147
25
|
/** Serves the model with the System One protocol Relay's `typesafe-system-one` classifier speaks. */
|
|
148
26
|
export const SERVE_SCRIPT = `import argparse
|
|
149
27
|
|
|
@@ -176,4 +54,74 @@ def system_one(body: dict):
|
|
|
176
54
|
|
|
177
55
|
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
|
|
178
56
|
`;
|
|
57
|
+
/** Downloads the shipped model while the image is built, checking every file against the manifest. */
|
|
58
|
+
export const FETCH_MODEL_SCRIPT = `import hashlib
|
|
59
|
+
import json
|
|
60
|
+
import pathlib
|
|
61
|
+
import sys
|
|
62
|
+
import urllib.request
|
|
63
|
+
|
|
64
|
+
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
65
|
+
target = pathlib.Path(sys.argv[2])
|
|
66
|
+
for file in manifest["files"]:
|
|
67
|
+
path = target / file["path"]
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
digest = hashlib.sha256()
|
|
70
|
+
size = 0
|
|
71
|
+
with urllib.request.urlopen(manifest["baseUrl"] + file["asset"]) as response, open(path, "wb") as out:
|
|
72
|
+
while chunk := response.read(1 << 20):
|
|
73
|
+
digest.update(chunk)
|
|
74
|
+
size += len(chunk)
|
|
75
|
+
out.write(chunk)
|
|
76
|
+
if size != file["size"] or digest.hexdigest() != file["sha256"]:
|
|
77
|
+
sys.exit(f"{file['asset']} does not match the manifest (size {size}, sha256 {digest.hexdigest()})")
|
|
78
|
+
print(f"{file['path']} ok", flush=True)
|
|
79
|
+
`;
|
|
80
|
+
function dockerfile(manifest, variant) {
|
|
81
|
+
const paths = LAYA_IMAGE_PATHS;
|
|
82
|
+
return `FROM python:3.11-slim
|
|
83
|
+
LABEL org.opencontainers.image.source="https://github.com/eltonssouza/relay-harness"
|
|
84
|
+
LABEL org.opencontainers.image.description="Laya routing model for Relay (${variant})"
|
|
85
|
+
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PYTHONIOENCODING=utf-8 PIP_NO_CACHE_DIR=1 \\
|
|
86
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1 USE_TF=0 HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 TOKENIZERS_PARALLELISM=false
|
|
87
|
+
RUN pip install --upgrade pip && pip install torch --index-url ${TORCH_INDEX[variant]}
|
|
88
|
+
RUN pip install "${manifest.layaPackage}" fastapi uvicorn
|
|
89
|
+
RUN useradd --create-home laya && mkdir -p ${paths.trainedModels} && chown laya ${paths.trainedModels}
|
|
90
|
+
COPY fetch_model.py model.json /opt/laya/
|
|
91
|
+
RUN python /opt/laya/fetch_model.py /opt/laya/model.json ${paths.shippedModel}
|
|
92
|
+
COPY serve.py train.py /opt/laya/
|
|
93
|
+
USER laya
|
|
94
|
+
EXPOSE ${LAYA_CONTAINER_PORT}
|
|
95
|
+
CMD ["python", "${paths.serveScript}", "--model", "${paths.shippedModel}", "--host", "0.0.0.0", "--port", "${LAYA_CONTAINER_PORT}"]
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
/** Files of the image's build context, by name. */
|
|
99
|
+
export function layaImageContext(manifest, variant) {
|
|
100
|
+
return {
|
|
101
|
+
Dockerfile: dockerfile(manifest, variant),
|
|
102
|
+
"fetch_model.py": FETCH_MODEL_SCRIPT,
|
|
103
|
+
"model.json": `${JSON.stringify({ baseUrl: manifest.baseUrl, files: manifest.files }, null, 2)}\n`,
|
|
104
|
+
"serve.py": SERVE_SCRIPT,
|
|
105
|
+
"train.py": TRAIN_SCRIPT,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Image tag: the model version, the variant and a hash of the build context. Changing a script, a
|
|
110
|
+
* dependency or the model gives a new tag, so a Relay version always runs the image built from its
|
|
111
|
+
* own sources, and an unchanged context reuses the published image.
|
|
112
|
+
*/
|
|
113
|
+
export function layaImageTag(manifest, variant) {
|
|
114
|
+
const hash = createHash("sha256");
|
|
115
|
+
for (const [name, content] of Object.entries(layaImageContext(manifest, variant)).sort(([a], [b]) => a.localeCompare(b))) {
|
|
116
|
+
hash.update(`${name}\0${content}\0`);
|
|
117
|
+
}
|
|
118
|
+
return `${manifest.version}-${variant}-${hash.digest("hex").slice(0, 12)}`;
|
|
119
|
+
}
|
|
120
|
+
export function layaImage(manifest, variant) {
|
|
121
|
+
return `${LAYA_IMAGE_REPOSITORY}:${layaImageTag(manifest, variant)}`;
|
|
122
|
+
}
|
|
123
|
+
/** Container path of a model trained from sessions. */
|
|
124
|
+
export function trainedModelPath(name) {
|
|
125
|
+
return `${LAYA_IMAGE_PATHS.trainedModels}/${name}`;
|
|
126
|
+
}
|
|
179
127
|
//# sourceMappingURL=runtime.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/extensions/laya/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { createWriteStream, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { Readable, Transform } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\n\n/**\n * Local Laya runtime: the trained model, a Python environment with the `laya` package, and the\n * script that serves them. The npm package carries none of these (the model is 650 MB, the\n * environment a few GB); `setupLayaRuntime()` fetches them on first use into the agent directory.\n */\n\nexport interface LayaModelFile {\n\t/** Path inside the model directory, with forward slashes. */\n\tpath: string;\n\t/** Flat file name of the release asset. */\n\tasset: string;\n\tsize: number;\n\tsha256: string;\n}\n\nexport interface LayaModelManifest {\n\tversion: string;\n\t/** pip requirement for the Laya runtime. */\n\tlayaPackage: string;\n\t/** Directory URL the assets are downloaded from, ending in a slash. */\n\tbaseUrl: string;\n\tfiles: LayaModelFile[];\n}\n\nexport type ExecFunction = (\n\tcommand: string,\n\targs: string[],\n\toptions?: { signal?: AbortSignal; timeout?: number },\n) => Promise<{ stdout: string; stderr: string; code: number }>;\n\nexport interface LayaPaths {\n\thome: string;\n\tmodel: string;\n\tvenv: string;\n\tvenvPython: string;\n\tserveScript: string;\n\tlog: string;\n}\n\nexport function layaPaths(home: string, manifest: LayaModelManifest): LayaPaths {\n\tconst venv = join(home, \"venv\");\n\treturn {\n\t\thome,\n\t\tmodel: join(home, \"models\", manifest.version),\n\t\tvenv,\n\t\tvenvPython: join(\n\t\t\tvenv,\n\t\t\tprocess.platform === \"win32\" ? \"Scripts\" : \"bin\",\n\t\t\tprocess.platform === \"win32\" ? \"python.exe\" : \"python\",\n\t\t),\n\t\tserveScript: join(home, \"serve.py\"),\n\t\tlog: join(home, \"server.log\"),\n\t};\n}\n\nexport type LayaRuntimeStatus = \"ready\" | \"missing-environment\" | \"missing-model\";\n\n/** Files of the model that are absent or have the wrong size. A full hash check happens at download. */\nexport function missingModelFiles(manifest: LayaModelManifest, paths: LayaPaths): LayaModelFile[] {\n\treturn manifest.files.filter((file) => {\n\t\tconst path = join(paths.model, ...file.path.split(\"/\"));\n\t\ttry {\n\t\t\treturn statSync(path).size !== file.size;\n\t\t} catch {\n\t\t\treturn true;\n\t\t}\n\t});\n}\n\nexport function runtimeStatus(manifest: LayaModelManifest, paths: LayaPaths): LayaRuntimeStatus {\n\tif (!existsSync(paths.venvPython) || !existsSync(paths.serveScript)) return \"missing-environment\";\n\treturn missingModelFiles(manifest, paths).length > 0 ? \"missing-model\" : \"ready\";\n}\n\nexport interface DownloadOptions {\n\tfetch?: typeof fetch;\n\tsignal?: AbortSignal;\n\tonProgress?: (file: LayaModelFile, received: number) => void;\n}\n\n/**\n * Downloads the files that are missing or wrong, verifying each against its manifest hash before it\n * becomes visible: a download is written to a `.part` file and renamed only when the hash matches.\n */\nexport async function downloadModel(\n\tmanifest: LayaModelManifest,\n\tpaths: LayaPaths,\n\toptions: DownloadOptions = {},\n): Promise<void> {\n\tconst request = options.fetch ?? globalThis.fetch;\n\tfor (const file of missingModelFiles(manifest, paths)) {\n\t\tconst target = join(paths.model, ...file.path.split(\"/\"));\n\t\tconst partial = `${target}.part`;\n\t\tmkdirSync(dirname(target), { recursive: true });\n\t\tconst response = await request(new URL(file.asset, manifest.baseUrl), { signal: options.signal });\n\t\tif (!response.ok || !response.body) {\n\t\t\tthrow new Error(`Downloading ${file.asset} failed: HTTP ${response.status}`);\n\t\t}\n\t\tconst hash = createHash(\"sha256\");\n\t\tlet received = 0;\n\t\tconst meter = new Transform({\n\t\t\ttransform(chunk: Buffer, _encoding, callback) {\n\t\t\t\thash.update(chunk);\n\t\t\t\treceived += chunk.length;\n\t\t\t\toptions.onProgress?.(file, received);\n\t\t\t\tcallback(null, chunk);\n\t\t\t},\n\t\t});\n\t\ttry {\n\t\t\tawait pipeline(\n\t\t\t\tReadable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]),\n\t\t\t\tmeter,\n\t\t\t\tcreateWriteStream(partial),\n\t\t\t\t{ signal: options.signal },\n\t\t\t);\n\t\t\tconst digest = hash.digest(\"hex\");\n\t\t\tif (received !== file.size || digest !== file.sha256) {\n\t\t\t\tthrow new Error(`${file.asset} does not match the manifest (size ${received}, sha256 ${digest})`);\n\t\t\t}\n\t\t\trenameSync(partial, target);\n\t\t} catch (error) {\n\t\t\trmSync(partial, { force: true });\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nconst PYTHON_CANDIDATES: Array<[string, string[]]> = [\n\t[\"python3\", []],\n\t[\"python\", []],\n\t[\"py\", [\"-3\"]],\n];\n\n/** A Python 3.10 to 3.13 interpreter (the range torch publishes wheels for), as command plus leading args. */\nexport async function findPython(exec: ExecFunction, override?: string): Promise<[string, string[]] | undefined> {\n\tconst candidates: Array<[string, string[]]> = override ? [[override, []]] : PYTHON_CANDIDATES;\n\tfor (const [command, leading] of candidates) {\n\t\ttry {\n\t\t\tconst result = await exec(\n\t\t\t\tcommand,\n\t\t\t\t[...leading, \"-c\", \"import sys; print(sys.version_info[0], sys.version_info[1])\"],\n\t\t\t\t{ timeout: 15_000 },\n\t\t\t);\n\t\t\tconst [major, minor] = result.stdout.trim().split(\" \").map(Number);\n\t\t\tif (result.code === 0 && major === 3 && minor >= 10 && minor <= 13) return [command, leading];\n\t\t} catch {\n\t\t\t// Not installed.\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface SetupOptions {\n\texec: ExecFunction;\n\thome: string;\n\tmanifest: LayaModelManifest;\n\tpython?: string;\n\tfetch?: typeof fetch;\n\tsignal?: AbortSignal;\n\treport: (message: string) => void;\n}\n\nconst PIP_TIMEOUT_MS = 30 * 60_000;\n\nasync function run(\n\texec: ExecFunction,\n\tcommand: string,\n\targs: string[],\n\toptions: { signal?: AbortSignal; timeout?: number },\n\tfailure: string,\n): Promise<void> {\n\tconst result = await exec(command, args, options);\n\tif (result.code !== 0) {\n\t\tthrow new Error(`${failure}: ${(result.stderr || result.stdout).trim().split(\"\\n\").slice(-4).join(\" \")}`);\n\t}\n}\n\n/** Torch wheel index: a CUDA build only where an NVIDIA GPU exists, the small CPU build otherwise. */\nasync function torchArgs(exec: ExecFunction): Promise<string[]> {\n\tif (process.platform === \"darwin\") return [\"torch\"];\n\tlet gpu = false;\n\ttry {\n\t\tgpu = (await exec(\"nvidia-smi\", [\"-L\"], { timeout: 10_000 })).code === 0;\n\t} catch {\n\t\t// No NVIDIA driver.\n\t}\n\treturn [\"torch\", \"--index-url\", `https://download.pytorch.org/whl/${gpu ? \"cu128\" : \"cpu\"}`];\n}\n\n/** Creates the environment, installs torch and Laya, writes the server script and downloads the model. */\nexport async function setupLayaRuntime(options: SetupOptions): Promise<LayaPaths> {\n\tconst { exec, report, signal } = options;\n\tconst paths = layaPaths(options.home, options.manifest);\n\tmkdirSync(paths.home, { recursive: true });\n\n\tconst importable = async () =>\n\t\texistsSync(paths.venvPython) &&\n\t\t(await exec(paths.venvPython, [\"-c\", \"import laya, torch, fastapi, uvicorn\"], { timeout: 120_000 })).code === 0;\n\n\tif (!(await importable())) {\n\t\tconst python = await findPython(exec, options.python);\n\t\tif (!python) {\n\t\t\tthrow new Error(\"Laya needs Python 3.10 to 3.13. Install it, or set laya.python to its path.\");\n\t\t}\n\t\treport(\"Creating the Python environment\");\n\t\tawait run(\n\t\t\texec,\n\t\t\tpython[0],\n\t\t\t[...python[1], \"-m\", \"venv\", paths.venv],\n\t\t\t{ signal, timeout: 120_000 },\n\t\t\t\"Creating the environment failed\",\n\t\t);\n\t\tconst pip = [paths.venvPython, \"-m\", \"pip\", \"install\", \"-q\", \"--disable-pip-version-check\"] as const;\n\t\treport(\"Installing torch (large download)\");\n\t\tawait run(\n\t\t\texec,\n\t\t\tpip[0],\n\t\t\t[...pip.slice(1), ...(await torchArgs(exec))],\n\t\t\t{ signal, timeout: PIP_TIMEOUT_MS },\n\t\t\t\"Installing torch failed\",\n\t\t);\n\t\treport(\"Installing Laya\");\n\t\tawait run(\n\t\t\texec,\n\t\t\tpip[0],\n\t\t\t[...pip.slice(1), options.manifest.layaPackage, \"fastapi\", \"uvicorn\"],\n\t\t\t{ signal, timeout: PIP_TIMEOUT_MS },\n\t\t\t\"Installing Laya failed\",\n\t\t);\n\t}\n\twriteFileSync(paths.serveScript, SERVE_SCRIPT, \"utf8\");\n\n\tlet lastReported = 0;\n\tawait downloadModel(options.manifest, paths, {\n\t\tfetch: options.fetch,\n\t\tsignal,\n\t\tonProgress: (file, received) => {\n\t\t\tif (file.size < 10_000_000 || received - lastReported < 25_000_000) return;\n\t\t\tlastReported = received;\n\t\t\treport(`Downloading the Laya model: ${file.asset} ${Math.round((received / file.size) * 100)}%`);\n\t\t},\n\t});\n\treturn paths;\n}\n\n/** Serves the model with the System One protocol Relay's `typesafe-system-one` classifier speaks. */\nexport const SERVE_SCRIPT = `import argparse\n\nimport laya\nimport torch\nimport uvicorn\nfrom fastapi import FastAPI, HTTPException\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--model\", required=True)\nparser.add_argument(\"--host\", default=\"127.0.0.1\")\nparser.add_argument(\"--port\", type=int, default=8000)\nargs = parser.parse_args()\n\nagent = laya.load(args.model, device=\"cuda\" if torch.cuda.is_available() else \"cpu\")\napp = FastAPI(title=\"relay-laya\")\n\n\n@app.get(\"/health\")\ndef health():\n return {\"ok\": True, \"model\": args.model}\n\n\n@app.post(\"/v1/systemone\")\ndef system_one(body: dict):\n if \"state\" not in body or not body.get(\"questions\"):\n raise HTTPException(422, \"send 'state' and 'questions'\")\n return agent.predict(body[\"state\"], body[\"questions\"])\n\n\nuvicorn.run(app, host=args.host, port=args.port, log_level=\"warning\")\n`;\n"]}
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/extensions/laya/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { TRAIN_SCRIPT } from \"./train-script.ts\";\n\n/**\n * Laya runs in Docker, never on the host: an image carries Python, torch, the `laya` package, the\n * shipped model and the scripts that serve and train it. Relay pulls the image, keeps one server\n * container running and starts a short-lived container for each training. The host only keeps\n * Relay's own data (training exercises, the model registry, telemetry).\n */\n\nexport interface LayaModelFile {\n\t/** Path inside the model directory, with forward slashes. */\n\tpath: string;\n\t/** File name in the model repository. */\n\tasset: string;\n\tsize: number;\n\tsha256: string;\n}\n\nexport interface LayaModelManifest {\n\tversion: string;\n\t/** pip requirement for the Laya runtime. */\n\tlayaPackage: string;\n\t/** Directory URL the model files are downloaded from when the image is built, ending in a slash. */\n\tbaseUrl: string;\n\tfiles: LayaModelFile[];\n}\n\n/** Registry the images are published to. */\nexport const LAYA_IMAGE_REPOSITORY = \"ghcr.io/eltonssouza/relay-laya\";\n/** The server container, kept running between Relay sessions. */\nexport const LAYA_CONTAINER = \"relay-laya\";\n/** The container of a running training; removed when it ends. */\nexport const LAYA_TRAIN_CONTAINER = \"relay-laya-train\";\n/** Volume with the models trained from sessions. */\nexport const LAYA_MODELS_VOLUME = \"relay-laya-models\";\n/** Port the server listens on inside its container. */\nexport const LAYA_CONTAINER_PORT = 8000;\n\n/** Paths inside the image and its containers. */\nexport const LAYA_IMAGE_PATHS = {\n\tshippedModel: \"/opt/laya/model\",\n\ttrainedModels: \"/data/models\",\n\tserveScript: \"/opt/laya/serve.py\",\n\ttrainScript: \"/opt/laya/train.py\",\n\tworkspace: \"/workspace\",\n} as const;\n\n/** `cpu` runs everywhere; `cuda` carries the CUDA build of torch for machines with an NVIDIA GPU. */\nexport type LayaImageVariant = \"cpu\" | \"cuda\";\n\nconst TORCH_INDEX: Record<LayaImageVariant, string> = {\n\tcpu: \"https://download.pytorch.org/whl/cpu\",\n\tcuda: \"https://download.pytorch.org/whl/cu128\",\n};\n\n/** Serves the model with the System One protocol Relay's `typesafe-system-one` classifier speaks. */\nexport const SERVE_SCRIPT = `import argparse\n\nimport laya\nimport torch\nimport uvicorn\nfrom fastapi import FastAPI, HTTPException\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--model\", required=True)\nparser.add_argument(\"--host\", default=\"127.0.0.1\")\nparser.add_argument(\"--port\", type=int, default=8000)\nargs = parser.parse_args()\n\nagent = laya.load(args.model, device=\"cuda\" if torch.cuda.is_available() else \"cpu\")\napp = FastAPI(title=\"relay-laya\")\n\n\n@app.get(\"/health\")\ndef health():\n return {\"ok\": True, \"model\": args.model}\n\n\n@app.post(\"/v1/systemone\")\ndef system_one(body: dict):\n if \"state\" not in body or not body.get(\"questions\"):\n raise HTTPException(422, \"send 'state' and 'questions'\")\n return agent.predict(body[\"state\"], body[\"questions\"])\n\n\nuvicorn.run(app, host=args.host, port=args.port, log_level=\"warning\")\n`;\n\n/** Downloads the shipped model while the image is built, checking every file against the manifest. */\nexport const FETCH_MODEL_SCRIPT = `import hashlib\nimport json\nimport pathlib\nimport sys\nimport urllib.request\n\nmanifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding=\"utf-8\"))\ntarget = pathlib.Path(sys.argv[2])\nfor file in manifest[\"files\"]:\n path = target / file[\"path\"]\n path.parent.mkdir(parents=True, exist_ok=True)\n digest = hashlib.sha256()\n size = 0\n with urllib.request.urlopen(manifest[\"baseUrl\"] + file[\"asset\"]) as response, open(path, \"wb\") as out:\n while chunk := response.read(1 << 20):\n digest.update(chunk)\n size += len(chunk)\n out.write(chunk)\n if size != file[\"size\"] or digest.hexdigest() != file[\"sha256\"]:\n sys.exit(f\"{file['asset']} does not match the manifest (size {size}, sha256 {digest.hexdigest()})\")\n print(f\"{file['path']} ok\", flush=True)\n`;\n\nfunction dockerfile(manifest: LayaModelManifest, variant: LayaImageVariant): string {\n\tconst paths = LAYA_IMAGE_PATHS;\n\treturn `FROM python:3.11-slim\nLABEL org.opencontainers.image.source=\"https://github.com/eltonssouza/relay-harness\"\nLABEL org.opencontainers.image.description=\"Laya routing model for Relay (${variant})\"\nENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PYTHONIOENCODING=utf-8 PIP_NO_CACHE_DIR=1 \\\\\n PIP_DISABLE_PIP_VERSION_CHECK=1 USE_TF=0 HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 TOKENIZERS_PARALLELISM=false\nRUN pip install --upgrade pip && pip install torch --index-url ${TORCH_INDEX[variant]}\nRUN pip install \"${manifest.layaPackage}\" fastapi uvicorn\nRUN useradd --create-home laya && mkdir -p ${paths.trainedModels} && chown laya ${paths.trainedModels}\nCOPY fetch_model.py model.json /opt/laya/\nRUN python /opt/laya/fetch_model.py /opt/laya/model.json ${paths.shippedModel}\nCOPY serve.py train.py /opt/laya/\nUSER laya\nEXPOSE ${LAYA_CONTAINER_PORT}\nCMD [\"python\", \"${paths.serveScript}\", \"--model\", \"${paths.shippedModel}\", \"--host\", \"0.0.0.0\", \"--port\", \"${LAYA_CONTAINER_PORT}\"]\n`;\n}\n\n/** Files of the image's build context, by name. */\nexport function layaImageContext(manifest: LayaModelManifest, variant: LayaImageVariant): Record<string, string> {\n\treturn {\n\t\tDockerfile: dockerfile(manifest, variant),\n\t\t\"fetch_model.py\": FETCH_MODEL_SCRIPT,\n\t\t\"model.json\": `${JSON.stringify({ baseUrl: manifest.baseUrl, files: manifest.files }, null, 2)}\\n`,\n\t\t\"serve.py\": SERVE_SCRIPT,\n\t\t\"train.py\": TRAIN_SCRIPT,\n\t};\n}\n\n/**\n * Image tag: the model version, the variant and a hash of the build context. Changing a script, a\n * dependency or the model gives a new tag, so a Relay version always runs the image built from its\n * own sources, and an unchanged context reuses the published image.\n */\nexport function layaImageTag(manifest: LayaModelManifest, variant: LayaImageVariant): string {\n\tconst hash = createHash(\"sha256\");\n\tfor (const [name, content] of Object.entries(layaImageContext(manifest, variant)).sort(([a], [b]) =>\n\t\ta.localeCompare(b),\n\t)) {\n\t\thash.update(`${name}\\0${content}\\0`);\n\t}\n\treturn `${manifest.version}-${variant}-${hash.digest(\"hex\").slice(0, 12)}`;\n}\n\nexport function layaImage(manifest: LayaModelManifest, variant: LayaImageVariant): string {\n\treturn `${LAYA_IMAGE_REPOSITORY}:${layaImageTag(manifest, variant)}`;\n}\n\n/** Container path of a model trained from sessions. */\nexport function trainedModelPath(name: string): string {\n\treturn `${LAYA_IMAGE_PATHS.trainedModels}/${name}`;\n}\n"]}
|
|
@@ -1,26 +1,50 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type DockerRun } from "./docker.ts";
|
|
2
|
+
export type LayaServerState =
|
|
3
|
+
/** Not checked yet in this process. */
|
|
4
|
+
"unknown"
|
|
5
|
+
/** The docker command is missing or its daemon does not answer. */
|
|
6
|
+
| "no-docker" | "installing" | "ready" | "failed";
|
|
2
7
|
export interface LayaServerOptions {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/** The configured System One URL, e.g. `http://127.0.0.1:8000/v1`. */
|
|
8
|
+
docker: DockerRun;
|
|
9
|
+
/** The configured System One URL, e.g. `http://127.0.0.1:8737/v1`. */
|
|
6
10
|
baseUrl: () => string;
|
|
11
|
+
/** Image to run. */
|
|
12
|
+
image: () => Promise<string>;
|
|
13
|
+
/** Model to serve, as a path inside the container. */
|
|
14
|
+
model: () => string;
|
|
7
15
|
fetch?: typeof fetch;
|
|
8
16
|
}
|
|
9
|
-
/** Whether a URL points at this machine, the only place Relay
|
|
17
|
+
/** Whether a URL points at this machine, the only place Relay runs a server. */
|
|
10
18
|
export declare function isLocalUrl(url: string): boolean;
|
|
11
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The Laya server container. `install()` pulls the image when it is missing and keeps one container
|
|
21
|
+
* running with the active model, published on the configured local port only. The container
|
|
22
|
+
* restarts with Docker, so later sessions find it answering. A server that answers without being
|
|
23
|
+
* ours (no container named relay-laya) is used as is, unless it serves other questions than Relay's.
|
|
24
|
+
*/
|
|
12
25
|
export declare class LayaServer {
|
|
13
26
|
private readonly options;
|
|
14
|
-
private
|
|
15
|
-
|
|
27
|
+
private installing;
|
|
28
|
+
state: LayaServerState;
|
|
29
|
+
/** Why the server is not ready, for `/laya status`. */
|
|
30
|
+
detail: string | undefined;
|
|
31
|
+
/** Image of the running container. */
|
|
32
|
+
image: string | undefined;
|
|
16
33
|
constructor(options: LayaServerOptions);
|
|
17
|
-
|
|
34
|
+
/** The server's health answer, when it answers. */
|
|
35
|
+
private health;
|
|
18
36
|
isUp(): Promise<boolean>;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
/** Makes the server answer: pulls, creates, replaces or starts the container. Concurrent calls share one run. */
|
|
38
|
+
install(report?: (message: string) => void, signal?: AbortSignal): Promise<boolean>;
|
|
39
|
+
private fail;
|
|
40
|
+
private run;
|
|
41
|
+
private ready;
|
|
42
|
+
private runArgs;
|
|
43
|
+
private inspect;
|
|
44
|
+
/** Frees the disk taken by images of earlier Relay versions. */
|
|
45
|
+
private removeOtherImages;
|
|
46
|
+
/** Stops the server container; the next install starts it again. */
|
|
47
|
+
stop(): Promise<void>;
|
|
48
|
+
describe(): string;
|
|
25
49
|
}
|
|
26
50
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/extensions/laya/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/extensions/laya/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,SAAS,EAAe,MAAM,aAAa,CAAC;AAsB1E,MAAM,MAAM,eAAe;AAC1B,uCAAuC;AACrC,SAAS;AACX,mEAAmE;GACjE,WAAW,GACX,YAAY,GACZ,OAAO,GACP,QAAQ,CAAC;AAEZ,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,oBAAoB;IACpB,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,sDAAsD;IACtD,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACrB;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAM/C;AASD;;;;;GAKG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,UAAU,CAA+B;IACjD,KAAK,EAAE,eAAe,CAAa;IACnC,uDAAuD;IACvD,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,sCAAsC;IACtC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAE1B,YAAY,OAAO,EAAE,iBAAiB,EAErC;IAED,mDAAmD;YACrC,MAAM;IAcd,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAE7B;IAED,iHAAiH;IACjH,OAAO,CAAC,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAK5F;IAED,OAAO,CAAC,IAAI;YAME,GAAG;IA8GjB,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,OAAO;YAyBD,OAAO;IAmBrB,gEAAgE;YAClD,iBAAiB;IAc/B,oEAAoE;IAC9D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1B;IAED,QAAQ,IAAI,MAAM,CAajB;CACD"}
|