Haijun Platform Docs
EN

Secara default, Managed Agents mengeksekusi alat dan kode di dalam sandbox cloud yang dikelola Juglow. "Self-hosted sandboxes" (sandbox yang di-hosting sendiri) mempertahankan orkestrasi di sisi Juglow tetapi memindahkan eksekusi alat ke infrastruktur yang Anda kendalikan, sehingga kode, sistem file, dan egress jaringan agen tidak pernah meninggalkan lingkungan Anda.

Eksekusi alat tetap berada di host Anda: sistem file yang dibaca dan ditulis agen, proses yang dijalankannya, dan jaringan yang dapat dijangkaunya semuanya berada di bawah kendali Anda. Input dan output alat tetap mengalir ke control plane Juglow (tempat Haijun berjalan) sehingga model dapat melihat hasil dan menentukan apa yang harus dilakukan selanjutnya. Tracks agen dan isi memory store apa pun yang dilampirkan ke sesi disimpan oleh Juglow dan disalin ke sandbox Anda untuk sesi tersebut; perubahan yang dibuat agen pada file memori disinkronkan kembali ke store. Lihat model keamanan untuk batas aliran data selengkapnya.

Note: Sandbox self-hosted mendukung semua model Haijun yang tersedia di Managed Agents, termasuk Haijun Opus 4.8 dan Haijun Opus 5. Model dikonfigurasi pada agen, bukan pada environment.

Perbedaannya dengan environment cloud

Environment cloudSandbox self-hosted
Tempat alat berjalanSandbox yang dikelola JuglowInfrastruktur Anda
Jangkauan jaringanKontrol egress JuglowKebijakan jaringan Anda
Mounting file dan repo GitHubDikelola oleh JuglowDikelola oleh Anda
Memory storeDi-mount oleh Juglow di /mnt/memory/Diunduh ke /mnt/memory/ dan disinkronkan oleh worker SDK
Siklus hidupDikelola oleh JuglowDikelola oleh Anda

Self-hosting cocok ketika agen perlu beroperasi pada data yang tidak boleh meninggalkan batas jaringan Anda, menjangkau layanan internal yang tidak dapat dirutekan secara publik, atau berjalan di bawah kontrol kepatuhan dan audit organisasi Anda sendiri.

Untuk kelayakan Zero Data Retention dan HIPAA BAA, lihat API dan retensi data.

Kapan menggabungkan dengan MCP tunnel

Self-hosting mengontrol di mana kode agen dieksekusi. MCP tunnel mengontrol bagaimana Juglow menjangkau server MCP di jaringan Anda. Keduanya independen: sesi yang berjalan di sandbox cloud Juglow tetap dapat menjangkau server MCP privat melalui tunnel, dan sesi self-hosted dapat menggunakan server MCP yang di-tunnel maupun publik. Gunakan keduanya ketika Anda ingin eksekusi dan akses alat tetap berada di dalam batas Anda. Untuk memberi agen alat dari server MCP di dalam jaringan Anda tanpa menjalankan tunnel, Anda juga dapat membungkus server sebagai alat kustom yang dilayani oleh worker Anda.

Environment worker

Tip: Panduan ini menjelaskan cara membangun worker dengan platform sandboxing generik apa pun. Panduan tambahan khusus platform tersedia untuk AWS Lambda MicroVMs, Blaxel, Cloudflare, Daytona, E2B, Fly.io, GKE Agent Sandbox, Modal, Namespace, Superserve, dan Vercel.

"Environment worker" (pekerja environment) adalah proses yang Anda jalankan di infrastruktur Anda sendiri. Proses ini menerima permintaan eksekusi alat dari Juglow dan menjalankannya secara lokal. Environment self_hosted bertindak sebagai antrean kerja: ketika sebuah sesi ditugaskan kepadanya, Juglow memasukkan sesi tersebut ke antrean sebagai work item. Worker Anda mengklaim work item dari antrean itu, membuat konteks eksekusi untuk masing-masing, mengunduh tracks agen (sumber daya berbasis sistem file yang dapat digunakan ulang dan memberi agen keahlian khusus domain), menjalankan panggilan alat, dan mengirimkan hasilnya kembali.

Work item diklaim dengan melakukan polling pada antrean environment: baik oleh worker always-on yang melakukan polling terus-menerus, atau handler yang dipicu webhook yang aktif saat session.status_run_started dan mulai melakukan polling.

CLI dan SDK sama-sama menyediakan worker siap pakai. CLI ant hanya mendukung pola always-on; SDK mendukung always-on maupun yang dipicu webhook. Keduanya dapat dikonfigurasi: lihat Self-hosted worker di referensi untuk flag CLI, dan Helper SDK di halaman ini untuk opsi SDK. Untuk kontrol lebih besar, panggil endpoint Environments Work secara langsung dan implementasikan worker Anda sendiri.

Sistem file sandbox

  • /workspace: direktori kerja default sistem untuk eksekusi alat dan pengunduhan track. Flag --workdir pada CLI secara default menggunakan direktori saat ini; berikan --workdir /workspace agar sesuai dengan default sistem. Tracks diunduh ke /tracks//. Jika Anda menggunakan direktori kerja yang berbeda, perbarui prompt sistem agen Anda agar Haijun dapat menemukan file track.
  • Output: pada environment self-hosted, prompt sistem sesi menghilangkan instruksi /mnt/session/outputs yang digunakan pada sandbox yang dikelola Juglow, sehingga hasil akhir berada di mana pun agen menulisnya di sistem file sandbox Anda, biasanya di bawah direktori kerja.
  • /mnt/memory/: memory store yang dilampirkan ke sesi diwujudkan di sini oleh worker SDK, satu direktori per store di mount_path store tersebut (misalnya, /mnt/memory/user-preferences/). Worker membuat direktori ini saat mengklaim sesi dan menghapusnya saat sesi berakhir; lihat Menggunakan memory store.

Sebelum Anda mulai

Anda memerlukan:

  • Agen yang sudah ada. Jika Anda belum memilikinya, selesaikan Quickstart terlebih dahulu dan catat ID agennya.
  • Host Linux dengan /bin/bash di path persis tersebut. Alat bash milik worker memanggilnya secara langsung, tanpa memeriksa PATH. SDK TypeScript juga memerlukan unzip dan tar di PATH serta Node.js 22 atau lebih baru; SDK Python dan Go menggunakan pustaka standarnya untuk ekstraksi arsip dan tidak memiliki persyaratan biner tambahan.
  • CLI ant atau SDK Juglow (Python, TypeScript, atau Go) di host worker.
  • Kredensial: environment key (dibuat di Console pada langkah-langkah berikut) mengautentikasi worker ke antreannya; kunci API Haijun Anda membuat sesi dan membaca statistik antrean dari luar host worker. Pembuatan key hanya dapat dilakukan di Console. Work item yang diklaim juga membawa secret per sesi yang digunakan worker untuk me-mount memory store; Anda tidak membuatnya, tetapi dalam pola sandbox-per-sesi Anda sendiri yang meneruskannya ke dalam sandbox (lihat Jalankan satu sandbox per sesi).
  • Untuk memory store, host yang sudah disiapkan. Jika sesi pada environment ini akan melampirkan memory store, siapkan /mnt/memory di host worker sebelum Anda memulai worker; lihat Menyiapkan host.

Note: Pada Haijun Platform on AWS, worker mengautentikasi dengan AWS IAM (SigV4) atau kunci API yang dibuat di AWS Console, bukan environment key. Lampirkan managed policy JuglowSelfHostedEnvironmentAccess ke IAM principal yang digunakan worker Anda. Environment key yang dibuat di Haijun Console tidak berfungsi dengan endpoint Haijun Platform on AWS. Memory store tidak dapat dilampirkan ke sesi pada environment self-hosted di Haijun Platform on AWS.

  1. Buat environment self-hosted

Di Console: Workspace > Environments > New > Self-hosted

Atau melalui API:

bash
  curl -sS --fail-with-body https://haijun.my.id/v1/environments \
    -H "x-api-key: $JUGLOW_API_KEY" \
    -H "juglow-version: 2023-06-01" \
    -H "juglow-beta: managed-agents-2026-04-01" \
    -H "content-type: application/json" \
    -d '{
      "name": "self-hosted",
      "config": {"type": "self_hosted"}
    }'
bash
    ant apply environment.yaml
yaml
      # yaml-language-server: $schema=https://platform.juglow.my.id/schemas/ant/beta/environment.json
      name: self-hosted
      config:
        type: self_hosted
python
  client = juglow.Juglow()

  environment = client.beta.environments.create(
      name="self-hosted", config={"type": "self_hosted"}
  )
  print(environment.id)
typescript
  const client = new Juglow();

  const environment = await client.beta.environments.create({
    name: "self-hosted",
    config: { type: "self_hosted" }
  });
  console.log(environment.id);
csharp
  using Juglow.Models.Beta.Environments;

  var client = new JuglowClient();

  var environment = await client.Beta.Environments.Create(
      new EnvironmentCreateParams
      {
          Name = "self-hosted",
          Config = new BetaSelfHostedConfigParams(),
      }
  );
  Console.WriteLine(environment.ID);
go
  client := juglow.NewClient()

  environment, err := client.Beta.Environments.New(context.Background(), juglow.BetaEnvironmentNewParams{
  	Name: "self-hosted",
  	Config: juglow.BetaEnvironmentNewParamsConfigUnion{
  		OfSelfHosted: &juglow.BetaSelfHostedConfigParams{},
  	},
  })
  if err != nil {
  	panic(err)
  }
  fmt.Println(environment.ID)
java
  import com.juglow.models.beta.environments.BetaSelfHostedConfigParams;
  import com.juglow.models.beta.environments.EnvironmentCreateParams;

  void main() {
      var client = JuglowOkHttpClient.fromEnv();

      var environment = client.beta().environments().create(
          EnvironmentCreateParams.builder()
              .name("self-hosted")
              .config(BetaSelfHostedConfigParams.builder().build())
              .build()
      );
      IO.println(environment.id());
  }
php
  $client = new Juglow\Client();

  $environment = $client->beta->environments->create(
      name: 'self-hosted',
      config: ['type' => 'self_hosted'],
  );
  echo $environment->id, PHP_EOL;
ruby
  client = Juglow::Client.new

  environment = client.beta.environments.create(
    name: "self-hosted",
    config: {type: :self_hosted}
  )
  puts environment.id
  1. Buat environment key

Di Console, buka environment dan klik Generate environment key. Pembuatan key hanya dapat dilakukan di Console, terlepas dari apakah Anda membuat environment melalui Console atau API. Kemudian ekspor ID environment dan key di host worker:

bash
export JUGLOW_ENVIRONMENT_KEY="sk-ant-oat01-..."
export JUGLOW_ENVIRONMENT_ID="env_..."

Note: Tracks dapat menyertakan file executable yang mungkin dijalankan agen secara langsung. Worker CLI dan SDK mempertahankan izin executable yang tercatat dalam bundel track saat mengekstraknya. Jika Anda mengimplementasikan pengunduhan tracks secara manual, Anda bertanggung jawab untuk mengatur izin executable.

Menjalankan worker

Pilih always-on untuk penyiapan paling sederhana: sebuah proses yang berjalan lama terus-menerus melakukan polling pada antrean dan hanya memerlukan HTTPS keluar. Pilih dipicu webhook jika Anda tidak ingin menjalankan poller yang menganggur. Opsi ini memerlukan endpoint webhook yang dapat dijangkau Juglow (lihat Webhook untuk penyiapan endpoint dan verifikasi tanda tangan).

Always-on (ant CLI)

  1. Instal CLI ant

Jalankan perintah ini di host worker.

Untuk lingkungan Linux, unduh biner rilis secara langsung.

bash
    VERSION=1.35.0
    OS=$(uname -s | tr '[:upper:]' '[:lower:]')
    case $(uname -m) in
      x86_64) ARCH=amd64 ;;
      aarch64) ARCH=arm64 ;;
    esac
    curl -fsSL "https://github.com/juglows/juglow-cli/releases/download/v${VERSION}/ant_${VERSION}_${OS}_${ARCH}.tar.gz" \
      | sudo tar -xz -C /usr/local/bin ant

Anda dapat menemukan semua rilis di halaman rilis GitHub.

mebrew (macOS)**

ash install juglows/tap/ant

  1. Jalankan worker

Dalam proses

ant beta:worker poll mengklaim work item yang ditugaskan ke environment, mengunduh track, mengeksekusi panggilan alat di direktori kerja, dan mengirimkan hasilnya kembali. Perintah ini membaca JUGLOW_ENVIRONMENT_KEY dan JUGLOW_ENVIRONMENT_ID dari environment.

bash
ant beta:worker poll --workdir "/workspace"

Worker berhenti dengan bersih saat menerima SIGTERM atau SIGINT. Sebelum berhenti, worker membatalkan panggilan alat yang sedang berjalan, mengirimkan hasil error-nya, dan melepaskan work item.

Sandbox per sesi

Jika Anda memerlukan isolasi yang lebih kuat (sistem file baru, batas sumber daya, atau kontrol jaringan per sesi), jalankan setiap sesi di sandbox-nya sendiri. Bangun image yang sudah menginstal ant dengan ant beta:worker run sebagai entrypoint. Image dasar harus menyediakan /bin/bash, sedangkan curl hanya digunakan saat build. Saat dimulai, sandbox membaca detail sesi dari variabel lingkungan, menangani sesi tersebut, lalu keluar:

text
FROM your-base-image
ARG ANT_VERSION=1.35.0
ARG TARGETARCH
RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo arm64 || echo amd64) && \
    curl -fsSL "https://github.com/juglows/juglow-cli/releases/download/v${ANT_VERSION}/ant_${ANT_VERSION}_linux_${ARCH}.tar.gz" \
      | tar -xz -C /usr/local/bin ant
WORKDIR /workspace
VOLUME /workspace
ENTRYPOINT ["ant", "beta:worker", "run"]

Kemudian tulis skrip spawn yang meneruskan detail sesi ke sandbox baru. Poller menyuntikkan JUGLOW_SESSION_ID, JUGLOW_WORK_ID, JUGLOW_ENVIRONMENT_ID, dan JUGLOW_ENVIRONMENT_KEY ke environment skrip. Poller juga menulis work item yang diklaim ke input standar skrip dalam format JSON, termasuk secret per sesi milik work item jika Juglow menerbitkannya. JUGLOW_BASE_URL bersifat opsional dan hanya diteruskan jika diatur di host poller; variabel ini menimpa endpoint API default.

Dalam contoh ini, /host/outputs adalah direktori host pilihan Anda. Direktori ini di-bind-mount ke direktori kerja sandbox (/workspace) agar Anda dapat mengambil hasil sesi setelah sandbox keluar. Pada environment self-hosted, agen menulis hasil di bawah direktori kerja, bukan di /mnt/session/outputs (lihat Sistem file sandbox). Karena itu, hasil tersebut ditangkap dengan me-mount direktori kerja. Mount ini juga mencakup pohon tracks/ yang diunduh dan file perantara apa pun yang dibuat agen.

bash
#!/bin/bash
# spawn.sh: dipanggil sekali untuk setiap item pekerjaan yang diklaim
mkdir -p "/host/outputs/$JUGLOW_SESSION_ID"
exec docker run --rm \
  -e JUGLOW_SESSION_ID -e JUGLOW_ENVIRONMENT_KEY \
  -e JUGLOW_WORK_ID -e JUGLOW_ENVIRONMENT_ID -e JUGLOW_BASE_URL \
  -v "/host/outputs/$JUGLOW_SESSION_ID":/workspace \
  your-image

Entrypoint ant beta:worker run tidak me-mount memory store. Jika sesi di environment ini melampirkan memory store, tetap gunakan poller, tetapi bangun image per sesi berbasis worker SDK. Perluas juga skrip spawn agar meneruskan secret milik work item ke dalam sandbox, seperti yang ditunjukkan di Jalankan satu sandbox per sesi.

Mulai poller dan arahkan ke skrip tersebut:

bash
ant beta:worker poll --on-work ./spawn.sh
  1. Jalankan worker

EnvironmentWorker mengklaim work item yang ditugaskan ke environment, mengunduh track, mengeksekusi panggilan alat di direktori kerja, dan mengirimkan hasilnya kembali. Lakukan autentikasi dengan kunci environment yang Anda buat di Sebelum Anda mulai.

python
  import asyncio
  import contextlib
  import os
  import signal
  from juglow import AsyncJuglow
  from juglow.lib.environments import EnvironmentWorker

  async def main() -> None:
      environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
      environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
      async with AsyncJuglow(auth_token=environment_key) as client:
          worker = EnvironmentWorker(
              client,
              environment_id=environment_id,
              environment_key=environment_key,
              workdir="/workspace",
          )
          task = asyncio.create_task(worker.run())
          # Membatalkan task, alih-alih mematikan proses, memungkinkan worker menghentikan
          # item pekerjaan yang sedang berjalan dan mengunggah file memori yang berubah sebelum keluar.
          loop = asyncio.get_running_loop()
          for signum in (signal.SIGINT, signal.SIGTERM):
              loop.add_signal_handler(signum, task.cancel)
          with contextlib.suppress(asyncio.CancelledError):
              await task

  asyncio.run(main())
typescript
  import Juglow from "@juglow-ai/sdk";
  import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";

  const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
  const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
  const client = new Juglow({ authToken: environmentKey });
  const controller = new AbortController();
  // Membatalkan pada salah satu sinyal memungkinkan worker mengunggah file memori yang berubah dan menghapus
  // direktori store-nya sebelum proses keluar.
  process.once("SIGINT", () => controller.abort());
  process.once("SIGTERM", () => controller.abort());

  await new EnvironmentWorker({
    client,
    environmentId,
    environmentKey,
    workdir: "/workspace",
    signal: controller.signal
  }).run();
csharp
  // EnvironmentWorker saat ini belum tersedia di SDK C#. Lihat tab Always-on (ant CLI).
go
  package main

  import (
  	"context"
  	"log"
  	"os"
  	"os/signal"
  	"syscall"

  	"github.com/juglows/juglow-sdk-go"
  	"github.com/juglows/juglow-sdk-go/lib/environments"
  	"github.com/juglows/juglow-sdk-go/option"
  )

  func main() {
  	environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")
  	environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")

  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  	defer stop()

  	client := juglow.NewClient(option.WithAuthToken(environmentKey))

  	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  		EnvironmentID:  environmentID,
  		EnvironmentKey: environmentKey,
  		Workdir:        "/workspace",
  	})
  	if err := worker.Run(ctx); err != nil {
  		log.Fatalf("worker: %v", err)
  	}
  }
java
  // EnvironmentWorker saat ini belum tersedia di Java SDK. Lihat tab Always-on (ant CLI).
php
  // EnvironmentWorker saat ini belum tersedia di PHP SDK. Lihat tab Always-on (ant CLI).
ruby
  # EnvironmentWorker saat ini belum tersedia di Ruby SDK. Lihat tab Always-on (ant CLI).
  1. Berlangganan webhook sesi

Di Console, tentukan endpoint webhook yang mendengarkan event session.status_run_started. Lihat Webhook untuk detailnya.

  1. Ekspor kunci penandatanganan webhook

Selain ID environment dan kunci dari Sebelum Anda mulai, ekspor kunci penandatanganan webhook di host handler Anda agar handler dapat memverifikasi payload yang masuk. Verifikasi tanda tangan di handler Python memerlukan paket tambahan webhooks: pip install "juglow[webhooks]".

bash
export JUGLOW_WEBHOOK_SIGNING_KEY="whsec_..."
  1. Implementasikan handler webhook

EnvironmentWorker mengklaim work item, mengunduh track, mengeksekusi panggilan alat di direktori kerja, mengirimkan hasilnya kembali, lalu keluar. Panggil worker ini saat session.status_run_started terpicu.

Jika Anda sendiri yang menyerahkan work item yang diklaim ke handle_item() (typescript: handleItem(); go: HandleItem()), seperti yang dilakukan handler ini, teruskan secret milik work item sebagai work_secret (typescript: workSecret; go: WorkSecret) agar sesi dapat me-mount memory store apa pun yang dilampirkan padanya. Handler seperti ini menjalankan setiap item yang diklaim dalam satu proses di satu host. Akibatnya, dua sesi yang melampirkan memory store yang sama tidak dapat berjalan melalui handler ini secara bersamaan (lihat Menyiapkan host). Jika sesi Anda berbagi store, jalankan satu sandbox per sesi sebagai gantinya.

python
  import asyncio
  import os
  import juglow
  import standardwebhooks  # installed by the juglow[webhooks] extra

  environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
  environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
  client = juglow.AsyncJuglow(
      auth_token=environment_key,
  )
  # Dibatalkan oleh shutdown() agar work item yang sedang berjalan dapat mengunggah file memori yang berubah dan
  # menghapus direktori store-nya sebelum proses berakhir.
  inflight: set[asyncio.Task[None]] = set()

  # Await ini dari hook shutdown milik host, misalnya shutdown lifespan ASGI (kode setelah
  # `yield` dalam lifespan FastAPI), yang dijalankan uvicorn saat SIGTERM. uvicorn membiarkan request terbuka
  # selesai sebelum hook itu berjalan, jadi atur --timeout-graceful-shutdown untuk membatasi waktu tunggu.
  async def shutdown() -> None:
      for task in inflight:
          task.cancel()
      await asyncio.gather(*inflight, return_exceptions=True)

  async def handle(raw: bytes, headers: dict[str, str]) -> tuple[dict[str, str], int]:
      try:
          event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
      except standardwebhooks.WebhookVerificationError:
          return {"error": "signature verification failed"}, 401
      if event.data.type != "session.status_run_started":
          return {"status": "ignored"}, 200
      task = asyncio.create_task(run_queued_work())
      inflight.add(task)
      task.add_done_callback(inflight.discard)
      try:
          # Dilindungi (shielded): pengiriman yang terputus atau timeout tidak boleh membatalkan item; shutdown() yang melakukannya.
          await asyncio.shield(task)
      except asyncio.CancelledError:
          return {"status": "shutting down"}, 503
      return {"status": "ok"}, 200

  async def run_queued_work() -> None:
      async for work in client.beta.environments.work.poller(
          environment_id=environment_id,
          environment_key=environment_key,
          block_ms=None,
          reclaim_older_than_ms=2000,
          drain=True,
          auto_stop=False,
      ):
          await client.beta.environments.work.worker(workdir="/workspace").handle_item(
              work_id=work.id,
              environment_id=environment_id,
              session_id=work.data.id,
              environment_key=environment_key,
              # Secret per sesi inilah yang memungkinkan worker me-mount memory store milik sesi tersebut.
              work_secret=work.secret,
          )
typescript
  import Juglow from "@juglow-ai/sdk";

  const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
  const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
  const client = new Juglow({
    authToken: environmentKey
  });
  // Panggil shutdown.abort() dari handler SIGTERM/SIGINT milik host, bersamaan dengan menutup server,
  // lalu tunggu panggilan handle() yang sedang berjalan sebelum keluar: abort memungkinkan item kerja yang berjalan
  // mengunggah file memori yang berubah dan menghapus direktori store-nya terlebih dahulu.
  export const shutdown = new AbortController();

  export async function handle(req: Request): Promise<Response> {
    // Jangan pernah mengakui pengiriman yang pekerjaannya tidak akan dijalankan di sini; 503 membuat pengirim mencoba lagi.
    if (shutdown.signal.aborted) {
      return Response.json({ status: "shutting down" }, { status: 503 });
    }
    const body = await req.text();
    let event;
    try {
      event = client.beta.webhooks.unwrap(body, { headers: Object.fromEntries(req.headers) });
    } catch {
      return new Response("signature verification failed", { status: 401 });
    }
    if (event.data.type !== "session.status_run_started") {
      return Response.json({ status: "ignored" });
    }

    for await (const work of client.beta.environments.work.poller({
      environmentId,
      environmentKey,
      blockMs: null,
      reclaimOlderThanMs: 2000,
      drain: true,
      autoStop: false,
      signal: shutdown.signal
    })) {
      await client.beta.environments.work.worker({ workdir: "/workspace" }).handleItem({
        workId: work.id,
        environmentId,
        sessionId: work.data.id,
        environmentKey,
        // Secret per sesi inilah yang memungkinkan worker me-mount memory store milik sesi tersebut.
        workSecret: work.secret ?? undefined,
        signal: shutdown.signal
      });
    }
    // Poller dan handleItem kembali secara diam-diam saat abort, sehingga drain yang terpotong berakhir di sini.
    if (shutdown.signal.aborted) {
      return Response.json({ status: "shutting down" }, { status: 503 });
    }
    return Response.json({ status: "ok" });
  }
csharp
  // EnvironmentWorker saat ini belum tersedia di SDK C#.
  // Untuk menangani item pekerjaan secara langsung, lihat endpoint Environments Work.
go
  package main

  import (
  	"context"
  	"encoding/json"
  	"errors"
  	"io"
  	"log/slog"
  	"net/http"
  	"os"
  	"os/signal"
  	"syscall"

  	"github.com/juglows/juglow-sdk-go"
  	"github.com/juglows/juglow-sdk-go/lib/environments"
  	"github.com/juglows/juglow-sdk-go/option"
  	"github.com/juglows/juglow-sdk-go/packages/param"
  )

  var (
  	environmentKey = os.Getenv("JUGLOW_ENVIRONMENT_KEY")
  	environmentID  = os.Getenv("JUGLOW_ENVIRONMENT_ID")
  	client         = juglow.NewClient(
  		option.WithAuthToken(environmentKey),
  		option.WithWebhookKey(os.Getenv("JUGLOW_WEBHOOK_SIGNING_KEY")),
  	)
  	worker = environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  		Workdir: "/workspace",
  	})
  	// Dibatalkan pada SIGINT atau SIGTERM (diatur di main) agar work item yang sedang berjalan dapat
  	// mengunggah file memori yang berubah dan menghapus direktori store-nya sebelum keluar.
  	shutdown context.Context
  )

  func handle(w http.ResponseWriter, r *http.Request) {
  	body, err := io.ReadAll(r.Body)
  	if err != nil {
  		http.Error(w, "bad request", http.StatusBadRequest)
  		return
  	}
  	event, err := client.Beta.Webhooks.Unwrap(body, r.Header)
  	if err != nil {
  		http.Error(w, "signature verification failed", http.StatusUnauthorized)
  		return
  	}
  	if event.Data.Type != "session.status_run_started" {
  		json.NewEncoder(w).Encode(map[string]string{"status": "ignored"})
  		return
  	}

  	// Go SDK tidak menyediakan kemudahan RunOne: kuras item yang tertunda
  	// dengan WorkPoller dan jalankan masing-masing dengan HandleItem.
  	// Lepaskan dari r.Context(): sesi dapat bertahan lebih lama dari batas waktu pengiriman webhook.
  	// Konteks shutdown tingkat proses tetap mengakhiri item dengan bersih pada SIGTERM.
  	ctx := shutdown
  	poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
  		EnvironmentID:      environmentID,
  		EnvironmentKey:     environmentKey,
  		BlockMs:            param.Null[int64](),
  		ReclaimOlderThanMs: param.NewOpt[int64](2000),
  		Drain:              true,
  		AutoStop:           param.NewOpt(false),
  	})
  	defer poller.Close()
  	for poller.Next() {
  		item := poller.Current()
  		if err := worker.HandleItem(ctx, environments.HandleItemOptions{
  			WorkID:         item.ID,
  			EnvironmentID:  item.EnvironmentID,
  			SessionID:      item.Data.ID,
  			EnvironmentKey: environmentKey,
  			// Secret per sesi inilah yang memungkinkan worker memasang memory store milik sesi.
  			WorkSecret: item.Secret,
  		}); err != nil {
  			slog.Error("handle work item", "work_id", item.ID, "err", err)
  			http.Error(w, "internal error", http.StatusInternalServerError)
  			return
  		}
  	}
  	if err := poller.Err(); err != nil {
  		slog.Error("poll work queue", "err", err)
  		http.Error(w, "internal error", http.StatusInternalServerError)
  		return
  	}
  	json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  }

  func main() {
  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  	defer stop()
  	shutdown = ctx

  	server := &http.Server{Addr: ":8080"}
  	http.HandleFunc("POST /webhook", handle)
  	go func() {
  		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
  			slog.Error("http server", "err", err)
  			os.Exit(1)
  		}
  	}()
  	// Saat ada sinyal, berhenti menerima pengiriman dan kembali hanya setelah handler yang sedang berjalan,
  	// dan karenanya teardown memori work item mereka, telah selesai.
  	<-ctx.Done()
  	if err := server.Shutdown(context.Background()); err != nil {
  		slog.Error("http shutdown", "err", err)
  	}
  }
java
  // EnvironmentWorker saat ini belum tersedia di Java SDK.
  // Untuk menangani item kerja secara langsung, lihat endpoint Environments Work.
php
  // EnvironmentWorker saat ini belum tersedia di PHP SDK.
  // Untuk menangani item pekerjaan secara langsung, lihat endpoint Environments Work.
ruby
  # EnvironmentWorker saat ini belum tersedia di Ruby SDK.
  # Untuk menangani item pekerjaan secara langsung, lihat endpoint Environments Work.

Helper SDK

SDK menyediakan tiga helper dengan tingkat kendali yang berbeda. EnvironmentWorker mencakup sebagian besar kasus penggunaan. Gunakan helper tingkat lebih rendah jika Anda perlu meluncurkan proses per sesi sendiri atau menjalankan alat pada sesi yang sudah diklaim.

  • EnvironmentWorker: worker siap pakai yang menangani polling, penyiapan, dan eksekusi secara menyeluruh.
  • .run() (go: .Run()): berjalan tanpa batas waktu dan mengambil sesi begitu sesi tersebut tiba.
  • .handle_item() (typescript: .handleItem(); go: .HandleItem()): menangani satu work item yang diklaim lalu keluar. Berikan pengidentifikasi work, sesi, dan environment secara eksplisit, atau biarkan metode ini membaca variabel JUGLOW_* yang diatur oleh ant beta:worker poll --on-work untuk proses yang dijalankannya. Agar sesi dapat me-mount memory store-nya, berikan juga secret milik work item sebagai work_secret (typescript: workSecret; go: WorkSecret) atau atur JUGLOW_WORK_SECRET. Karena ant beta:worker poll --on-work tidak mengatur variabel tersebut, baca secret dari JSON work item yang ditulisnya ke input standar skrip Anda, seperti yang ditunjukkan di Jalankan satu sandbox per sesi.
  • memory_sync_interval (typescript: memorySyncIntervalMs; go: MemorySyncInterval) dan memory_sync_deletions (typescript: memorySyncDeletions; go: MemorySyncDeletions): seberapa sering memory store yang dilampirkan direkonsiliasi dengan server selama sesi berjalan, dan apakah file yang dihapus agen secara lokal juga dihapus dari store. Lihat Konfigurasikan sinkronisasi untuk satuan, nilai default, dan cara menonaktifkan dukungan memori.
  • work.poller() (go: environments.NewWorkPoller()): melakukan polling pada antrean kerja atas nama Anda dan memberikan setiap sesi yang diklaim. Gunakan helper ini jika Anda ingin menentukan sendiri apa yang terjadi pada setiap sesi, misalnya meluncurkan sandbox alih-alih menjalankan alat dalam proses.
  • drain (go: Drain): apakah polling dihentikan setelah antrean kosong alih-alih menunggu pekerjaan baru.
  • block_ms (python; typescript: blockMs; go: BlockMs): berapa lama menunggu pekerjaan tiba sebelum kembali, dalam milidetik. Nilainya harus antara 1 dan 999 (waktu tunggu per polling; helper melakukan polling ulang secara otomatis). Berikan null (typescript; python: None; go: param.Null[int64]()) untuk pemeriksaan non-blocking. Jika parameter ini dihilangkan, long-poll default 999 ms akan digunakan.
  • reclaim_older_than_ms (typescript: reclaimOlderThanMs; go: ReclaimOlderThanMs): mengklaim ulang work item yang sudah diklaim tetapi tidak pernah dikonfirmasi dalam jumlah milidetik ini.
  • auto_stop (typescript: autoStop; go: AutoStop): apakah sinyal stop dikirimkan untuk setiap work item setelah badan loop Anda selesai memprosesnya. Nonaktifkan opsi ini jika komponen yang menjalankan work item sudah mengirimkan stop sendiri. handle_item() (typescript: handleItem(); go: HandleItem()) melakukannya, jadi atur opsi ini ke false saat Anda menyerahkan item yang diklaim ke handle_item() (typescript: handleItem(); go: HandleItem()), seperti yang dilakukan handler webhook di halaman ini. Hal yang sama berlaku untuk sandbox yang Anda luncurkan jika sandbox tersebut yang bertanggung jawab atas panggilan stop.
  • client.beta.sessions.events.tool_runner(): menjalankan panggilan alat untuk satu sesi berdasarkan ID sesi dan daftar alat. Gunakan helper ini jika Anda sudah mengklaim pekerjaan dan hanya memerlukan lapisan eksekusi.

Gunakan work.poller() (typescript: new WorkPoller(); go: environments.NewWorkPoller()) secara langsung jika Anda ingin meluncurkan proses per sesi sendiri, misalnya menjalankan sandbox untuk setiap sesi yang diklaim:

bash
  # Work poller adalah helper SDK (Python, TypeScript, Go), bukan endpoint
  # mentah. Dari shell, gunakan `ant beta:worker poll --on-work` sebagai gantinya;
  # lihat tab Always-on (ant CLI).
bash
  # Work poller adalah helper SDK (Python, TypeScript, Go), bukan endpoint
  # mentah. Dari shell, gunakan `ant beta:worker poll --on-work` sebagai gantinya;
  # lihat tab Always-on (ant CLI).
python
  import asyncio
  import os

  from juglow import AsyncJuglow
  from juglow.types.beta.environments import BetaSelfHostedWork

  SANDBOX_ENV = (
      "JUGLOW_ENVIRONMENT_ID",
      "JUGLOW_ENVIRONMENT_KEY",
      "JUGLOW_WORK_ID",
      "JUGLOW_SESSION_ID",
      "JUGLOW_WORK_SECRET",
      "JUGLOW_BASE_URL",  # forwarded only when set on this host
  )

  async def launch_container(work: BetaSelfHostedWork) -> None:
      print(f"claimed session {work.data.id}")
      # Ganti `docker run` dengan peluncur sandbox Anda sendiri. Teruskan kunci
      # environment (jangan pernah kunci API Anda) dan secret per sesi milik item kerja: worker
      # di dalamnya memerlukan secret tersebut untuk me-mount memory store milik sesi.
      env = os.environ | {
          "JUGLOW_WORK_ID": work.id,
          "JUGLOW_SESSION_ID": work.data.id,
          "JUGLOW_WORK_SECRET": work.secret or "",
      }
      forward = [arg for name in SANDBOX_ENV for arg in ("-e", name)]
      launcher = await asyncio.create_subprocess_exec(
          "docker", "run", "--rm", "--detach", *forward, "your-sdk-worker-image", env=env
      )
      await launcher.wait()

  async def main() -> None:
      environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
      environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
      async with AsyncJuglow(auth_token=environment_key) as client:
          async for work in client.beta.environments.work.poller(
              environment_id=environment_id,
              environment_key=environment_key,
              auto_stop=False,  # the launched sandbox owns the stop call
          ):
              await launch_container(work)

  asyncio.run(main())
typescript
  import { spawn } from "node:child_process";
  import { once } from "node:events";
  import Juglow from "@juglow-ai/sdk";
  import { WorkPoller } from "@juglow-ai/sdk/helpers/beta/environments";
  import type { BetaSelfHostedWork } from "@juglow-ai/sdk/resources/beta/environments";

  const SANDBOX_ENV = [
    "JUGLOW_ENVIRONMENT_ID",
    "JUGLOW_ENVIRONMENT_KEY",
    "JUGLOW_WORK_ID",
    "JUGLOW_SESSION_ID",
    "JUGLOW_WORK_SECRET",
    "JUGLOW_BASE_URL" // forwarded only when set on this host
  ];

  const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
  const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
  const client = new Juglow({ authToken: environmentKey });

  async function launchContainer(work: BetaSelfHostedWork): Promise<void> {
    console.log(`claimed session ${work.data.id}`);
    // Ganti `docker run` dengan peluncur sandbox Anda sendiri. Teruskan kunci
    // environment (jangan pernah kunci API Anda) dan secret per sesi milik work item: worker
    // di dalamnya memerlukan secret tersebut untuk me-mount memory store milik sesi.
    const env = {
      ...process.env,
      JUGLOW_WORK_ID: work.id,
      JUGLOW_SESSION_ID: work.data.id,
      JUGLOW_WORK_SECRET: work.secret ?? ""
    };
    const forward = SANDBOX_ENV.flatMap((name) => ["-e", name]);
    const launcher = spawn(
      "docker",
      ["run", "--rm", "--detach", ...forward, "your-sdk-worker-image"],
      { env, stdio: "inherit" }
    );
    await once(launcher, "close");
  }

  const poller = new WorkPoller({
    client,
    environmentId,
    environmentKey,
    autoStop: false // the launched sandbox owns the stop call
  });

  for await (const work of poller) {
    await launchContainer(work);
  }
csharp
  // Helper untuk polling pekerjaan saat ini belum tersedia di SDK C#.
  // Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work.
go
  package main

  import (
  	"context"
  	"fmt"
  	"log"
  	"os"
  	"os/exec"

  	"github.com/juglows/juglow-sdk-go"
  	"github.com/juglows/juglow-sdk-go/lib/environments"
  	"github.com/juglows/juglow-sdk-go/option"
  	"github.com/juglows/juglow-sdk-go/packages/param"
  )

  var sandboxEnv = []string{
  	"JUGLOW_ENVIRONMENT_ID",
  	"JUGLOW_ENVIRONMENT_KEY",
  	"JUGLOW_WORK_ID",
  	"JUGLOW_SESSION_ID",
  	"JUGLOW_WORK_SECRET",
  	"JUGLOW_BASE_URL", // forwarded only when set on this host
  }

  func launchContainer(ctx context.Context, work *juglow.BetaSelfHostedWork) error {
  	fmt.Printf("claimed session %s\n", work.Data.ID)
  	// Ganti `docker run` dengan peluncur sandbox Anda sendiri. Teruskan kunci
  	// environment (jangan pernah kunci API Anda) dan secret per sesi milik work item: worker
  	// di dalamnya memerlukan secret tersebut untuk me-mount memory store milik sesi.
  	args := []string{"run", "--rm", "--detach"}
  	for _, name := range sandboxEnv {
  		args = append(args, "-e", name)
  	}
  	launcher := exec.CommandContext(ctx, "docker", append(args, "your-sdk-worker-image")...)
  	launcher.Env = append(os.Environ(),
  		"JUGLOW_WORK_ID="+work.ID,
  		"JUGLOW_SESSION_ID="+work.Data.ID,
  		"JUGLOW_WORK_SECRET="+work.Secret,
  	)
  	launcher.Stdout, launcher.Stderr = os.Stdout, os.Stderr
  	return launcher.Run()
  }

  func main() {
  	environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")
  	environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")

  	client := juglow.NewClient(option.WithAuthToken(environmentKey))

  	ctx := context.Background()

  	poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
  		EnvironmentID:  environmentID,
  		EnvironmentKey: environmentKey,
  		AutoStop:       param.NewOpt(false), // the launched sandbox owns the stop call
  	})
  	defer poller.Close()

  	for work, err := range poller.All() {
  		if err != nil {
  			log.Fatal(err)
  		}
  		if err := launchContainer(ctx, work); err != nil {
  			log.Fatal(err)
  		}
  	}
  }
java
  // Helper untuk polling pekerjaan saat ini belum tersedia di Java SDK.
  // Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work.
php
  // Helper untuk polling pekerjaan saat ini belum tersedia di PHP SDK.
  // Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work.
ruby
  # Helper untuk polling pekerjaan saat ini belum tersedia di Ruby SDK.
  # Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work.

Komponen apa pun yang meluncurkan sandbox harus meneruskan secret milik work item yang diklaim ke dalamnya (misalnya sebagai JUGLOW_WORK_SECRET), bersama dengan pengidentifikasi sesi, work, dan environment. Dengan begitu, worker di dalam sandbox dapat me-mount memory store milik sesi; lihat Jalankan satu sandbox per sesi.

AgentToolContext adalah konteks eksekusi untuk panggilan alat. Konteks ini menentukan direktori kerja dan kebijakan path, serta dapat mengunduh track sesi. Alat file (read, write, edit, glob, grep) dibatasi pada direktori kerja ditambah direktori apa pun yang tercantum di allowed_roots (typescript: allowedRoots; go: AllowedRoots). Selain itu, write dan edit menolak path di bawah read_only_roots (typescript: readOnlyRoots; go: ReadOnlyRoots). EnvironmentWorker menambahkan direktori memory store sesi ke daftar ini secara otomatis. Pembatasan ini hanya berfungsi sebagai pengaman untuk alat file, bukan sebagai sandbox, dan tidak membatasi bash. beta_agent_toolset_20260401(env) (typescript: betaAgentToolset20260401(ctx); go: agenttoolset.BetaAgentToolset20260401(env)) menerima AgentToolContext dan mengembalikan implementasi alat standar (bash, read, write, edit, glob, grep).

Dengan EnvironmentWorker: keduanya dikelola secara otomatis. Berikan factory tools (go: ToolsFunc) untuk menyesuaikan daftar alat:

python
  EnvironmentWorker(client, ..., tools=lambda env: [beta_bash_tool(env), my_custom_tool])
typescript
  new EnvironmentWorker({
    client,
    environmentId,
    environmentKey,
    tools: (ctx) => [betaBashTool(ctx), myCustomTool]
  });
csharp
  // EnvironmentWorker saat ini belum tersedia di SDK C#.
  // Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.
go
  worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  	EnvironmentID:  environmentID,
  	EnvironmentKey: environmentKey,
  	ToolsFunc: func(env *agenttoolset.AgentToolContext) []juglow.BetaTool {
  		return []juglow.BetaTool{agenttoolset.BetaBashTool(env), myCustomTool}
  	},
  })
java
  // EnvironmentWorker saat ini belum tersedia di Java SDK.
  // Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.
php
  // EnvironmentWorker saat ini belum tersedia di PHP SDK.
  // Untuk menjawab panggilan alat kustom secara langsung, lihat stream event sesi.
ruby
  # EnvironmentWorker saat ini belum tersedia di Ruby SDK.
  # Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.

Dengan work.poller() (typescript; go: environments.NewWorkPoller()) dan tool_runner(): berikan daftar alat sebagai tools ke client.beta.sessions.events.tool_runner(). Untuk membangun daftar tersebut, siapkan AgentToolContext sendiri lalu panggil beta_agent_toolset_20260401(env) (typescript: betaAgentToolset20260401(ctx); go: agenttoolset.BetaAgentToolset20260401(env)):

python
  from juglow.lib.tools.agent_toolset import (
      AgentToolContext,
      beta_agent_toolset_20260401,
  )

  async with AgentToolContext(
      workdir="/workspace", client=client, session_id=work.data.id
  ) as env:
      # tracks diunduh ke /workspace/tracks/<name>/
      tools = beta_agent_toolset_20260401(env)
typescript
  import {
    setupSkills,
    betaAgentToolset20260401
  } from "@juglow-ai/sdk/tools/agent-toolset/node";

  const ctx = { workdir: "/workspace", client, sessionId: work.data.id };
  await setupSkills(ctx);
  const tools = betaAgentToolset20260401(ctx);
csharp
  // AgentToolContext saat ini belum tersedia di SDK C#.
go
  env := &agenttoolset.AgentToolContext{Workdir: "/workspace"}
  if err := env.SetupSkills(ctx, client, work.Data.ID); err != nil {
  	panic(err)
  }
  // tracks diunduh ke /workspace/tracks/<name>/
  tools := agenttoolset.BetaAgentToolset20260401(env)
java
  // AgentToolContext saat ini belum tersedia di Java SDK.
php
  // AgentToolContext saat ini belum tersedia di PHP SDK.
ruby
  # AgentToolContext saat ini belum tersedia di Ruby SDK.

Memverifikasi worker terhubung

Dari shell terpisah, dengan JUGLOW_API_KEY diatur ke kunci API Haijun Anda (bukan environment key), pastikan workers_polling setidaknya 1:

bash
ant beta:environments:work stats --environment-id "$JUGLOW_ENVIRONMENT_ID"

Jika workers_polling tetap 0, worker tidak menjangkau antrean: pastikan JUGLOW_ENVIRONMENT_KEY dan JUGLOW_ENVIRONMENT_ID diatur di host worker. Lihat Membaca kedalaman antrean untuk respons statistik lengkap dan contoh bahasa lainnya.

Memulai sesi

Setelah worker Anda berjalan, buat sesi yang menargetkan environment tersebut. Atur AGENT_ID ke ID agen yang Anda catat di Sebelum Anda mulai. Sesi masuk ke antrean kerja environment dan menunggu di sana hingga worker mengklaimnya; jika tidak ada worker yang terhubung, sesi tetap dalam antrean alih-alih gagal.

Juglow tidak me-mount file atau repositori GitHub ke sandbox self-hosted. Untuk menyediakan file khusus sesi, berikan referensi file (seperti path S3 atau commit SHA) di field metadata sesi. Work item yang diklaim tidak membawa metadata sesi, tetapi membawa ID sesi: skrip spawn atau handler --on-work Anda mengambil sesi (GET /v1/sessions/{session_id}) untuk membaca field metadata, lalu menyiapkan file ke direktori kerja sebelum eksekusi alat dimulai.

bash
  curl -sS --fail-with-body https://haijun.my.id/v1/sessions \
    -H "x-api-key: $JUGLOW_API_KEY" \
    -H "juglow-version: 2023-06-01" \
    -H "juglow-beta: managed-agents-2026-04-01" \
    -H "content-type: application/json" \
    -d @- <<EOF
  {
    "agent": "$AGENT_ID",
    "environment_id": "$JUGLOW_ENVIRONMENT_ID",
    "metadata": {"input_file": "s3://my-bucket/data.csv"}
  }
  EOF
bash
  ant beta:sessions create \
    --agent "$AGENT_ID" \
    --environment-id "$JUGLOW_ENVIRONMENT_ID" \
    --metadata '{"input_file": "s3://my-bucket/data.csv"}'
python
  session = client.beta.sessions.create(
      agent=agent.id,
      environment_id=environment.id,
      metadata={"input_file": "s3://my-bucket/data.csv"},
  )
typescript
  const session = await client.beta.sessions.create({
    agent: agent.id,
    environment_id: environment.id,
    metadata: { input_file: "s3://my-bucket/data.csv" }
  });
csharp
  var session = await client.Beta.Sessions.Create(new()
  {
      Agent = agent.ID,
      EnvironmentID = environment.ID,
      Metadata = new Dictionary<string, string> { ["input_file"] = "s3://my-bucket/data.csv" },
  });
go
  session, err := client.Beta.Sessions.New(ctx, juglow.BetaSessionNewParams{
  	Agent:         juglow.BetaSessionNewParamsAgentUnion{OfString: juglow.String(agent.ID)},
  	EnvironmentID: environment.ID,
  	Metadata: map[string]string{
  		"input_file": "s3://my-bucket/data.csv",
  	},
  })
  if err != nil {
  	panic(err)
  }
java
  var session = client.beta().sessions().create(SessionCreateParams.builder()
      .agent(agent.id())
      .environmentId(environment.id())
      .metadata(SessionCreateParams.Metadata.builder()
          .putAdditionalProperty("input_file", JsonValue.from("s3://my-bucket/data.csv"))
          .build())
      .build());
php
  $session = $client->beta->sessions->create(
      agent: $agent->id,
      environmentID: $environment->id,
      metadata: ['input_file' => 's3://my-bucket/data.csv'],
  );
ruby
  session = client.beta.sessions.create(
    agent: agent.id,
    environment_id: environment.id,
    metadata: {input_file: "s3://my-bucket/data.csv"}
  )

Note: Sandbox self-hosted hanya mendukung resource memory_store; lihat Menggunakan memory store. Sesi pada environment self-hosted yang menyertakan resource file atau github_repository ditolak dengan error 400: ``text wrap Environment env_... is a self-hosted environment. resources are not supported with self-hosted environments. `` Deployment yang menargetkan environment self-hosted mengikuti aturan yang sama.

Lihat Self-hosted worker di referensi untuk daftar lengkap flag CLI, dan Helper SDK untuk opsi helper SDK.

Menggunakan memory store

Sesi pada environment self-hosted melampirkan memory store dengan cara yang sama seperti sesi pada environment cloud: cantumkan store tersebut di resources saat Anda membuat sesi, seperti yang ditunjukkan di Melampirkan memory store ke sesi. Satu sesi dapat menerima hingga 8 memory store. Pada environment self-hosted, worker SDK (bukan infrastruktur Juglow) yang mewujudkan setiap store untuk agen. Karena itu, memory store di environment ini memerlukan EnvironmentWorker (atau metode handle_item() (typescript: handleItem(); go: HandleItem())-nya) dari SDK Python, TypeScript, atau Go.

Worker CLI ant (ant beta:worker poll dan ant beta:worker run) tidak me-mount memory store. Untuk menggabungkan poller CLI dengan memory store, jalankan worker SDK di dalam sandbox per sesi seperti yang dijelaskan di Jalankan satu sandbox per sesi.

Memory store tidak dapat dilampirkan ke sesi pada environment self-hosted di Haijun Platform on AWS.

Cara worker menangani memori

Ketika worker mengklaim work item yang sesinya memiliki memory store terlampir, ia:

  1. Mengunduh setiap store yang dilampirkan ke mount_path-nya di host worker, mengautentikasi dengan secret per sesi milik work item. mount_path adalah direktori yang sama di bawah /mnt/memory/ yang digunakan sesi cloud (misalnya, /mnt/memory/user-preferences/ untuk store bernama "User Preferences"), dan prompt sistem sesi menjelaskannya kepada agen.
  1. Menambahkan direktori tersebut ke allowed roots alat file, dan direktori store yang dilampirkan dengan access: "read_only" ke read-only roots-nya, sehingga agen bekerja pada memori dengan alat read, write, edit, glob, dan grep yang sama yang digunakannya di direktori kerja.
  1. Merekonsiliasi perubahan lokal dan remote setelah panggilan alat, paling banyak sekali per interval sinkronisasi (15 detik secara default): memori yang berubah di store ditulis ke disk, dan file yang diubah agen diunggah ke store.
  1. Menjalankan sinkronisasi akhir saat sesi berakhir, menyelesaikan unggahan apa pun yang masih tertunda hingga 30 detik, lalu menghapus direktori yang dibuatnya. Worker yang dibatalkan saat sesi berjalan melewatkan sinkronisasi akhir tetapi tetap mengunggah file yang berubah dan menghapus direktori sebelum keluar.

Memory store di sisi Juglow tetap menjadi sumber kebenaran. Versi memori, redaksi, dan melihat atau mengedit memori di Console berfungsi seperti pada sesi cloud, dan pembacaan serta penulisan memori oleh agen muncul di event stream sebagai event alat biasa. Karena setiap worker melakukan sinkronisasi berdasarkan interval, perubahan yang ditulis di satu sesi baru terlihat oleh sesi lain yang sedang berjalan setelah keduanya melakukan sinkronisasi, biasanya jauh di bawah satu menit pada interval default; sesi pada sandbox cloud melihat perubahan satu sama lain hampir seketika.

Setiap direktori store berisi file penanda bernama .juglow-memory-store yang mengikat direktori ke store-nya. Biarkan di tempatnya: worker tidak menyinkronkan direktori yang penandanya hilang atau diubah.

Menyiapkan host

Memory store pada sandbox self-hosted memerlukan sistem file POSIX di host worker (host Linux dari Sebelum Anda mulai); host Windows tidak didukung, karena worker memerlukan O_NOFOLLOW saat membuka file memori. Sistem file yang case-sensitive direkomendasikan, agar path memori yang hanya berbeda huruf besar-kecilnya tidak bertabrakan.

Sebelum Anda memulai worker, buat direktori induk dan jadikan dapat ditulis oleh pengguna yang menjalankan worker:

bash
sudo mkdir -p /mnt/memory && sudo chown "$USER" /mnt/memory

Jangan membuat direktori per store sendiri. Worker membuat direktori mount_path setiap store (misalnya, /mnt/memory/user-preferences) saat sesi dimulai, menolak memulai pekerjaan sesi jika sudah ada sesuatu di path tersebut, dan menghapus direktori saat sesi berakhir. Dua aturan operasional berlaku:

  • Jalankan satu sesi per sistem file ketika sesi melampirkan store yang sama. Dua sesi tidak dapat me-mount store yang sama di satu host pada waktu yang sama, karena keduanya memerlukan path yang sama. Memberi setiap sesi sandbox-nya sendiri, seperti dijelaskan di Jalankan satu sandbox per sesi, memenuhi aturan ini.
  • Hentikan worker dengan baik. Ketika Anda menghentikan worker saat sesi berjalan, EnvironmentWorker mengunggah file memori sesi yang berubah dan menghapus direktori store-nya hanya jika ia dibatalkan, bukan di-kill: proses yang di-kill tidak menjalankan teardown, dan worker tidak memasang signal handler sendiri. Hubungkan SIGTERM dan SIGINT ke pembatalan di proses yang menjalankannya: abort signal yang Anda berikan ke worker di TypeScript, batalkan context di Go, dan di Python batalkan task yang menjalankan run() atau handle_item(). Lakukan itu dari signal handler ketika worker Anda adalah prosesnya, seperti yang dilakukan worker mandiri di halaman ini, atau dari shutdown hook server Anda sendiri ketika worker berjalan di dalam handler webhook, yang tidak boleh mengambil alih sinyal server. Kemudian hentikan worker dengan SIGTERM dan beri setidaknya 30 detik untuk keluar sebelum hard kill apa pun, karena unggahan akhir dapat memakan waktu selama itu. Jika worker di-kill sebelum teardown-nya berjalan, hapus direktori store yang tersisa di bawah /mnt/memory/ sebelum sesi berikutnya yang melampirkan store tersebut; edit apa pun di dalamnya yang belum tersinkronisasi akan hilang.

Jalankan satu sandbox per sesi

Pola sandbox-per-sesi di Menjalankan worker memberikan sistem file baru untuk setiap sesi, sesuai yang disyaratkan Menyiapkan host jika beberapa sesi melampirkan store yang sama. Tetap gunakan ant beta:worker poll --on-work (atau work.poller() (go: environments.NewWorkPoller()) dari SDK) sebagai poller di host.

Entrypoint ant beta:worker run yang ditampilkan di sana tidak me-mount memory store. Karena itu, bangun image per-sesi di sekitar worker SDK. Entrypoint-nya membuat EnvironmentWorker dan memanggil handle_item() (typescript: handleItem(); go: HandleItem()), yang membaca pengidentifikasi sesi, work, dan environment dari variabel JUGLOW_*, serta secret per-sesi milik work item dari JUGLOW_WORK_SECRET. Anda juga dapat meneruskan secret secara eksplisit sebagai work_secret (typescript: workSecret; go: WorkSecret).

python
  import asyncio
  import contextlib
  import os
  import signal
  from juglow import AsyncJuglow
  from juglow.lib.environments import EnvironmentWorker

  async def main() -> None:
      async with AsyncJuglow(auth_token=os.environ["JUGLOW_ENVIRONMENT_KEY"]) as client:
          worker = EnvironmentWorker(client, workdir="/workspace")
          # Tanpa argumen, handle_item() membaca variabel JUGLOW_* yang diteruskan oleh
          # skrip spawn, termasuk JUGLOW_WORK_SECRET.
          task = asyncio.create_task(worker.handle_item())
          # Membatalkan task saat container dihentikan memungkinkan worker mengunggah
          # file memori yang berubah dan menghapus direktori store sebelum keluar.
          loop = asyncio.get_running_loop()
          for signum in (signal.SIGINT, signal.SIGTERM):
              loop.add_signal_handler(signum, task.cancel)
          with contextlib.suppress(asyncio.CancelledError):
              await task

  asyncio.run(main())
typescript
  import Juglow from "@juglow-ai/sdk";
  import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";

  const client = new Juglow({ authToken: process.env.JUGLOW_ENVIRONMENT_KEY });
  const controller = new AbortController();
  // Membatalkan saat container dihentikan memungkinkan worker mengunggah file memori yang berubah
  // dan menghapus direktori store sebelum keluar.
  process.once("SIGTERM", () => controller.abort());
  process.once("SIGINT", () => controller.abort());

  // Tanpa argumen, handleItem() membaca variabel JUGLOW_* yang diteruskan oleh skrip
  // spawn, termasuk JUGLOW_WORK_SECRET.
  await new EnvironmentWorker({
    client,
    workdir: "/workspace",
    signal: controller.signal
  }).handleItem();
csharp
  // EnvironmentWorker saat ini belum tersedia di SDK C#.
go
  package main

  import (
  	"context"
  	"log"
  	"os"
  	"os/signal"
  	"syscall"

  	"github.com/juglows/juglow-sdk-go"
  	"github.com/juglows/juglow-sdk-go/lib/environments"
  	"github.com/juglows/juglow-sdk-go/option"
  )

  func main() {
  	// Membatalkan context saat container dihentikan memungkinkan worker mengunggah
  	// file memori yang berubah dan menghapus direktori store sebelum keluar.
  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  	defer stop()

  	client := juglow.NewClient(option.WithAuthToken(os.Getenv("JUGLOW_ENVIRONMENT_KEY")))
  	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  		Workdir: "/workspace",
  	})
  	// Dengan opsi bernilai nol, HandleItem membaca variabel JUGLOW_* yang diteruskan
  	// oleh skrip spawn, termasuk JUGLOW_WORK_SECRET.
  	if err := worker.HandleItem(ctx, environments.HandleItemOptions{}); err != nil {
  		log.Fatalf("worker: %v", err)
  	}
  }
java
  // EnvironmentWorker saat ini belum tersedia di Java SDK.
php
  // EnvironmentWorker saat ini belum tersedia di PHP SDK.
ruby
  # EnvironmentWorker saat ini belum tersedia di Ruby SDK.

ant beta:worker poll --on-work tidak menetapkan JUGLOW_WORK_SECRET untuk skrip yang dijalankannya. Karena itu, skrip spawn membaca secret dari JSON work item pada standard input-nya dan meneruskannya ke dalam sandbox:

bash
#!/bin/bash
# spawn.sh: dipanggil sekali untuk setiap item kerja yang diklaim
# Item kerja yang diklaim diterima sebagai JSON melalui stdin. Secret-nya adalah
# kredensial per sesi yang diwajibkan oleh endpoint memory store.
JUGLOW_WORK_SECRET="$(jq -r '.secret // empty')"
export JUGLOW_WORK_SECRET
mkdir -p "/host/outputs/$JUGLOW_SESSION_ID"
exec docker run --rm \
  -e JUGLOW_SESSION_ID -e JUGLOW_ENVIRONMENT_KEY \
  -e JUGLOW_WORK_ID -e JUGLOW_ENVIRONMENT_ID -e JUGLOW_BASE_URL \
  -e JUGLOW_WORK_SECRET \
  -v "/host/outputs/$JUGLOW_SESSION_ID":/workspace \
  your-sdk-worker-image

Jika Anda mengklaim pekerjaan dengan work.poller() (go: environments.NewWorkPoller()) dari SDK, teruskan secret dari setiap item yang diklaim ke sandbox yang Anda luncurkan dengan cara yang sama. Teruskan secret hanya ke sandbox yang melayani sesi tersebut, dan jangan pernah mencatatnya di log.

Image sandbox juga memerlukan /mnt/memory yang dapat ditulisi (lihat Menyiapkan host). Karena setiap sandbox hanya melayani satu sesi dan dibuang setelahnya, tidak ada direktori sisa yang perlu dibersihkan. Direktori memori juga tidak perlu di-bind-mount ke host, karena worker mengunggah isinya ke store sebelum sandbox keluar. Jika Anda menghentikan container sebelum sesinya berakhir, kirim sinyal yang diubah entrypoint menjadi pembatalan (lihat Menyiapkan host), bukan mematikannya secara paksa, agar unggahan tetap berjalan. Beri juga waktu bagi container untuk menyelesaikan unggahan. Secara default, Docker mengirim SIGKILL 10 detik setelah sinyal stop, jadi naikkan batas tersebut setidaknya menjadi 30 detik seperti yang disyaratkan bagian Menyiapkan host, dengan --stop-timeout pada docker run atau termination grace period di orchestrator Anda.

Konfigurasikan sinkronisasi

Dua opsi EnvironmentWorker mengontrol perilaku memori:

  • memory_sync_interval (typescript: memorySyncIntervalMs; go: MemorySyncInterval) (dalam detik di Python, dalam milidetik di TypeScript, dan berupa durasi di Go): seberapa sering store yang dilampirkan direkonsiliasi dengan server selama sesi berjalan. Nilai default-nya 15 detik, dan minimumnya 5 detik. Interval yang lebih pendek mempersempit jendela waktu ketika sesi lain melihat memori yang usang, dengan konsekuensi lebih banyak permintaan ke memory store. None di Python, null di TypeScript, atau durasi negatif di Go menonaktifkan dukungan memori sepenuhnya. Dalam kondisi ini, worker tidak mengunduh maupun menyinkronkan store, dan sesi yang melampirkan memory store akan berjalan tanpa store tersebut meskipun prompt sistemnya masih mendeskripsikannya. Karena itu, nonaktifkan dukungan memori hanya pada worker yang sesinya tidak melampirkan memory store. Selama dukungan memori aktif, work item yang datang tanpa secret per-sesi untuk sesi yang melampirkan store akan gagal, alih-alih berjalan tanpa memori (lihat Pecahkan masalah mount memori).
  • memory_sync_deletions (typescript: memorySyncDeletions; go: MemorySyncDeletions): apakah file yang dihapus agen secara lokal juga dihapus dari store. Di Python dan TypeScript, nilainya salah satu dari "enabled" (default), "log_only", atau "disabled". Di Go, nilainya salah satu konstanta environments.MemorySyncDeletionsEnabled (nilai nol), environments.MemorySyncDeletionsLogOnly, atau environments.MemorySyncDeletionsDisabled. Jika diaktifkan, worker menghapus memori dari store setelah sinkronisasi berikutnya mengonfirmasi bahwa file tersebut masih tidak ada. Dalam mode log-only, worker menjalankan pemeriksaan yang sama tetapi hanya mencatat apa yang akan dihapusnya, sehingga Anda dapat memantau apa yang akan dihapus worker sebelum memercayai mode enabled. Jika dinonaktifkan, worker tidak pernah menghapus apa pun dari store. Pengaturan ini tidak memengaruhi unggahan dan unduhan.

Tetapkan opsi-opsi ini di tempat Anda membuat worker, baik melalui konstruktor EnvironmentWorker maupun, di Python dan TypeScript, melalui factory client.beta.environments.work.worker() yang digunakan webhook handler.

Misalnya, untuk menyinkronkan setiap 10 detik dan hanya mencatat penghapusan yang akan dilakukan worker:

python
  worker = EnvironmentWorker(
      client,
      environment_id=environment_id,
      environment_key=environment_key,
      workdir="/workspace",
      memory_sync_interval=10,  # seconds
      memory_sync_deletions="log_only",
  )
typescript
  const worker = new EnvironmentWorker({
    client,
    environmentId,
    environmentKey,
    workdir: "/workspace",
    memorySyncIntervalMs: 10_000,
    memorySyncDeletions: "log_only"
  });
csharp
  // EnvironmentWorker saat ini belum tersedia di SDK C#.
go
  worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  	EnvironmentID:       environmentID,
  	EnvironmentKey:      environmentKey,
  	Workdir:             "/workspace",
  	MemorySyncInterval:  10 * time.Second,
  	MemorySyncDeletions: environments.MemorySyncDeletionsLogOnly,
  })
java
  // EnvironmentWorker saat ini belum tersedia di Java SDK.
php
  // EnvironmentWorker saat ini belum tersedia di PHP SDK.
ruby
  # EnvironmentWorker saat ini belum tersedia di Ruby SDK.

Store read-only dan konflik

Untuk store yang dilampirkan dengan access: "read_only", alat write dan edit menolak mengubah file di dalam direktorinya, dan worker tidak pernah mengunggah apa pun darinya. Perubahan yang dibuat melalui bash, atau melalui alat kustom atau server MCP yang Anda layani dari sandbox, tidak diblokir secara lokal: perubahan tersebut tidak pernah disinkronkan ke store, dan perubahan remote berikutnya pada memori itu akan menimpanya. Jika Anda memerlukan salinan lokal itu sendiri tetap tidak berubah selama sesi, nonaktifkan alat bash untuk agen tersebut dan jangan berikan alat kustom yang menulis ke filesystem sandbox; jangan me-mount path store sebagai read-only, karena worker itu sendiri harus membuat direktori dan menulis memori yang diunduh ke dalamnya.

Konflik diselesaikan dengan memenangkan store. Ketika agen mengubah file memori yang juga berubah di store sejak sesi terakhir menyinkronkannya, worker mempertahankan versi store pada sinkronisasi berikutnya, menimpa file lokal dengannya, dan mencatat peringatan ke log; alat write dan edit itu sendiri berhasil dan tidak ada error yang sampai ke agen. Jika perubahan agen masih berlaku, agen dapat membaca ulang file setelah sinkronisasi dan membuat perubahan itu lagi.

Pecahkan masalah mount memori

Worker mencatat kegagalan mount dan sinkronisasi latar belakang ke log alih-alih melaporkannya ke sesi; hanya penolakan read-only yang sampai ke agen, sebagai error alat (lihat Store read-only dan konflik). Jika memory store tidak dapat di-mount ketika worker mengklaim sesi, worker menggagalkan work item: sesi tidak memancarkan event error dan tetap idle.

GejalaPenyebabPerbaikan
Log worker berisi the work item carried no sessions token (di Go, error ErrSessionMemoryNoToken) dan work item gagal.secret per-sesi milik work item tidak sampai ke worker: memory store pada sandbox self-hosted tidak diaktifkan untuk organisasi Anda, atau skrip spawn Anda tidak meneruskan secret ke dalam sandbox.Dalam pola sandbox-per-sesi, teruskan JUGLOW_WORK_SECRET ke dalam sandbox seperti yang ditunjukkan di Jalankan satu sandbox per sesi. Jika worker melakukan polling dan menjalankan sesi dalam satu proses dan masih mencatat ini ke log, hubungi dukungan.
Log worker berisi something already exists at the memory store's path.Direktori sisa dari sesi sebelumnya, biasanya sesi yang worker-nya dimatikan paksa sebelum teardown-nya berjalan.Hapus direktori sisa yang disebutkan oleh baris log. Edit di dalamnya yang belum tersinkronisasi akan hilang.
Log worker berisi cannot create the memory store's folder dan the worker host must make this mount path writable.Pengguna yang menjalankan worker tidak dapat membuat direktori di bawah /mnt/memory.Buat /mnt/memory dan chown ke pengguna tersebut; lihat Menyiapkan host.
Sesi berada dalam status idle dengan stop reason requires_action dan tanpa event error tak lama setelah worker mengklaimnya.Worker menggagalkan work item karena tidak dapat me-mount memory store, karena salah satu alasan sebelumnya.Perbaiki penyebabnya di host, lalu kirim event user.interrupt: work sesi diantrekan lagi dan worker berikutnya yang mengklaimnya mencoba ulang mount tersebut.

Layani alat kustom dari sandbox Anda

Alat kustom adalah alat yang dieksekusi oleh kode Anda sendiri. Agen memancarkan event agent.custom_tool_use dan menunggu user.custom_tool_result yang sesuai. Worker dapat berperan sebagai kode tersebut. Karena worker berjalan di dalam sandbox Anda, alat tersebut dapat menjangkau layanan internal, kredensial, dan network egress yang Anda konfigurasikan untuk sandbox, dan tidak lebih dari itu. Kunci environment memberikan otorisasi untuk mengirim hasil alat kustom, sehingga kunci API Haijun Anda tetap berada di luar host worker.

Note: Melayani alat kustom memerlukan worker SDK, karena worker CLI ant tidak dapat mendaftarkan implementasi alat kustom. Dalam pola sandbox-per-sesi, jalankan EnvironmentWorker di dalam sandbox dengan handle_item() (typescript: handleItem(); go: HandleItem()) sebagai pengganti ant beta:worker run.

  1. Deklarasikan alat pada agen

Tambahkan entri custom ke tools milik agen dengan name yang cocok dengan alat yang didaftarkan worker Anda. Lihat Alat kustom untuk bentuk deklarasi lengkapnya.

json
{
  "type": "custom",
  "name": "get_order_status",
  "description": "Look up an order in the internal fulfillment system by order ID.",
  "input_schema": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "description": "The order ID" }
    },
    "required": ["order_id"]
  }
}
  1. Daftarkan implementasi ke worker

Teruskan alat melalui factory tools (go: ToolsFunc) milik worker (lihat Helper SDK), bersama dengan toolset bawaan:

python
  import asyncio
  import os
  from juglow import AsyncJuglow, beta_async_tool
  from juglow.lib.environments import EnvironmentWorker
  from juglow.lib.tools.agent_toolset import beta_agent_toolset_20260401

  @beta_async_tool
  async def get_order_status(order_id: str) -> str:
      """Look up an order in the internal fulfillment system by order ID."""
      # Berjalan di host worker: dapat memanggil apa pun yang bisa dijangkau sandbox.
      return f"Order {order_id}: shipped"

  async def main() -> None:
      environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
      environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
      async with AsyncJuglow(auth_token=environment_key) as client:
          await EnvironmentWorker(
              client,
              environment_id=environment_id,
              environment_key=environment_key,
              workdir="/workspace",
              tools=lambda env: [*beta_agent_toolset_20260401(env), get_order_status],
          ).run()

  asyncio.run(main())
typescript
  import Juglow from "@juglow-ai/sdk";
  import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";
  import { betaTool } from "@juglow-ai/sdk/helpers/beta/json-schema";
  import { betaAgentToolset20260401 } from "@juglow-ai/sdk/tools/agent-toolset/node";

  const getOrderStatus = betaTool({
    name: "get_order_status",
    description: "Look up an order in the internal fulfillment system by order ID.",
    inputSchema: {
      type: "object",
      properties: { order_id: { type: "string", description: "The order ID" } },
      required: ["order_id"]
    },
    // Berjalan di host worker: panggil apa pun yang dapat dijangkau sandbox.
    run: async ({ order_id }) => `Order ${order_id}: shipped`
  });

  const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
  const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
  const client = new Juglow({ authToken: environmentKey });
  const controller = new AbortController();
  process.once("SIGTERM", () => controller.abort());

  await new EnvironmentWorker({
    client,
    environmentId,
    environmentKey,
    workdir: "/workspace",
    signal: controller.signal,
    tools: (ctx) => [...betaAgentToolset20260401(ctx), getOrderStatus]
  }).run();
csharp
  // EnvironmentWorker saat ini belum tersedia di SDK C#.
  // Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.
go
  package main

  import (
  	"context"
  	"log"
  	"os"
  	"os/signal"
  	"syscall"

  	"github.com/juglows/juglow-sdk-go"
  	"github.com/juglows/juglow-sdk-go/lib/environments"
  	"github.com/juglows/juglow-sdk-go/option"
  	"github.com/juglows/juglow-sdk-go/toolrunner"
  	"github.com/juglows/juglow-sdk-go/tools/agenttoolset"
  )

  type orderStatusInput struct {
  	OrderID string `json:"order_id"`
  }

  func main() {
  	environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")
  	environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")

  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  	defer stop()

  	getOrderStatus := toolrunner.NewBetaTool(
  		"get_order_status",
  		"Look up an order in the internal fulfillment system by order ID.",
  		juglow.BetaToolInputSchemaParam{
  			Properties: map[string]any{
  				"order_id": map[string]any{"type": "string", "description": "The order ID"},
  			},
  			Required: []string{"order_id"},
  		},
  		// Berjalan di host worker: dapat memanggil apa pun yang bisa dijangkau sandbox.
  		func(ctx context.Context, input orderStatusInput) (juglow.BetaToolResultBlockParamContentUnion, error) {
  			return juglow.BetaToolResultBlockParamContentUnion{
  				OfText: &juglow.BetaTextBlockParam{Text: "Order " + input.OrderID + ": shipped"},
  			}, nil
  		},
  	)

  	client := juglow.NewClient(option.WithAuthToken(environmentKey))

  	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  		EnvironmentID:  environmentID,
  		EnvironmentKey: environmentKey,
  		Workdir:        "/workspace",
  		ToolsFunc: func(env *agenttoolset.AgentToolContext) []juglow.BetaTool {
  			return append(agenttoolset.BetaAgentToolset20260401(env), getOrderStatus)
  		},
  	})
  	if err := worker.Run(ctx); err != nil {
  		log.Fatalf("worker: %v", err)
  	}
  }
java
  // EnvironmentWorker saat ini belum tersedia di Java SDK.
  // Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.
php
  // EnvironmentWorker saat ini belum tersedia di PHP SDK.
  // Untuk menjawab panggilan alat kustom secara langsung, lihat event stream sesi.
ruby
  # EnvironmentWorker saat ini belum tersedia di Ruby SDK.
  # Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.

Worker hanya menjawab alat yang didaftarkan kepadanya. Alat kustom yang dideklarasikan pada agen tetapi tidak didaftarkan ke worker atau klien mana pun akan membuat sesi terjeda dengan stop reason requires_action sampai ada yang mengirimkan hasilnya. Lihat Menangani panggilan alat kustom untuk alur event-nya.

Bungkus server MCP sebagai alat kustom

Konektor MCP terhubung ke server MCP dari sisi Juglow, sehingga server harus mengekspos endpoint HTTP yang dapat dijangkau Juglow, baik secara langsung maupun melalui tunnel MCP. Untuk menggunakan server yang hanya dapat dijangkau dari jaringan Anda, jadikan worker sebagai klien MCP dan deklarasikan alat-alat server tersebut sebagai alat kustom. Server MCP tidak memerlukan konektivitas masuk dari luar jaringan Anda. Juglow menerima definisi alat yang Anda deklarasikan pada agen, input setiap panggilan, dan hasil yang dikirim kembali oleh worker Anda. Saat runtime, model memanggil alat yang dibungkus seperti alat kustom lainnya:

  1. Agen memancarkan event agent.custom_tool_use.
  1. Worker, di dalam sandbox Anda, meneruskan panggilan melalui sesi MCP yang terbuka ke server di jaringan Anda.
  1. Worker mengirimkan respons server sebagai user.custom_tool_result.

Helper MCP sisi klien dari SDK mengonversi alat-alat server menjadi alat yang dapat dijalankan dan diterima oleh worker. Instal SDK MCP bersama SDK Juglow (pip install "juglow[mcp]" "mcp>=1.24", npm install @modelcontextprotocol/sdk, go get github.com/modelcontextprotocol/go-sdk). Contoh-contoh ini terhubung tanpa autentikasi. Untuk mengirim kredensial, konfigurasikan klien HTTP atau opsi permintaan yang Anda berikan ke transport MCP (http_client (typescript: requestInit; go: HTTPClient)).

  1. Deklarasikan alat-alat server pada agen

Ambil daftar alat-alat server MCP dan deklarasikan masing-masing sebagai alat custom. name, description, dan inputSchema dari MCP dipetakan satu per satu ke field alat kustom. Jika server membagi daftar alatnya ke beberapa halaman (paginasi), deklarasikan setiap halaman, dan worker harus mengambil daftar dari halaman yang sama.

python
  import asyncio
  from typing import Any, cast
  from juglow import AsyncJuglow
  from juglow.types.beta import BetaManagedAgentsCustomToolParams
  from mcp import ClientSession, types
  # Memerlukan mcp >= 1.24, yang mengganti nama streamablehttp_client menjadi streamable_http_client.
  from mcp.client.streamable_http import streamable_http_client

  MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"

  def to_custom_tool(tool: types.Tool) -> BetaManagedAgentsCustomToolParams:
      # Field MCP dipetakan satu-ke-satu ke deklarasi alat kustom. Cast ini
      # meneruskan dictionary skema ke parameter bertipe milik SDK tanpa perubahan.
      return {
          "type": "custom",
          "name": tool.name,
          "description": tool.description or tool.name,
          "input_schema": cast(Any, tool.inputSchema),
      }

  async def main() -> None:
      # Jalankan ini di tempat Anda membuat agen, bukan di host worker: skrip ini
      # melakukan autentikasi dengan kunci API Haijun Anda (JUGLOW_API_KEY).
      async with (
          streamable_http_client(MCP_SERVER_URL) as (read, write, _),
          ClientSession(read, write) as mcp_session,
          AsyncJuglow() as client,
      ):
          await mcp_session.initialize()
          listed = await mcp_session.list_tools()
          agent = await client.beta.agents.create(
              name="Internal tools agent",
              model="haijun-opus-5-5",
              tools=[
                  {"type": "agent_toolset_20260401"},
                  *[to_custom_tool(tool) for tool in listed.tools],
              ],
          )
          print(agent.id)

  asyncio.run(main())
typescript
  import Juglow from "@juglow-ai/sdk";
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

  const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp";

  // Jalankan ini di tempat Anda membuat agen, bukan di host worker: kode ini
  // melakukan autentikasi dengan kunci API Haijun Anda (JUGLOW_API_KEY).
  const client = new Juglow();

  const mcpClient = new Client({ name: "declare-agent-tools", version: "1.0.0" });
  await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL)));
  const { tools } = await mcpClient.listTools();

  const agent = await client.beta.agents.create({
    name: "Internal tools agent",
    model: "haijun-opus-5-5",
    tools: [
      { type: "agent_toolset_20260401" },
      // Field MCP dipetakan satu-ke-satu ke deklarasi alat kustom.
      ...tools.map((tool) => ({
        type: "custom" as const,
        name: tool.name,
        description: tool.description || tool.name,
        input_schema: tool.inputSchema
      }))
    ]
  });
  console.log(agent.id);

  await mcpClient.close();
csharp
  // Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
  // C# bekerja dengan cara yang sama setelah Anda mencantumkan alat server dengan klien MCP.
go
  package main

  import (
  	"context"
  	"encoding/json"
  	"fmt"
  	"log"

  	"github.com/juglows/juglow-sdk-go"
  	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
  )

  const mcpServerURL = "http://mcp.internal.example.com:8000/mcp"

  // toCustomTool memetakan satu definisi alat MCP ke deklarasi alat kustom.
  // Field-nya dipetakan satu ke satu: parameter bertipe membawa `properties` dan
  // `required`, dan setiap kata kunci JSON Schema lain yang dikeluarkan server disalurkan di
  // ExtraFields sehingga skema yang dideklarasikan cocok dengan skema server.
  func toCustomTool(tool *mcpsdk.Tool) (juglow.BetaAgentNewParamsToolUnion, error) {
  	raw, err := json.Marshal(tool.InputSchema)
  	if err != nil {
  		return juglow.BetaAgentNewParamsToolUnion{}, err
  	}
  	var schema map[string]any
  	if err := json.Unmarshal(raw, &schema); err != nil {
  		return juglow.BetaAgentNewParamsToolUnion{}, err
  	}

  	inputSchema := juglow.BetaManagedAgentsCustomToolInputSchemaParam{ExtraFields: map[string]any{}}
  	for keyword, value := range schema {
  		switch keyword {
  		case "type":
  			// Tipe parameter selalu di-marshal sebagai "type": "object".
  		case "properties":
  			properties, _ := value.(map[string]any)
  			inputSchema.Properties = properties
  		case "required":
  			entries, _ := value.([]any)
  			for _, entry := range entries {
  				if name, isString := entry.(string); isString {
  					inputSchema.Required = append(inputSchema.Required, name)
  				}
  			}
  		default:
  			inputSchema.ExtraFields[keyword] = value
  		}
  	}

  	description := tool.Description
  	if description == "" {
  		description = tool.Name
  	}
  	return juglow.BetaAgentNewParamsToolUnion{
  		OfCustom: &juglow.BetaManagedAgentsCustomToolParams{
  			Type:        juglow.BetaManagedAgentsCustomToolParamsTypeCustom,
  			Name:        tool.Name,
  			Description: description,
  			InputSchema: inputSchema,
  		},
  	}, nil
  }

  func main() {
  	ctx := context.Background()

  	// Jalankan ini di mana pun Anda membuat agen, bukan di host worker: ini
  	// mengautentikasi dengan kunci API Haijun Anda (JUGLOW_API_KEY).
  	client := juglow.NewClient()

  	mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "declare-agent-tools", Version: "1.0.0"}, nil)
  	session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil)
  	if err != nil {
  		log.Fatalf("connect to MCP server: %v", err)
  	}
  	defer session.Close()

  	listed, err := session.ListTools(ctx, nil)
  	if err != nil {
  		log.Fatalf("list MCP tools: %v", err)
  	}

  	tools := []juglow.BetaAgentNewParamsToolUnion{
  		{OfAgentToolset20260401: &juglow.BetaManagedAgentsAgentToolset20260401Params{
  			Type: juglow.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
  		}},
  	}
  	for _, tool := range listed.Tools {
  		custom, err := toCustomTool(tool)
  		if err != nil {
  			log.Fatalf("convert MCP tool %s: %v", tool.Name, err)
  		}
  		tools = append(tools, custom)
  	}

  	agent, err := client.Beta.Agents.New(ctx, juglow.BetaAgentNewParams{
  		Name:  "Internal tools agent",
  		Model: juglow.BetaManagedAgentsModelConfigParams{ID: juglow.BetaManagedAgentsModelHaijunOpus5_5},
  		Tools: tools,
  	})
  	if err != nil {
  		log.Fatalf("create agent: %v", err)
  	}
  	fmt.Println(agent.ID)
  }
java
  // Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
  // Java bekerja dengan cara yang sama setelah Anda mencantumkan alat server dengan klien MCP.
php
  // Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
  // PHP bekerja dengan cara yang sama setelah Anda mencantumkan alat server dengan klien MCP.
ruby
  # Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
  # Ruby bekerja dengan cara yang sama setelah Anda mendaftar alat server dengan klien MCP.
  1. Layani alat-alat dari worker

Hubungkan ke server MCP yang sama saat startup, konversi alat-alatnya dengan async_mcp_tool (python; typescript: mcpTools; go: mcp.NewBetaTools), lalu daftarkan bersama beta_agent_toolset_20260401 (python; typescript: betaAgentToolset20260401; go: agenttoolset.BetaAgentToolset20260401). Pertahankan satu sesi MCP tetap terbuka selama worker berjalan.

python
  import asyncio
  import os
  from datetime import timedelta
  from juglow import AsyncJuglow
  from juglow.lib.environments import EnvironmentWorker
  from juglow.lib.tools.agent_toolset import beta_agent_toolset_20260401
  from juglow.lib.tools.mcp import async_mcp_tool
  from mcp import ClientSession
  # Memerlukan mcp >= 1.24, yang mengganti nama streamablehttp_client menjadi streamable_http_client.
  from mcp.client.streamable_http import streamable_http_client

  MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"

  async def main() -> None:
      environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
      environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
      # Hubungkan ke server MCP sekali saat startup dan biarkan sesi tetap terbuka selama
      # masa hidup worker. Timeout mengubah panggilan alat yang macet menjadi hasil
      # error alih-alih panggilan yang terhenti.
      async with (
          streamable_http_client(MCP_SERVER_URL) as (read, write, _),
          ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as mcp_session,
          AsyncJuglow(auth_token=environment_key) as client,
      ):
          await mcp_session.initialize()
          listed = await mcp_session.list_tools()
          mcp_tools = [async_mcp_tool(tool, mcp_session) for tool in listed.tools]
          await EnvironmentWorker(
              client,
              environment_id=environment_id,
              environment_key=environment_key,
              workdir="/workspace",
              tools=lambda env: [*beta_agent_toolset_20260401(env), *mcp_tools],
          ).run()

  asyncio.run(main())
typescript
  import Juglow from "@juglow-ai/sdk";
  import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";
  import {
    mcpTools,
    type MCPCallToolResultLike,
    type MCPClientLike
  } from "@juglow-ai/sdk/helpers/beta/mcp";
  import { betaAgentToolset20260401 } from "@juglow-ai/sdk/tools/agent-toolset/node";
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

  const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp";

  const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
  const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
  const client = new Juglow({ authToken: environmentKey });
  const controller = new AbortController();
  process.once("SIGTERM", () => controller.abort());

  // Hubungkan ke server MCP sekali saat startup dan pertahankan koneksi tetap terbuka
  // selama worker berjalan.
  const mcpClient = new Client({ name: "sandbox-worker", version: "1.0.0" });
  await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL)));
  const { tools } = await mcpClient.listTools();

  // Tipe kembalian callTool dari MCP SDK masih menyertakan bentuk hasil lawas yang
  // tidak diterima mcpTools; persempit tipenya. Hapus ini setelah MCPClientLike diperluas.
  const mcpClientForTools: MCPClientLike = {
    callTool: (params) => mcpClient.callTool(params) as Promise<MCPCallToolResultLike>
  };

  await new EnvironmentWorker({
    client,
    environmentId,
    environmentKey,
    workdir: "/workspace",
    signal: controller.signal,
    tools: (ctx) => [...betaAgentToolset20260401(ctx), ...mcpTools(tools, mcpClientForTools)]
  }).run();
csharp
  // EnvironmentWorker saat ini belum tersedia di SDK C#.
go
  package main

  import (
  	"context"
  	"log"
  	"os"
  	"os/signal"
  	"syscall"

  	"github.com/juglows/juglow-sdk-go"
  	"github.com/juglows/juglow-sdk-go/lib/environments"
  	"github.com/juglows/juglow-sdk-go/mcp"
  	"github.com/juglows/juglow-sdk-go/option"
  	"github.com/juglows/juglow-sdk-go/tools/agenttoolset"
  	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
  )

  const mcpServerURL = "http://mcp.internal.example.com:8000/mcp"

  func main() {
  	environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")
  	environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")

  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  	defer stop()

  	client := juglow.NewClient(option.WithAuthToken(environmentKey))

  	// Hubungkan ke server MCP sekali saat startup dan biarkan sesi tetap terbuka selama
  	// worker berjalan.
  	mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "sandbox-worker", Version: "1.0.0"}, nil)
  	session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil)
  	if err != nil {
  		log.Fatalf("connect to MCP server: %v", err)
  	}
  	defer session.Close()

  	listed, err := session.ListTools(ctx, nil)
  	if err != nil {
  		log.Fatalf("list MCP tools: %v", err)
  	}
  	mcpTools, err := mcp.NewBetaTools(listed.Tools, session)
  	if err != nil {
  		log.Fatalf("convert MCP tools: %v", err)
  	}

  	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  		EnvironmentID:  environmentID,
  		EnvironmentKey: environmentKey,
  		Workdir:        "/workspace",
  		ToolsFunc: func(env *agenttoolset.AgentToolContext) []juglow.BetaTool {
  			return append(agenttoolset.BetaAgentToolset20260401(env), mcpTools...)
  		},
  	})
  	if err := worker.Run(ctx); err != nil {
  		log.Fatalf("worker: %v", err)
  	}
  }
java
  // EnvironmentWorker saat ini belum tersedia di Java SDK.
php
  // EnvironmentWorker saat ini belum tersedia di PHP SDK.
ruby
  # EnvironmentWorker saat ini belum tersedia di Ruby SDK.

Perhatikan hal-hal berikut saat Anda membungkus server MCP:

  • Alat dideklarasikan, bukan ditemukan saat runtime. Worker mengambil daftar alat-alat server MCP satu kali saat startup dan tidak dapat menambahkan alat ke sesi yang sedang berjalan. Jika alat-alat server berubah, deklarasikan ulang alat tersebut, baik pada agen maupun pada sesi yang idle melalui Memperbarui konfigurasi agen, lalu mulai ulang worker.
  • Nama dan deskripsi harus sesuai dengan Managed Agents API. Nama alat kustom harus unik per agen dan hanya menggunakan huruf, angka, garis bawah, dan tanda hubung (1–128 karakter). Deskripsi yang tidak kosong wajib diisi. Array tools milik agen menampung paling banyak 128 entri (setiap alat yang dibungkus dihitung satu entri, dan toolset bawaan dihitung satu entri lagi). API menolak deklarasi yang menggunakan ulang nama alat, menamai alat kustom dengan nama alat agen bawaan seperti bash atau read, atau menggunakan prefiks mcp__ yang dicadangkan. Helper MCP mempertahankan nama dan deskripsi dari server, jadi ganti nama atau pangkas jika diperlukan. Jika dua server mengekspos nama alat yang sama, definisikan sendiri pembungkusnya dengan nama berprefiks dan buat pembungkus itu memanggil nama alat asli di server.
  • Sebagian besar skema diteruskan tanpa perubahan. API menerima kata kunci JSON Schema yang umum dihasilkan server MCP, seperti additionalProperties dan title. API menolak kata kunci referensi seperti $ref di bagian mana pun dari input_schema alat kustom, jadi jadikan inline skema yang dipisahkan ke $defs oleh generator seperti pydantic. API juga menolak oneOf, anyOf, dan allOf di tingkat atas, serta nama properti yang mengandung karakter selain huruf, angka, garis bawah, titik, dan tanda hubung (1–64 karakter).
  • Kegagalan alat muncul sebagai hasil alat berupa error. Jika server MCP melaporkan error alat, worker mengirimkan hasil alat berupa error yang dapat ditanggapi model. Konten MCP yang tidak memiliki padanan hasil alat, seperti blok audio dan tautan sumber daya, juga muncul sebagai error. Tetapkan timeout pada klien MCP agar kegagalan terjadi lebih cepat dan lebih jelas, seperti yang dilakukan contoh worker Python dengan read_timeout_seconds. Tanpa timeout, panggilan yang macet baru menjadi hasil error ketika timeout permintaan default SDK MCP TypeScript terpicu (sekitar satu menit), atau ketika batas pengaman milik worker sendiri terpicu: sekitar dua setengah menit di Python, dan dua menit di Go, tempat worker membatalkan panggilan alat yang melebihi default 120 detik lalu mengirimkan hasil error.
  • Bungkus hanya server yang Anda operasikan atau percayai. Nama, deskripsi, dan hasil alat yang dibungkus masuk ke konteks model seperti alat lainnya. Semua itu merupakan input tidak tepercaya yang dapat memengaruhi apa yang dilakukan agen dengan alat-alat lainnya, termasuk bash di host worker. Deklarasikan hanya alat yang memang Anda maksudkan untuk digunakan agen.
  • Kebijakan izin tidak berlaku untuk alat kustom. Kebijakan izin mengatur toolset bawaan dan toolset MCP. Worker mengeksekusi setiap panggilan alat yang dibungkus yang dibuat model, jadi tempatkan langkah persetujuan apa pun di kode alat Anda sendiri.

Pemantauan dan operasi

Panggilan-panggilan ini dijalankan dari tooling pemantauan atau operasi Anda, diautentikasi dengan kunci API Haijun Anda, untuk mengamati dan mengelola armada worker. Loop klaim dan keep-alive ditangani di dalam helper worker, jadi Anda tidak memanggil endpoint tersebut secara langsung.

Warning: Endpoint ini menerima kunci API organisasi Anda atau kunci environment. Panggil dari luar host worker dengan kunci API organisasi Anda. Menetapkan JUGLOW_API_KEY di host worker mengekspos kredensial bercakupan organisasi ke panggilan alat agen.

Baca kedalaman antrean

work.stats mengembalikan status antrean untuk sebuah environment:

  • depth adalah jumlah item yang menunggu untuk diklaim. Skalakan armada worker Anda atau buat peringatan atas backlog berdasarkan nilai ini.
  • pending adalah jumlah item yang diklaim oleh worker tetapi belum di-acknowledge. Helper worker meng-acknowledge setiap item sebelum memprosesnya, sehingga nilai ini tetap mendekati nol dalam operasi normal; nilai bukan nol yang berkelanjutan berarti sebuah worker macet di antara mengklaim dan meng-acknowledge.
  • oldest_queued_at adalah timestamp item tertua yang masih berada di antrean, menunggu untuk diklaim atau sudah diklaim tetapi belum di-acknowledge, atau null ketika tidak ada.
  • workers_polling adalah jumlah worker yang telah melakukan polling dalam 30 detik terakhir. Gunakan ini untuk peringatan liveness.
bash
  curl -sS "https://haijun.my.id/v1/environments/$JUGLOW_ENVIRONMENT_ID/work/stats" \
    -H "x-api-key: $JUGLOW_API_KEY" \
    -H "juglow-beta: managed-agents-2026-04-01" \
    -H "juglow-version: 2023-06-01"
bash
  ant beta:environments:work stats --environment-id "$JUGLOW_ENVIRONMENT_ID"
python
  import os

  import juglow

  client = juglow.Juglow()

  stats = client.beta.environments.work.stats(os.environ["JUGLOW_ENVIRONMENT_ID"])
  print(f"depth={stats.depth} pending={stats.pending}")
typescript
  import Juglow from "@juglow-ai/sdk";

  const client = new Juglow();

  const stats = await client.beta.environments.work.stats(process.env.JUGLOW_ENVIRONMENT_ID!);

  console.log(`depth=${stats.depth} pending=${stats.pending}`);
csharp
  using Juglow;

  var client = new JuglowClient();

  var environmentId = Environment.GetEnvironmentVariable("JUGLOW_ENVIRONMENT_ID")!;

  var stats = await client.Beta.Environments.Work.Stats(environmentId);

  Console.WriteLine($"depth={stats.Depth} pending={stats.Pending}");
go
  package main

  import (
  	"context"
  	"fmt"
  	"os"

  	"github.com/juglows/juglow-sdk-go"
  )

  func main() {
  	client := juglow.NewClient()
  	environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")

  	stats, err := client.Beta.Environments.Work.Stats(
  		context.Background(),
  		environmentID,
  		juglow.BetaEnvironmentWorkStatsParams{},
  	)
  	if err != nil {
  		panic(err)
  	}

  	fmt.Printf("depth=%d pending=%d\n", stats.Depth, stats.Pending)
  }
java
  import com.juglow.client.JuglowClient;
  import com.juglow.client.okhttp.JuglowOkHttpClient;
  import com.juglow.models.beta.environments.work.BetaSelfHostedWorkQueueStats;

  void main() {
      JuglowClient client = JuglowOkHttpClient.fromEnv();

      BetaSelfHostedWorkQueueStats stats = client.beta()
          .environments()
          .work()
          .stats(System.getenv("JUGLOW_ENVIRONMENT_ID"));

      IO.println("depth=" + stats.depth() + " pending=" + stats.pending());
  }
php
  <?php

  use Juglow\Client;

  $client = new Client();

  $stats = $client->beta->environments->work->stats(getenv('JUGLOW_ENVIRONMENT_ID'));

  printf("depth=%d pending=%d\n", $stats->depth, $stats->pending);
ruby
  require "juglow"

  client = Juglow::Client.new

  stats = client.beta.environments.work.stats(ENV.fetch("JUGLOW_ENVIRONMENT_ID"))

  puts "depth=#{stats.depth} pending=#{stats.pending}"
text
{
  "type": "work_queue_stats",
  "depth": 0,
  "pending": 0,
  "oldest_queued_at": null,
  "workers_polling": 0
}

Hentikan sesi dengan anggun

Gunakan work.stop untuk meminta worker yang menangani sesi tertentu agar mematikannya. Secara default work item berpindah ke stopping: worker menyadarinya pada heartbeat lease berikutnya, membatalkan panggilan alat sesi yang sedang berjalan, dan mengonfirmasi pematian, dan pada titik itu work item menjadi stopped. Teruskan force: true di body permintaan (dengan CLI, teruskan --force) untuk menandai work item sebagai stopped segera alih-alih menunggu konfirmasi worker.

Karena panggilan-panggilan ini dijalankan dari tooling operasi Anda dan bukan dari host worker, JUGLOW_WORK_ID tidak ditetapkan secara otomatis. Tetapkan ke ID work item target sebelum menjalankan contoh-contoh berikut. Untuk menemukan ID work item, daftar work item milik environment melalui endpoint Environments Work.

bash
  curl -sS "https://haijun.my.id/v1/environments/$JUGLOW_ENVIRONMENT_ID/work/$JUGLOW_WORK_ID/stop" \
    -H "x-api-key: $JUGLOW_API_KEY" \
    -H "juglow-beta: managed-agents-2026-04-01" \
    -H "juglow-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{}'
bash
  ant beta:environments:work stop \
    --environment-id "$JUGLOW_ENVIRONMENT_ID" \
    --work-id "$JUGLOW_WORK_ID"
python
  import os

  import juglow

  client = juglow.Juglow()

  work = client.beta.environments.work.stop(
      os.environ["JUGLOW_WORK_ID"],
      environment_id=os.environ["JUGLOW_ENVIRONMENT_ID"],
  )
  print(work.state)
typescript
  import Juglow from "@juglow-ai/sdk";

  const client = new Juglow();

  const work = await client.beta.environments.work.stop(process.env.JUGLOW_WORK_ID!, {
    environment_id: process.env.JUGLOW_ENVIRONMENT_ID!
  });

  console.log(work.state);
csharp
  using Juglow;

  var client = new JuglowClient();

  var work = await client.Beta.Environments.Work.Stop(
      Environment.GetEnvironmentVariable("JUGLOW_WORK_ID")!,
      new()
      {
          EnvironmentID = Environment.GetEnvironmentVariable("JUGLOW_ENVIRONMENT_ID")!
      }
  );

  Console.WriteLine(work.State);
go
  package main

  import (
  	"context"
  	"fmt"
  	"os"

  	"github.com/juglows/juglow-sdk-go"
  )

  func main() {
  	client := juglow.NewClient()

  	work, err := client.Beta.Environments.Work.Stop(
  		context.Background(),
  		os.Getenv("JUGLOW_WORK_ID"),
  		juglow.BetaEnvironmentWorkStopParams{
  			EnvironmentID: os.Getenv("JUGLOW_ENVIRONMENT_ID"),
  		},
  	)
  	if err != nil {
  		panic(err)
  	}
  	fmt.Println(work.State)
  }
java
  import com.juglow.client.JuglowClient;
  import com.juglow.client.okhttp.JuglowOkHttpClient;
  import com.juglow.models.beta.environments.work.BetaSelfHostedWork;
  import com.juglow.models.beta.environments.work.BetaSelfHostedWorkStopRequest;
  import com.juglow.models.beta.environments.work.WorkStopParams;

  void main() {
      JuglowClient client = JuglowOkHttpClient.fromEnv();

      BetaSelfHostedWork work = client.beta().environments().work().stop(
          WorkStopParams.builder()
              .environmentId(System.getenv("JUGLOW_ENVIRONMENT_ID"))
              .workId(System.getenv("JUGLOW_WORK_ID"))
              .betaSelfHostedWorkStopRequest(BetaSelfHostedWorkStopRequest.builder().build())
              .build()
      );

      IO.println(work.state());
  }
php
  <?php

  use Juglow\Client;

  $client = new Client();

  $work = $client->beta->environments->work->stop(
      getenv('JUGLOW_WORK_ID'),
      environmentID: getenv('JUGLOW_ENVIRONMENT_ID'),
  );

  echo $work->state . "\n";
ruby
  require "juglow"

  client = Juglow::Client.new

  work = client.beta.environments.work.stop(
    ENV.fetch("JUGLOW_WORK_ID"),
    environment_id: ENV.fetch("JUGLOW_ENVIRONMENT_ID")
  )

  puts work.state

Langkah selanjutnya

Model tanggung jawab bersama untuk environment sandbox self-hosted.

Buat sesi untuk menjalankan agen Anda dan mulai mengeksekusi tugas.

Hubungkan Haijun dengan aman ke server MCP yang berjalan di jaringan privat Anda tanpa membuka port masuk atau mengekspos layanan ke internet publik.

On this page
Perbedaannya dengan environment cloudKapan menggabungkan dengan MCP tunnelEnvironment workerSistem file sandboxSebelum Anda mulaiMenjalankan workerHelper SDKMemverifikasi worker terhubungMemulai sesiMenggunakan memory storeCara worker menangani memoriMenyiapkan hostJalankan satu sandbox per sesiKonfigurasikan sinkronisasiStore read-only dan konflikPecahkan masalah mount memoriLayani alat kustom dari sandbox AndaBungkus server MCP sebagai alat kustomPemantauan dan operasiBaca kedalaman antreanHentikan sesi dengan anggunLangkah selanjutnya