{
  "format": "relay-web-workflow",
  "version": 1,
  "title": "One-Line Pavilion · Agent Growth · Bundled Canopy",
  "request": "Try a more structured One-Line Pavilion inspired by Kokkugia’s agent studies. Grow four colonies towards a cloud of points above the site, join neighbouring colonies, then thread one continuous line through the branching structure and all four feet.",
  "workflow": {
    "schema_version": 1,
    "kind": "workflow",
    "inputs": {},
    "tasks": [
      {
        "schema_version": 3,
        "id": "pavilion",
        "operation": "code.javascript",
        "objective": "Grow four colonies toward a shared cloud, let nearby branches add cross-links, and sweep a single tube along the resulting trail.",
        "inputs": {
          "code": {
            "value": "function run(inputs, controls) {\n  const trace = inputs?.capture ? [] : null;\n  const path = [];\n  const feet = [[-3,-3,0],[3,-3,0],[3,3,0],[-3,3,0]];\n  const clamp = (n, low, high) => Math.max(low, Math.min(high, n));\n  const mix = (a, b, t) => a + (b - a) * t;\n  const add = (a, b) => a.map((v, i) => v + b[i]);\n  const scale = (a, n) => a.map(v => v * n);\n  const sub = (a, b) => add(a, scale(b, -1));\n  const dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);\n  const length = v => Math.hypot(...v);\n  const unit = a => scale(a, 1 / Math.max(1e-8,length(a)));\n  const cross = ([a,b,c], [x,y,z]) => [b*z-c*y, c*x-a*z, a*y-b*x];\n  const lerp = (a,b,t) => add(a,scale(sub(b,a),t));\n  const smooth = t => { const p=clamp(t,0,1); return p*p*(3-2*p); };\n  const clip = ([x,y,z])=>[clamp(x,-3,3),clamp(y,-3,3),clamp(z,.14,4.8)];\n\n  // Keep the site and the four contacts fixed. Each rule supplies its own route.\n  function append(route) {\n    path.push(...route.slice(path.length ? 1 : 0));\n  }\n\n  function round(route, amount, steps) {\n    const points=[route[0]];\n    for(let i=1;i<route.length-1;i++){\n      const p=route[i], a=lerp(p,route[i-1],amount), b=lerp(p,route[i+1],amount);\n      for(let j=0;j<steps;j++){\n        const t=j/(steps-1);\n        points.push(lerp(lerp(a,p,t),lerp(p,b,t),t));\n      }\n    }\n    points.push(route.at(-1));\n    return points;\n  }\n\n  function randomFrom(seed) {\n    let state = seed >>> 0;\n    return () => {\n      state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n      return state / 4294967296;\n    };\n  }\n\n  function checkedSeed(seed) {\n    if (!Number.isInteger(seed) || seed < 1 || seed > 9999) throw new Error('Choose a seed from 1 to 9999.');\n    return randomFrom(seed);\n  }\n\n  const { seed, stitches } = controls;\n  if(!Number.isInteger(stitches) || stitches<0 || stitches>24) throw Error('Choose 0 to 24 stitches.');\n  const random = checkedSeed(seed);\n  const limits = p => [clamp(p[0],-3,3),clamp(p[1],-3,3),clamp(p[2],.12,4.8)];\n  const step = .41;\n  // A rising bias starts four colonies. Samples pull their active tips\n  // sideways into a shared volume. Points in the central clearing are excluded.\n  const sites = Array.from({length:76},()=>[\n    5.55*(random()-.5),5.55*(random()-.5),2.5+2.12*random()\n  ]);\n  const nodes=feet.map(p=>({p,parent:-1,root:true,children:0}));\n  const edges=[], frames=[];\n  const nearby=(a,b)=>length(sub(a,b));\n  function addEdge(a,b){\n    edges.push([a,b]);\n  }\n  let targets=[...sites];\n  for(let tick=0;tick<26 && targets.length;tick++){\n    const groups=nodes.map(()=>[]);\n    targets=targets.filter(p=>{\n      let closest=-1, best=Infinity;\n      for(let i=0;i<nodes.length;i++){\n        const d=nearby(p,nodes[i].p);\n        if(d<.53) return false;\n        if(nodes[i].children<(nodes[i].root?1:2) && d<best){\n          closest=i;\n          best=d;\n        }\n      }\n      if(closest>=0) groups[closest].push(p);\n      return true;\n    });\n    for(let i=0;i<groups.length;i++){\n      const samples=groups[i];\n      if(!samples.length || nodes.length>=152) continue;\n      const from=nodes[i].p;\n      const incident=nodes[i].parent<0?[0,0,1]:unit(sub(from,nodes[nodes[i].parent].p));\n      const directions=samples.map(p=>unit(sub(p,from)));\n      const attraction=unit(directions.reduce(add,[0,0,0]));\n      const flow=unit(add(scale(attraction,1.15),scale(incident,.44)));\n      const spread=nodes[i].children===0?[0,0,0]:scale(cross(flow,[0,0,1]),.31);\n      let direction=unit(add(flow,spread));\n      const p=limits(add(from,scale(direction,step)));\n      if(p[2]<2.25 && Math.abs(p[0])<1.44 && Math.abs(p[1])<1.44){\n        direction=unit(add(direction,[0,0,1.3]));\n      }\n      const next=limits(add(from,scale(direction,step)));\n      if(nodes.some(n=>nearby(n.p,next)<.22)) continue;\n      nodes[i].children++;\n      addEdge(i,nodes.length);\n      nodes.push({p:next,parent:i,children:0});\n    }\n    if(trace) frames.push(edges.map(([a,b])=>[nodes[a].p,nodes[b].p]));\n  }\n\n  // Neighbouring colonies can link overhead. Three cross-links join the\n  // four roots while keeping a tree that can be threaded without forks.\n  const labels=nodes.map((n,i)=>n.root?i:0);\n  for(let i=feet.length;i<nodes.length;i++) labels[i]=labels[nodes[i].parent];\n  const links=[];\n  for(let i=feet.length;i<nodes.length;i++) for(let j=i+1;j<nodes.length;j++){\n    const a=nodes[i].p,b=nodes[j].p;\n    if(labels[i]!==labels[j] && Math.min(a[2],b[2])>2.34)\n      links.push([nearby(a,b),i,j]);\n  }\n  links.sort((a,b)=>a[0]-b[0]);\n  const joins=[];\n  for(const [gap,i,j] of links){\n    if(labels[i]===labels[j]) continue;\n    const old=labels[j], next=labels[i];\n    labels.forEach((label,k)=>{if(label===old) labels[k]=next;});\n    addEdge(i,j);\n    joins.push([nodes[i].p,nodes[j].p]);\n    if(joins.length===3) break;\n  }\n  if(joins.length!==3) throw Error('The four colonies did not meet above the clearing.');\n\n  // Drop the short terminal shoots. The longer branches and the connections\n  // are what the single tube follows. No node is removed from a through route.\n  const network=nodes.map(()=>[]);\n  for(const [a,b] of edges){network[a].push(b);network[b].push(a);}\n  // Trim just the shortest shoots. Keep room for new connections overhead.\n  for(const [i,neighbours] of network.entries()){\n    if(i<4 || neighbours.length!==1) continue;\n    const j=neighbours[0];\n    if(network[j].length<3) continue;\n    network[j]=network[j].filter(k=>k!==i);\n    network[i]=[];\n  }\n  function distances(from){\n    const route=nodes.map(()=>Infinity), prev=nodes.map(()=>-1);\n    const open=new Set([from]);\n    route[from]=0;\n    while(open.size){\n      const i=[...open].reduce((a,b)=>route[a]<route[b]?a:b);\n      open.delete(i);\n      for(const j of network[i]){\n        const cost=route[i]+nearby(nodes[i].p,nodes[j].p);\n        if(cost<route[j]){\n          route[j]=cost;\n          prev[j]=i;\n          open.add(j);\n        }\n      }\n    }\n    return {route,prev};\n  }\n  // A local chord can brace a long detour through the canopy. Cap how\n  // many strands meet at any one junction, so the canopy stays porous.\n  const possible=[];\n  for(let i=4;i<nodes.length;i++){\n    const a=nodes[i].p;\n    if(a[2]<2.65 || network[i].length===0) continue;\n    const {route}=distances(i);\n    for(let j=i+1;j<nodes.length;j++){\n      const b=nodes[j].p, gap=nearby(a,b);\n      if(b[2]<2.65 || network[j].length===0 || gap<1.20 || gap>2.36 || route[j]<gap*2.1) continue;\n      const brace=unit(sub(b,a));\n      const tangent=unit(sub(a,nodes[nodes[i].parent].p));\n      const transverse=1-Math.abs(dot(brace,tangent));\n      possible.push({i,j,score:route[j]/gap+.8*transverse});\n    }\n  }\n  possible.sort((a,b)=>b.score-a.score);\n  const stitch=[];\n  const usedNodes=new Set();\n  for(const {i,j} of possible){\n    if(stitch.length===stitches) break;\n    if(network[i].length>=4 || network[j].length>=4 || usedNodes.has(i) || usedNodes.has(j)) continue;\n    const {route}=distances(i);\n    if(route[j]<nearby(nodes[i].p,nodes[j].p)*1.76) continue;\n    network[i].push(j);\n    network[j].push(i);\n    usedNodes.add(i);\n    usedNodes.add(j);\n    stitch.push([nodes[i].p,nodes[j].p]);\n  }\n\n  // Pair odd junctions by duplicating short graph paths. That leaves only\n  // the first and last feet odd, so all ribs fit in one open Euler trail.\n  const threads=[];\n  function thread(a,b){threads.push({a,b});}\n  network.forEach((near,i)=>near.forEach(j=>{if(i<j) thread(i,j);}));\n  const unmatched=network.map((near,i)=>((near.length%2) !== Number(i===0 || i===3))?i:-1).filter(i=>i>=0);\n  while(unmatched.length){\n    const a=unmatched.shift();\n    const {route,prev}=distances(a);\n    const index=unmatched.reduce((best,j,i)=>route[j]<route[unmatched[best]]?i:best,0);\n    const b=unmatched.splice(index,1)[0];\n    for(let j=b;j!==a;j=prev[j]) thread(j,prev[j]);\n  }\n  const incident=nodes.map(()=>[]);\n  const copies=new Map();\n  const key=(a,b)=>Math.min(a,b)+':'+Math.max(a,b);\n  threads.forEach((edge,i)=>{\n    const {a,b}=edge,k=key(a,b);\n    if(!copies.has(k)) copies.set(k,[]);\n    copies.get(k).push(i);\n    incident[a].push(i);\n    incident[b].push(i);\n  });\n  const used=new Set(), hops=[];\n  function visit(a,from){\n    while(true){\n      const options=incident[a].filter(i=>!used.has(i)).map(i=>{\n        const e=threads[i], b=e.a===a?e.b:e.a;\n        const turn=from<0?0:dot(unit(sub(nodes[a].p,nodes[from].p)),unit(sub(nodes[b].p,nodes[a].p)));\n        return {i,b,turn};\n      });\n      if(!options.length) return;\n      options.sort((a,b)=>b.turn-a.turn);\n      const {i,b}=options[0];\n      used.add(i);\n      visit(b,a);\n      hops.push([a,b,i]);\n    }\n  }\n  visit(0,-1);\n  hops.reverse();\n  if(hops.length!==threads.length || hops.at(-1)[1]!==3) throw Error('The canopy did not produce one open trail.');\n\n  // Parallel copies occupy separate tracks around the same growth edge.\n  const walked=[feet[0]];\n  for(const [a,b,i] of hops){\n    const p=nodes[a].p,q=nodes[b].p;\n    const side=unit(cross(sub(nodes[Math.max(a,b)].p,nodes[Math.min(a,b)].p),[.33,-.21,1]));\n    const lanes=copies.get(key(a,b));\n    const offset=scale(side,(lanes.indexOf(i)-(lanes.length-1)/2)*.235);\n    walked.push(limits(add(lerp(p,q,.24),offset)),limits(add(lerp(p,q,.76),offset)));\n    if(b<4) walked.push(q);\n  }\n  let span=[walked[0]];\n  for(const p of walked.slice(1)){\n    span.push(p);\n    if(p[2]===0){\n      append(round(span,.32,2));\n      span=[p];\n    }\n  }\n  if(trace) trace.push({sites,frames,joins,stitch,walked});\n\n  // Give overlapping passes room for the tube, leaving the four contacts fixed.\n  // The simulation decides the route. This step only moves near neighbours.\n  const gap = 0.185;\n  const fixed = path.map(p => p[2] === 0);\n  const stations = [0];\n  for (let i=1; i<path.length; i++) {\n    stations.push(stations.at(-1)+Math.hypot(...sub(path[i],path[i-1])));\n  }\n\n  function separate(i,j) {\n    const a=path[i], b=path[i+1], c=path[j], d=path[j+1];\n    for(let k=0;k<3;k++){\n      if(Math.min(a[k],b[k])-Math.max(c[k],d[k])>gap || Math.min(c[k],d[k])-Math.max(a[k],b[k])>gap) return false;\n    }\n    const u=sub(b,a), v=sub(d,c), r=sub(a,c);\n    const uu=dot(u,u), vv=dot(v,v), uv=dot(u,v);\n    const ru=dot(r,u), rv=dot(r,v), den=uu*vv-uv*uv;\n    let s=den>1e-10 ? clamp((uv*rv-vv*ru)/den,0,1) : 0;\n    let t=(uv*s+rv)/vv;\n    if(t<0 || t>1){\n      t=clamp(t,0,1);\n      s=clamp((uv*t-ru)/uu,0,1);\n    }\n    const delta=sub(add(r,scale(u,s)),scale(v,t));\n    const distance=Math.hypot(...delta);\n    if(distance>=gap) return false;\n    const direction=distance>1e-7 ? scale(delta,1/distance) : [1,0,0];\n    const weights=[1-s,s,t-1,-t].map((weight,k)=>fixed[[i,i+1,j,j+1][k]] ? 0 : weight);\n    const total=dot(weights,weights);\n    if(total<1e-8) return false;\n    const shift=scale(direction,0.72*(gap-distance)/total);\n    [i,i+1,j,j+1].forEach((k,n)=>{\n      if(fixed[k]) return;\n      const [x,y,z]=add(path[k],scale(shift,weights[n]));\n      path[k]=clip([x,y,z]);\n    });\n    return true;\n  }\n\n  // Sort segment bounds once per pass so only nearby pairs need the\n  // distance calculation. A small margin allows for motion during the pass.\n  const search = gap + .10;\n  for(let iteration=0;iteration<32;iteration++){\n    const bounds = path.slice(1).map((b,i)=>{\n      const a=path[i];\n      return {i, min:a.map((x,k)=>Math.min(x,b[k])),max:a.map((x,k)=>Math.max(x,b[k]))};\n    }).sort((a,b)=>a.min[0]-b.min[0]);\n    let moved=false;\n    for(let i=0;i<bounds.length;i++){\n      const a=bounds[i];\n      for(let j=i+1;j<bounds.length && bounds[j].min[0]<=a.max[0]+search;j++){\n        const b=bounds[j];\n        if(a.min[1]>b.max[1]+search || b.min[1]>a.max[1]+search ||\n           a.min[2]>b.max[2]+search || b.min[2]>a.max[2]+search) continue;\n        const first=Math.min(a.i,b.i), second=Math.max(a.i,b.i);\n        if(stations[second]-stations[first+1]>.30 && separate(first,second)) moved=true;\n      }\n    }\n    if(!moved) break;\n  }\n\n  const curve = path;\n  // Sweep one circular section in a parallel-transport frame.\n  const radius = 0.050;\n  const sides = 10;\n  const vertices = [];\n  const faces = [Array.from({length:sides},(_,j)=>sides-1-j)];\n  let normal=[0,1,0];\n  for(let i=0;i<curve.length;i++){\n    const p=curve[i];\n    const tangent=unit(sub(curve[Math.min(i+1,curve.length-1)],curve[Math.max(i-1,0)]));\n    normal=unit(sub(normal,scale(tangent,dot(normal,tangent))));\n    const binormal=cross(tangent,normal);\n    for(let j=0;j<sides;j++){\n      const angle=2*Math.PI*j/sides;\n      const q=add(p,add(scale(normal,radius*Math.cos(angle)),scale(binormal,radius*Math.sin(angle))));\n      vertices.push(q.map(v=>Number(v.toFixed(5))));\n      if(i){\n        const a=(i-1)*sides+j, b=(i-1)*sides+(j+1)%sides;\n        faces.push([a,b,b+sides,a+sides]);\n      }\n    }\n  }\n  const last=vertices.length-sides;\n  faces.push(Array.from({length:sides},(_,j)=>last+j));\n  return {\n    version:2, units:'metres',\n    objects:[{type:'MESH',name:'One continuous tube',color:'#3954b5',vertices,faces}]\n  };\n}\n"
          },
          "controls": {
            "value": {
              "seed": 42,
              "stitches": 19
            }
          }
        }
      },
      {
        "schema_version": 3,
        "id": "viewer",
        "operation": "scene.view",
        "objective": "Inspect the pavilion in 3D and render a model reference.",
        "inputs": {
          "scene": {
            "task": "pavilion",
            "output": "result"
          },
          "azimuth": {
            "value": -55
          },
          "elevation": {
            "value": 25
          },
          "resolution": {
            "value": 640
          }
        }
      },
      {
        "schema_version": 3,
        "id": "visualization",
        "operation": "gemini.image",
        "objective": "Visualize the exact pavilion in its setting with Gemini.",
        "inputs": {
          "model": {
            "value": "gemini-3.1-flash-image"
          },
          "files": {
            "value": [
              {
                "task": "viewer",
                "output": "preview"
              }
            ]
          },
          "aspect_ratio": {
            "value": "16:9"
          },
          "prompt": {
            "value": "Create one architectural concept image based on the supplied 3D viewer reference. Match its camera and preserve the exact continuous blue tube, including every curve where it forms the enclosure, folds overhead, and reaches the ground. Keep the spaces between the folds visible. Show matte powder-coated steel on paving in a public garden in daylight. Keep the geometry fixed and change materials, light and surroundings. No text or extra architecture."
          }
        }
      }
    ]
  }
}
