Haijun Platform Docs
EN

Komunikasi dengan Haijun Managed Agents berbasis event. Anda mengirim event pengguna ke agen, dan menerima kembali event agen dan event sesi untuk melacak status.

Jenis event

Event mengalir dalam dua arah.

  • Event pengguna dan event sistem adalah yang Anda kirim ke agen: event user.* memulai sesi dan mengarahkannya seiring berjalannya sesi; system.message menambahkan konteks tingkat sistem yang berlaku untuk giliran yang menyertainya dan semua giliran berikutnya.
  • Event sesi, event span, dan event agen dikirim kepada Anda untuk observabilitas terhadap status sesi dan kemajuan agen Anda. Koneksi stream yang memilih ikut serta juga menerima delta event.

String jenis event sesi, span, agen, pengguna, dan sistem mengikuti konvensi penamaan {domain}.{action}. Event pratinjau delta khusus stream (event_start, event_delta) adalah pengecualiannya. Lihat Jenis event di referensi untuk katalog lengkapnya. Jenis event webhook terpisah, dan beberapa namanya berbeda dari nama di stream (misalnya, session.status_idled alih-alih session.status_idle).

Setiap event yang dipersistensi menyertakan timestamp processed_at yang ditetapkan saat event selesai diproses. Pada event yang Anda kirim, processed_at bernilai null selama event masih mengantre di belakang event sebelumnya. Pengecualiannya adalah user.define_outcome, user.custom_tool_result, dan user.tool_result, yang diproses saat diterima dan digemakan kembali dengan processed_at yang sudah terisi.

Mengintegrasikan event

Mengirim event

Kirim event user.message untuk memulai atau melanjutkan pekerjaan agen:

bash
  curl --fail-with-body -sS "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
    -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'
  {
    "events": [
      {
        "type": "user.message",
        "content": [
          {"type": "text", "text": "Analyze the performance of the sort function in utils.py"}
        ]
      }
    ]
  }
  EOF
bash
  ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
  events:
    - type: user.message
      content:
        - type: text
          text: Analyze the performance of the sort function in utils.py
  YAML
python
  client.beta.sessions.events.send(
      session.id,
      events=[
          {
              "type": "user.message",
              "content": [
                  {
                      "type": "text",
                      "text": "Analyze the performance of the sort function in utils.py",
                  },
              ],
          },
      ],
  )
typescript
  await client.beta.sessions.events.send(session.id, {
    events: [
      {
        type: "user.message",
        content: [
          {
            type: "text",
            text: "Analyze the performance of the sort function in utils.py",
          },
        ],
      },
    ],
  });
csharp
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsUserMessageEventParams
          {
              Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
              Content =
              [
                  new BetaManagedAgentsTextBlock
                  {
                      Type = BetaManagedAgentsTextBlockType.Text,
                      Text = "Analyze the performance of the sort function in utils.py",
                  },
              ],
          },
      ],
  });
go
  if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  	Events: []juglow.BetaManagedAgentsEventParamsUnion{{
  		OfUserMessage: &juglow.BetaManagedAgentsUserMessageEventParams{
  			Type: juglow.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
  			Content: []juglow.BetaManagedAgentsUserMessageEventParamsContentUnion{{
  				OfText: &juglow.BetaManagedAgentsTextBlockParam{
  					Type: juglow.BetaManagedAgentsTextBlockTypeText,
  					Text: "Analyze the performance of the sort function in utils.py",
  				},
  			}},
  		},
  	}},
  }); err != nil {
  	panic(err)
  }
java
  client.beta().sessions().events().send(
      session.id(),
      EventSendParams.builder()
          .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
              .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
              .addTextContent("Analyze the performance of the sort function in utils.py")
              .build())
          .build());
php
  $client->beta->sessions->events->send(
      $session->id,
      events: [
          [
              'type' => 'user.message',
              'content' => [
                  [
                      'type' => 'text',
                      'text' => 'Analyze the performance of the sort function in utils.py',
                  ],
              ],
          ],
      ],
  );
ruby
  client.beta.sessions.events.send_(
    session.id,
    events: [
      {
        type: "user.message",
        content: [
          {
            type: "text",
            text: "Analyze the performance of the sort function in utils.py"
          }
        ]
      }
    ]
  )

Kirim event user.interrupt untuk menghentikan agen di tengah eksekusi, lalu lanjutkan dengan event user.message untuk mengarahkannya ulang:

bash
  # Agen sedang menganalisis sebuah file...
  # Interupsi dengan arahan baru:
  curl --fail-with-body -sS "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
    -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'
  {
    "events": [
      {"type": "user.interrupt"},
      {
        "type": "user.message",
        "content": [
          {"type": "text", "text": "Instead, focus on fixing the bug in line 42."}
        ]
      }
    ]
  }
  EOF
bash
  # Agen sedang menganalisis sebuah file...
  # Interupsi dengan arahan baru:
  ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
  events:
    - type: user.interrupt
    - type: user.message
      content:
        - type: text
          text: Instead, focus on fixing the bug in line 42.
  YAML
python
  # Agen sedang menganalisis sebuah file...
  # Interupsi dengan arahan baru:
  client.beta.sessions.events.send(
      session.id,
      events=[
          {"type": "user.interrupt"},
          {
              "type": "user.message",
              "content": [
                  {
                      "type": "text",
                      "text": "Instead, focus on fixing the bug in line 42.",
                  },
              ],
          },
      ],
  )
typescript
  // Agen sedang menganalisis sebuah file...
  // Interupsi dengan arahan baru:
  await client.beta.sessions.events.send(session.id, {
    events: [
      { type: "user.interrupt" },
      {
        type: "user.message",
        content: [
          {
            type: "text",
            text: "Instead, focus on fixing the bug in line 42.",
          },
        ],
      },
    ],
  });
csharp
  // Agen sedang menganalisis sebuah file...
  // Interupsi dengan arahan baru:
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsUserInterruptEventParams
          {
              Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt,
          },
          new BetaManagedAgentsUserMessageEventParams
          {
              Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
              Content =
              [
                  new BetaManagedAgentsTextBlock
                  {
                      Type = BetaManagedAgentsTextBlockType.Text,
                      Text = "Instead, focus on fixing the bug in line 42.",
                  },
              ],
          },
      ],
  });
go
  // Agen sedang menganalisis sebuah file...
  // Interupsi dengan arahan baru:
  if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  	Events: []juglow.BetaManagedAgentsEventParamsUnion{
  		{
  			OfUserInterrupt: &juglow.BetaManagedAgentsUserInterruptEventParams{
  				Type: juglow.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt,
  			},
  		},
  		{
  			OfUserMessage: &juglow.BetaManagedAgentsUserMessageEventParams{
  				Type: juglow.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
  				Content: []juglow.BetaManagedAgentsUserMessageEventParamsContentUnion{{
  					OfText: &juglow.BetaManagedAgentsTextBlockParam{
  						Type: juglow.BetaManagedAgentsTextBlockTypeText,
  						Text: "Instead, focus on fixing the bug in line 42.",
  					},
  				}},
  			},
  		},
  	},
  }); err != nil {
  	panic(err)
  }
java
  // Agen sedang menganalisis sebuah file...
  // Interupsi dengan arahan baru:
  client.beta().sessions().events().send(
      session.id(),
      EventSendParams.builder()
          .addEvent(BetaManagedAgentsUserInterruptEventParams.builder()
              .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT)
              .build())
          .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
              .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
              .addTextContent("Instead, focus on fixing the bug in line 42.")
              .build())
          .build());
php
  // Agen sedang menganalisis sebuah file...
  // Interupsi dengan arahan baru:
  $client->beta->sessions->events->send(
      $session->id,
      events: [
          ['type' => 'user.interrupt'],
          [
              'type' => 'user.message',
              'content' => [
                  [
                      'type' => 'text',
                      'text' => 'Instead, focus on fixing the bug in line 42.',
                  ],
              ],
          ],
      ],
  );
ruby
  # Agen sedang menganalisis sebuah file...
  # Interupsi dengan arahan baru:
  client.beta.sessions.events.send_(
    session.id,
    events: [
      {type: "user.interrupt"},
      {
        type: "user.message",
        content: [
          {type: "text", text: "Instead, focus on fixing the bug in line 42."}
        ]
      }
    ]
  )

Panggilan tersebut kembali segera setelah event diantrekan, dan processed_at milik interupsi tetap null hingga agen menerapkannya. Respons model yang sedang berlangsung berhenti seketika. Interupsi dapat memerlukan waktu lebih lama untuk diterapkan saat pemanggilan alat sedang berjalan, dan sesi tetap running hingga interupsi diterapkan. Event user.interrupt kemudian muncul di stream, dan giliran yang diinterupsi berakhir dengan event session.status_idle. stop_reason-nya adalah end_turn, nilai yang sama dengan giliran yang selesai dengan sendirinya; tidak ada stop reason khusus untuk interupsi. Agen memulai giliran berikutnya dengan user.message yang Anda kirim setelah interupsi.

Streaming event

Lakukan streaming event dari sesi untuk menerima pembaruan real-time saat agen bekerja. Hanya event yang dipancarkan setelah stream dibuka yang dikirimkan, jadi buka stream sebelum mengirim event untuk menghindari race condition.

bash
  # Buka stream terlebih dahulu, lalu kirim pesan pengguna
  exec {stream}< <(
    curl --fail-with-body -sS -N \
      "https://haijun.my.id/v1/sessions/$SESSION_ID/events/stream?beta=true" \
      -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" \
      -H "accept: text/event-stream"
  )

  curl --fail-with-body -sS \
    "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
    -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 @- >/dev/null <<'EOF'
  {
    "events": [
      {
        "type": "user.message",
        "content": [{"type": "text", "text": "Summarize the repo README"}]
      }
    ]
  }
  EOF

  while IFS= read -r -u "$stream" event_line; do
    [[ $event_line == data:* ]] || continue
    event_json=${event_line#data: }
    case $(jq -r '.type' <<<"$event_json") in
      agent.message)
        jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
        ;;
      session.status_idle)
        break
        ;;
      session.error)
        printf '\n[Error: %s]\n' "$(jq -r '.error.message // "unknown"' <<<"$event_json")"
        break
        ;;
    esac
  done
  exec {stream}<&-
bash
  # Alur kerja ini tidak cocok dijadikan perintah shell sekali jalan.
  # Gunakan salah satu contoh SDK dalam grup kode ini sebagai gantinya.
python
  # Buka stream terlebih dahulu, lalu kirim pesan pengguna
  with client.beta.sessions.events.stream(session.id) as stream:
      client.beta.sessions.events.send(
          session.id,
          events=[
              {
                  "type": "user.message",
                  "content": [{"type": "text", "text": "Summarize the repo README"}],
              },
          ],
      )

      for event in stream:
          match event.type:
              case "agent.message":
                  for block in event.content:
                      if block.type == "text":
                          print(block.text, end="")
              case "session.status_idle":
                  break
              case "session.error":
                  error_message = event.error.message if event.error else "unknown"
                  print(f"\n[Error: {error_message}]")
                  break
typescript
  // Buka stream terlebih dahulu, lalu kirim pesan pengguna
  const stream = await client.beta.sessions.events.stream(session.id);
  await client.beta.sessions.events.send(session.id, {
    events: [
      {
        type: "user.message",
        content: [{ type: "text", text: "Summarize the repo README" }]
      }
    ]
  });

  events: for await (const event of stream) {
    switch (event.type) {
      case "agent.message":
        for (const block of event.content) {
          if (block.type === "text") {
            process.stdout.write(block.text);
          }
        }
        break;
      case "session.status_idle":
        break events;
      case "session.error":
        console.log(`\n[Error: ${event.error?.message ?? "unknown"}]`);
        break events;
    }
  }
csharp
  // Buka stream terlebih dahulu, lalu kirim pesan pengguna
  using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsUserMessageEventParams
          {
              Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
              Content =
              [
                  new BetaManagedAgentsTextBlock
                  {
                      Type = BetaManagedAgentsTextBlockType.Text,
                      Text = "Summarize the repo README",
                  },
              ],
          },
      ],
  });

  await foreach (var streamEvent in stream.Enumerate())
  {
      if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
      {
          foreach (var block in message.Content)
          {
              if (block.Value is BetaManagedAgentsTextBlock textBlock)
              {
                  Console.Write(textBlock.Text);
              }
          }
      }
      else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
      {
          break;
      }
      else if (streamEvent.Value is BetaManagedAgentsSessionErrorEvent error)
      {
          Console.WriteLine($"\n[Error: {error.Error?.Message ?? "unknown"}]");
          break;
      }
  }
go
  	// Buka stream terlebih dahulu, lalu kirim pesan pengguna
  	stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, juglow.BetaSessionEventStreamParams{})
  	defer stream.Close()

  	if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  		Events: []juglow.BetaManagedAgentsEventParamsUnion{{
  			OfUserMessage: &juglow.BetaManagedAgentsUserMessageEventParams{
  				Type: juglow.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
  				Content: []juglow.BetaManagedAgentsUserMessageEventParamsContentUnion{{
  					OfText: &juglow.BetaManagedAgentsTextBlockParam{
  						Type: juglow.BetaManagedAgentsTextBlockTypeText,
  						Text: "Summarize the repo README",
  					},
  				}},
  			},
  		}},
  	}); err != nil {
  		panic(err)
  	}

  events:
  	for stream.Next() {
  		switch event := stream.Current().AsAny().(type) {
  		case juglow.BetaManagedAgentsAgentMessageEvent:
  			// daftar bertipe konkret: BetaManagedAgentsTextBlock
  			for _, block := range event.Content {
  				fmt.Print(block.Text)
  			}
  		case juglow.BetaManagedAgentsSessionStatusIdleEvent:
  			break events
  		case juglow.BetaManagedAgentsSessionErrorEvent:
  			fmt.Printf("\n[Error: %s]\n", cmp.Or(event.Error.Message, "unknown"))
  			break events
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
java
  // Buka stream terlebih dahulu, lalu kirim pesan pengguna
  try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
      client.beta().sessions().events().send(
          session.id(),
          EventSendParams.builder()
              .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
                  .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
                  .addTextContent("Summarize the repo README")
                  .build())
              .build()
      );

      Iterable<BetaManagedAgentsStreamSessionEvents> events = stream.stream()::iterator;
      events:
      for (var event : events) {
          switch (event.type().value()) {
              case AGENT_MESSAGE -> event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())));
              case SESSION_STATUS_IDLE -> {
                  break events;
              }
              case SESSION_ERROR -> {
                  // Field `message` ada di semua varian error; baca dari JSON mentah.
                  var errorMessage =
                      event.asSessionError().error()._json().orElse(null) instanceof JsonObject json
                          ? json.values().get("message").asStringOrThrow()
                          : "unknown";
                  IO.println("\n[Error: " + errorMessage + "]");
                  break events;
              }
          }
      }
  }
php
  // Buka stream terlebih dahulu, lalu kirim pesan pengguna
  $stream = $client->beta->sessions->events->streamStream($session->id);
  $client->beta->sessions->events->send(
      $session->id,
      events: [
          [
              'type' => 'user.message',
              'content' => [['type' => 'text', 'text' => 'Summarize the repo README']],
          ],
      ],
  );

  foreach ($stream as $event) {
      match (true) {
          $event instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent => array_walk(
              $event->content,
              static fn ($block) => $block instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsTextBlock ? print($block->text) : null,
          ),
          $event instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsSessionErrorEvent => printf("\n[Error: %s]", $event->error?->message ?? 'unknown'),
          default => null,
      };
      if ($event->type === 'session.status_idle' || $event->type === 'session.error') {
          break;
      }
  }
  $stream->close();
ruby
  # Buka stream terlebih dahulu, lalu kirim pesan pengguna
  stream = client.beta.sessions.events.stream_events(session.id)

  client.beta.sessions.events.send_(
    session.id,
    events: [{
      type: "user.message",
      content: [{type: "text", text: "Summarize the repo README"}]
    }]
  )

  stream.each do |event|
    case event
    when Juglow::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
      event.content.each { print it.text }
    when Juglow::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
      break
    when Juglow::Beta::Sessions::BetaManagedAgentsSessionErrorEvent
      puts "\n[Error: #{event.error&.message || "unknown"}]"
      break
    else
      # abaikan tipe event lainnya
    end
  end

Untuk menyambung kembali ke sesi yang sudah ada tanpa melewatkan event:

  1. Buka stream baru.
  1. Daftarkan riwayat event lengkap untuk mengisi awal sekumpulan ID event yang sudah terlihat.
  1. Ikuti stream langsung, dengan melewati event apa pun yang sudah dikembalikan oleh daftar riwayat.
bash
  exec {stream}< <(
    curl --fail-with-body -sS -N \
      "https://haijun.my.id/v1/sessions/$SESSION_ID/events/stream?beta=true" \
      -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" \
      -H "accept: text/event-stream"
  )

  # Stream terbuka dan melakukan buffering. Tampilkan riwayat sebelum mengikuti event langsung.
  declare -A seen_event_ids
  while IFS= read -r event_id; do
    seen_event_ids[$event_id]=1
  done < <(
    curl --fail-with-body -sS \
      "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
      -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" | jq -r '.data[].id'
  )

  # Ikuti event langsung, lewati yang sudah pernah dilihat
  while IFS= read -r -u "$stream" event_line; do
    [[ $event_line == data:* ]] || continue
    event_json=${event_line#data: }
    event_id=$(jq -r '.id' <<<"$event_json")
    [[ -n ${seen_event_ids[$event_id]+seen} ]] && continue
    seen_event_ids[$event_id]=1
    case $(jq -r '.type' <<<"$event_json") in
      agent.message)
        jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
        ;;
      session.status_idle)
        break
        ;;
    esac
  done
  exec {stream}<&-
bash
  # Alur kerja ini tidak cocok dijadikan perintah shell sekali jalan.
  # Gunakan salah satu contoh SDK dalam grup kode ini sebagai gantinya.
python
  with client.beta.sessions.events.stream(session.id) as stream:
      # Stream terbuka dan sedang buffering. Tampilkan riwayat sebelum mengikuti event langsung.
      history = client.beta.sessions.events.list(session.id)
      seen_event_ids = {past_event.id for past_event in history}

      # Ikuti event langsung, lewati yang sudah pernah terlihat
      for event in stream:
          if event.type == "event_start" or event.type == "event_delta":
              # Pratinjau delta tidak diaktifkan pada koneksi ini.
              continue
          if event.id in seen_event_ids:
              continue
          seen_event_ids.add(event.id)
          match event.type:
              case "agent.message":
                  for block in event.content:
                      if block.type == "text":
                          print(block.text, end="")
              case "session.status_idle":
                  break
typescript
  const seenEventIds = new Set<string>();
  const stream = await client.beta.sessions.events.stream(session.id);

  // Stream terbuka dan melakukan buffering. Tampilkan riwayat sebelum mengikuti event langsung.
  for await (const event of client.beta.sessions.events.list(session.id)) {
    seenEventIds.add(event.id);
  }

  // Ikuti event langsung, lewati yang sudah terlihat
  tail: for await (const event of stream) {
    // Event pratinjau (event_start/event_delta) tidak membawa id tingkat atas
    if (event.type === "event_start" || event.type === "event_delta") continue;
    if (seenEventIds.has(event.id)) continue;
    seenEventIds.add(event.id);
    switch (event.type) {
      case "agent.message":
        for (const block of event.content) {
          if (block.type === "text") {
            process.stdout.write(block.text);
          }
        }
        break;
      case "session.status_idle":
        break tail;
    }
  }
csharp
  using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);

  // Stream terbuka dan sedang buffering. Tampilkan riwayat sebelum tailing live.
  HashSet<string> seenEventIds = [];
  var history = await client.Beta.Sessions.Events.List(session.ID);
  await foreach (var pastEvent in history.Paginate())
  {
      seenEventIds.Add(pastEvent.ID);
  }

  // Tail event live, lewati yang sudah pernah dilihat
  await foreach (var streamEvent in stream.Enumerate())
  {
      if (!seenEventIds.Add(streamEvent.ID))
      {
          continue;
      }
      if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
      {
          foreach (var block in message.Content)
          {
              if (block.Value is BetaManagedAgentsTextBlock textBlock)
              {
                  Console.Write(textBlock.Text);
              }
          }
      }
      else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
      {
          break;
      }
  }
go
  	stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, juglow.BetaSessionEventStreamParams{})
  	defer stream.Close()

  	// Stream terbuka dan sedang buffering. Tampilkan riwayat sebelum tailing live.
  	seenEventIDs := map[string]struct{}{}
  	history := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, juglow.BetaSessionEventListParams{})
  	for history.Next() {
  		seenEventIDs[history.Current().ID] = struct{}{}
  	}
  	if err := history.Err(); err != nil {
  		panic(err)
  	}

  	// Tail event live, lewati yang sudah pernah dilihat
  tail:
  	for stream.Next() {
  		event := stream.Current()
  		if _, seen := seenEventIDs[event.ID]; seen {
  			continue
  		}
  		seenEventIDs[event.ID] = struct{}{}
  		switch event := event.AsAny().(type) {
  		case juglow.BetaManagedAgentsAgentMessageEvent:
  			// daftar bertipe konkret: BetaManagedAgentsTextBlock
  			for _, block := range event.Content {
  				fmt.Print(block.Text)
  			}
  		case juglow.BetaManagedAgentsSessionStatusIdleEvent:
  			break tail
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
java
  try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
      // Stream terbuka dan sedang buffering. Tampilkan riwayat sebelum tailing live.
      // Setiap varian event membawa `id`; baca dari JSON mentah untuk dedup lintas varian.
      var seenEventIds = new HashSet<String>();
      for (var pastEvent : client.beta().sessions().events().list(session.id()).autoPager()) {
          if (pastEvent._json().orElseThrow() instanceof JsonObject json) {
              seenEventIds.add(json.values().get("id").asStringOrThrow());
          }
      }

      // Tail event live; Set.add mengembalikan false untuk ID yang sudah dilihat, melewati replay.
      stream.stream()
          .filter(event -> event._json().orElseThrow() instanceof JsonObject json
              && seenEventIds.add(json.values().get("id").asStringOrThrow()))
          .takeWhile(event -> !event.isSessionStatusIdle())
          .filter(BetaManagedAgentsStreamSessionEvents::isAgentMessage)
          .forEach(event -> event.asAgentMessage().content()
              .forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text()))));
  }
php
  $stream = $client->beta->sessions->events->streamStream($session->id);

  // Stream terbuka dan sedang buffering. Tampilkan riwayat sebelum mengikuti event langsung.
  $seenEventIds = [];
  foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) {
      $seenEventIds[$event->id] = true;
  }

  // Ikuti event langsung, lewati apa pun yang sudah terlihat
  foreach ($stream as $event) {
      if (isset($seenEventIds[$event->id])) {
          continue;
      }
      $seenEventIds[$event->id] = true;
      match (true) {
          $event instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent => array_walk(
              $event->content,
              static fn ($block) => $block instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsTextBlock ? print($block->text) : null,
          ),
          default => null,
      };
      if ($event->type === 'session.status_idle') {
          break;
      }
  }
  $stream->close();
ruby
  stream = client.beta.sessions.events.stream_events(session.id)

  # Stream terbuka dan melakukan buffering. Tampilkan riwayat sebelum mengikuti event langsung.
  seen_event_ids = Set.new
  client.beta.sessions.events.list(session.id).auto_paging_each { seen_event_ids << it.id }

  # Ikuti event langsung, lewati yang sudah terlihat — Set#add? mengembalikan nil untuk duplikat
  stream.each do |event|
    next unless seen_event_ids.add?(event.id)
    case event
    when Juglow::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
      event.content.each { print it.text }
    when Juglow::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
      break
    else
      # abaikan tipe event lainnya
    end
  end

Mendaftar event sebelumnya

Ambil riwayat event lengkap untuk sebuah sesi:

bash
  curl --fail-with-body -sS "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
    -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"
bash
  ant beta:sessions:events list --session-id "$SESSION_ID" --format jsonl
python
  events = client.beta.sessions.events.list(session.id)
  for event in events.data:
      print(f"[{event.type}] {event.processed_at}")
typescript
  const events = await client.beta.sessions.events.list(session.id);
  for (const event of events.data) {
    console.log(`[${event.type}] ${event.processed_at}`);
  }
csharp
  var events = await client.Beta.Sessions.Events.List(session.ID);
  foreach (var sessionEvent in events.Items)
  {
      Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
  }
go
  events, err := client.Beta.Sessions.Events.List(ctx, session.ID, juglow.BetaSessionEventListParams{})
  if err != nil {
  	panic(err)
  }
  for _, event := range events.Data {
  	fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
  }
java
  var events = client.beta().sessions().events().list(session.id());
  for (var event : events.data()) {
      var eventJson = event._json().orElseThrow().convert(JsonNode.class);
      var processedAt = eventJson.path("processed_at");
      IO.println("[" + eventJson.get("type").asText() + "] "
          + (processedAt.isTextual() ? processedAt.asText() : "null"));
  }
php
  $events = $client->beta->sessions->events->list($session->id);
  foreach ($events->data as $event) {
      $processedAt = ($event->processedAt ?? null)?->format(DATE_RFC3339) ?? 'null';
      echo "[{$event->type}] {$processedAt}\n";
  }
ruby
  events = client.beta.sessions.events.list(session.id)
  events.data.each { puts "[#{it.type}] #{it.processed_at}" }

Teruskan filter types untuk mengembalikan hanya jenis event tertentu:

bash
  curl --fail-with-body -sS "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true&types[]=agent.tool_use&types[]=agent.tool_result" \
    -H "x-api-key: $JUGLOW_API_KEY" \
    -H "juglow-version: 2023-06-01" \
    -H "juglow-beta: managed-agents-2026-04-01"
bash
  ant beta:sessions:events list --session-id "$SESSION_ID" \
    --type agent.tool_use --type agent.tool_result \
    --format jsonl
python
  events = client.beta.sessions.events.list(
      session.id,
      types=["agent.tool_use", "agent.tool_result"],
  )
  for event in events.data:
      print(f"[{event.type}] {event.processed_at}")
typescript
  const events = await client.beta.sessions.events.list(session.id, {
    types: ["agent.tool_use", "agent.tool_result"],
  });
  for (const event of events.data) {
    console.log(`[${event.type}] ${event.processed_at}`);
  }
csharp
  var events = await client.Beta.Sessions.Events.List(session.ID, new()
  {
      Types = ["agent.tool_use", "agent.tool_result"],
  });
  foreach (var sessionEvent in events.Items)
  {
      Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
  }
go
  events, err := client.Beta.Sessions.Events.List(ctx, session.ID, juglow.BetaSessionEventListParams{
  	Types: []string{"agent.tool_use", "agent.tool_result"},
  })
  if err != nil {
  	panic(err)
  }
  for _, event := range events.Data {
  	fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
  }
java
  var events = client.beta().sessions().events().list(
      session.id(),
      EventListParams.builder()
          .addType("agent.tool_use")
          .addType("agent.tool_result")
          .build());
  for (var event : events.data()) {
      event.agentToolUse().ifPresent(toolUse ->
          IO.println("[" + toolUse.type() + "] " + toolUse.processedAt()));
      event.agentToolResult().ifPresent(toolResult ->
          IO.println("[" + toolResult.type() + "] " + toolResult.processedAt()));
  }
php
  // Di PHP, teruskan tipe yang Anda inginkan pada EventListParams; lihat Juglow PHP SDK.
ruby
  events = client.beta.sessions.events.list(
    session.id,
    types: ["agent.tool_use", "agent.tool_result"]
  )
  events.data.each { puts "[#{it.type}] #{it.processed_at}" }

Delta event

Secara default, teks respons agen mencapai stream sebagai event agent.message yang di-buffer, masing-masing dipancarkan hanya setelah permintaan model yang menghasilkannya selesai. "Event deltas" (delta event) memungkinkan Anda merender teks tersebut secara bertahap, sebagai pratinjau langsung, selagi model masih menghasilkannya. Pratinjau bukanlah respons: pratinjau adalah alat bantu tampilan best-effort, dan agent.message yang di-buffer selalu menjadi catatan otoritatif. Klien yang mengabaikan pratinjau tetap menerima stream yang lengkap dan benar.

Memilih ikut serta dalam pratinjau

Pratinjau bersifat opt-in per koneksi stream. Tambahkan parameter query event_deltas[] ke stream yang Anda baca, dengan mengulanginya sekali untuk setiap jenis event yang ingin Anda pratinjau. Karena [] adalah pola glob shell, beri tanda kutip pada URL setiap kali Anda menyusun permintaan di shell; contoh-contoh di sini melakukan percent-encode pada tanda kurung siku sebagai %5B%5D, yang juga berfungsi. Kedua endpoint stream menerima parameter ini: stream tingkat sesi di GET /v1/sessions/{session_id}/events/stream, dan stream milik setiap thread sesi di GET /v1/sessions/{session_id}/threads/{thread_id}/stream. Nilai yang diterima adalah agent.message dan agent.thinking; nilai lain apa pun mengembalikan error 400, begitu pula permintaan dengan lebih dari 100 nilai. Pratinjau subagen muncul di stream thread milik subagen itu sendiri.

Ketika event yang dipratinjau dimulai, stream memancarkan event_start yang membawa jenis dan id event yang akan datang:

json
{
  "type": "event_start",
  "event": {
    "type": "agent.message",
    "id": "sevt_01abc..."
  }
}

Untuk agent.message, start tersebut diikuti oleh event event_delta yang membawa teks bertahap. Setiap delta menyebutkan event yang diperluasnya di event_id dan blok konten yang diperluasnya di delta.index:

json
{
  "type": "event_delta",
  "event_id": "sevt_01abc...",
  "delta": {
    "type": "content_delta",
    "index": 0,
    "content": {
      "type": "text",
      "text": "Here is the summary"
    }
  }
}

Ketika event agent.thinking dipratinjau, hanya event_start yang dipancarkan. Tidak ada event event_delta yang mengikuti, dan event agent.thinking yang di-buffer yang mengakhiri pratinjau tidak membawa konten thinking; event tersebut adalah sinyal kemajuan, bukan pembawa konten.

Tidak seperti event yang dipersistensi, event_start dan event_delta tidak memiliki id atau processed_at sendiri. Satu-satunya pengenal yang dibawanya adalah id dari event yang dipratinjaunya.

Note: Delta event menggunakan format wire yang berbeda dari Streaming pesan, dan perbedaan ini disengaja. agent.message yang dipratinjau mendapatkan satu event_start yang hanya diikuti oleh event event_delta. Tidak ada event start atau stop per blok konten dan tidak ada event stop untuk event yang dipratinjau itu sendiri. Jenis deltanya adalah content_delta, bukan content_block_delta. Kode akumulator yang ditulis untuk Messages API tidak dapat dipakai begitu saja tanpa perubahan.

Mengakumulasi dan merekonsiliasi

Setiap SDK yang mendukung delta event menyertakan helper akumulator yang menangani pembukuan index untuk Anda. Helper Go, Java, Ruby, dan C# juga mengunci pratinjau yang sedang diakumulasi berdasarkan id event; dengan helper Python, TypeScript, dan PHP Anda menyimpan map tersebut sendiri dan menggabungkan setiap delta ke entri untuk id-nya. Pola manual juga berfungsi di setiap bahasa ketika Anda memerlukan pembukuan kustom: terapkan pola tersebut pada jenis event yang dihasilkan.

Dalam pola manual, perlakukan pratinjau sebagai buffer sementara dan event yang di-buffer sebagai catatannya. Kunci buffer berdasarkan (event_id, index). Rekonsiliasi per permintaan model: sebuah giliran dibuka dengan satu event session.status_running, lalu pada giliran yang selesai secara normal setiap permintaan model menghasilkan, secara berurutan, span.model_request_start, event_start, event-event event_delta, agent.message yang di-buffer, dan terakhir span.model_request_end (di tab Span events). Di wire, inilah bagian yang dipratinjau dari urutan tersebut, berselang-seling dengan event ter-buffer lain milik koneksi:

text
event_start     {"event": {"type": "agent.message", "id": "sevt_01abc..."}}
event_delta     {"event_id": "sevt_01abc...", "delta": {"type": "content_delta", "index": 0, "content": {"type": "text", "text": "..."}}}
...
agent.message   {"id": "sevt_01abc...", "content": [...]}

Baris event_delta berulang sekali per fragmen teks. Proses setiap event saat tiba:

  1. Pada event_start, catat id yang diumumkan. Pengenalnya selalu selaras: event_start.event.id, setiap event_delta.event_id, dan id milik agent.message yang di-buffer adalah nilai yang sama.
  1. Pada setiap event_delta, tambahkan delta.content.text ke entri di (event_id, delta.index) dan render teks yang sedang berjalan. Delta pertama untuk suatu index membuat entri tersebut.
  1. Ketika agent.message yang di-buffer tiba, cocokkan berdasarkan id, buang pratinjau yang terakumulasi, dan render konten pesan tersebut sebagai gantinya.
  1. Pada span.model_request_end, tutup pratinjau apa pun yang belum direkonsiliasi oleh event ter-buffer-nya. Tidak ada lagi delta yang akan datang untuknya. Jika giliran mengalami error atau diinterupsi, event yang di-buffer mungkin tidak pernah tiba; span.model_request_end tetap tiba.

Jaminan yang diandalkan pola ini:

  • Menggabungkan delta-delta sebuah pratinjau dalam urutan kedatangan, dikunci berdasarkan (event_id, index), menghasilkan prefiks dari content[index].text di event yang di-buffer (sebuah prefiks, belum tentu seluruh teks, karena delta mungkin dibuang saat beban tinggi).
  • Sebuah koneksi memancarkan paling banyak satu event_start per event_id, dan event yang di-buffer adalah hal terakhir yang dikirimkan koneksi tersebut untuk id itu.
bash
  # Aktifkan pratinjau agent.message melalui event_deltas, lalu akumulasikan secara manual.
  exec {stream}< <(
    curl --fail-with-body -sS -N \
      "https://haijun.my.id/v1/sessions/$SESSION_ID/events/stream?beta=true&event_deltas%5B%5D=agent.message" \
      -H "x-api-key: $JUGLOW_API_KEY" \
      -H "juglow-version: 2023-06-01" \
      -H "juglow-beta: managed-agents-2026-04-01" \
      -H "accept: text/event-stream"
  )

  curl --fail-with-body -sS \
    "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
    -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 @- >/dev/null <<'EOF'
  {
    "events": [
      {
        "type": "user.message",
        "content": [{"type": "text", "text": "In one short sentence, describe what an event delta is."}]
      }
    ]
  }
  EOF

  # Akumulasikan delta dengan kunci (id pesan, indeks konten); agent.message
  # final membawa teks lengkap, sehingga menggantikan semua pratinjau untuk id tersebut.
  declare -A preview
  while IFS= read -r -u "$stream" event_line; do
    [[ $event_line == data:* ]] || continue
    event_json=${event_line#data: }
    case $(jq -r '.type' <<<"$event_json") in
      event_start)
        preview_id=$(jq -r '.event.id' <<<"$event_json")
        printf '[event_start id=%s]\n' "$preview_id"
        ;;
      event_delta)
        preview_key=$(jq -r '.event_id + ":" + (.delta.index | tostring)' <<<"$event_json")
        preview[$preview_key]+=$(jq -r '.delta.content.text' <<<"$event_json")
        printf '[event_delta] %s\n' "${preview[$preview_key]}"
        ;;
      agent.message)
        msg_id=$(jq -r '.id' <<<"$event_json")
        for preview_key in "${!preview[@]}"; do
          [[ $preview_key == "$msg_id":* ]] && unset "preview[$preview_key]"
        done
        printf '[agent.message id=%s] ' "$msg_id"
        jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
        printf '\n'
        ;;
      span.model_request_end)
        for preview_key in "${!preview[@]}"; do
          printf '[closing unreconciled preview for %s]\n' "${preview_key%%:*}"
        done
        preview=()
        ;;
      session.status_idle)
        break
        ;;
    esac
  done
  exec {stream}<&-
bash
  # Alur kerja ini tidak cocok dijadikan perintah shell sekali jalan.
  # Gunakan salah satu contoh SDK dalam grup kode ini sebagai gantinya.
python
  # Snapshot pratinjau, dengan kunci id event. accumulate_managed_agents_event melipat setiap
  # event_start / event_delta menjadi snapshot agent.message; agent.message
  # yang di-buffer akan menggantikannya.
  previews: dict[str, BetaManagedAgentsAgentMessageEvent] = {}

  # Aktifkan pratinjau agent.message pada koneksi ini
  with client.beta.sessions.events.stream(
      session.id, event_deltas=["agent.message"]
  ) as stream:
      client.beta.sessions.events.send(
          session.id,
          events=[
              {
                  "type": "user.message",
                  "content": [{"type": "text", "text": "Describe the repo in one sentence."}],
              },
          ],
      )

      for event in stream:
          match event.type:
              case "event_start":
                  snapshot = accumulate_managed_agents_event(None, event)
                  if snapshot is not None:
                      previews[event.event.id] = snapshot
                  print(f"event_start             {event.event.type} {event.event.id}")
              case "event_delta":
                  preview = accumulate_managed_agents_event(previews.get(event.event_id), event)
                  if preview is not None:
                      previews[event.event_id] = preview
                      text = "".join(block.text for block in preview.content)
                      print(f"event_delta             preview: {text!r}")
              case "agent.message":
                  # Event yang di-buffer adalah catatan resminya: ia menggantikan dan menutup pratinjau
                  preview = accumulate_managed_agents_event(previews.pop(event.id, None), event)
                  text = "".join(block.text for block in preview.content)
                  print(f"agent.message           {event.id} {text!r}")
              case "span.model_request_end":
                  # Tidak ada delta lagi yang akan datang. Tutup setiap pratinjau yang
                  # event buffer-nya tidak pernah tiba.
                  for event_id in previews:
                      print(f"span.model_request_end  closing preview for {event_id}")
                  previews.clear()
              case "session.status_idle":
                  break
typescript
  // Snapshot pratinjau, dikunci berdasarkan id event. `accumulateManagedAgentsEvent`
  // menggabungkan pratinjau event_start / event_delta menjadi snapshot agent.message.
  const previews = new Map<string, BetaManagedAgentsAgentMessageEvent>();

  // Aktifkan pratinjau agent.message hanya untuk koneksi ini
  const stream = await client.beta.sessions.events.stream(session.id, {
    event_deltas: ["agent.message"],
  });
  await client.beta.sessions.events.send(session.id, {
    events: [
      {
        type: "user.message",
        content: [{ type: "text", text: "Summarize the repo README" }]
      }
    ]
  });

  deltas: for await (const event of stream) {
    switch (event.type) {
      case "event_start": {
        // 1. Catat id yang diumumkan dan buka snapshot. Delta dan
        //    event yang di-buffer membawa id yang sama.
        const preview = accumulateManagedAgentsEvent(undefined, event);
        if (preview) previews.set(event.event.id, preview);
        console.log(`event_start             ${event.event.type} ${event.event.id}`);
        break;
      }
      case "event_delta": {
        // 2. Gabungkan fragmen ke dalam snapshot lalu render
        const preview = accumulateManagedAgentsEvent(previews.get(event.event_id), event);
        if (preview) {
          previews.set(event.event_id, preview);
          const text = preview.content
            .map((block) => (block.type === "text" ? block.text : ""))
            .join("");
          console.log(`event_delta             preview: ${JSON.stringify(text)}`);
        }
        break;
      }
      case "agent.message": {
        // 3. Event yang di-buffer adalah catatannya: ia menggantikan dan menutup pratinjau
        const message = accumulateManagedAgentsEvent(previews.get(event.id), event);
        previews.delete(event.id);
        const text = message.content
          .map((block) => (block.type === "text" ? block.text : ""))
          .join("");
        console.log(`agent.message           ${event.id} ${JSON.stringify(text)}`);
        break;
      }
      case "span.model_request_end":
        // 4. Tidak ada delta lagi yang akan datang. Tutup pratinjau yang belum pernah direkonsiliasi.
        for (const eventId of previews.keys()) {
          console.log(`span.model_request_end  closing preview for ${eventId}`);
        }
        previews.clear();
        break;
      case "session.status_idle":
        break deltas;
    }
  }
  stream.controller.abort();
csharp
  // Aktifkan delta event: event agent.message dipratinjau saat diproduksi.
  using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(
      session.ID,
      new() { EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] }
  );
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsUserMessageEventParams
          {
              Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
              Content =
              [
                  new BetaManagedAgentsTextBlock
                  {
                      Type = BetaManagedAgentsTextBlockType.Text,
                      Text = "Write a haiku about event streams.",
                  },
              ],
          },
      ],
  });

  // Akumulasikan fragmen pratinjau per (id event, indeks konten). Event
  // agent.message ter-buffer yang menyusul membawa konten lengkap, jadi ia
  // menggantikan pratinjau yang terakumulasi, bukan menambahkannya.
  Dictionary<string, SortedDictionary<long, string>> previews = [];

  await foreach (var streamEvent in stream.Enumerate())
  {
      if (streamEvent.TryPickStartEvent(out var start))
      {
          // Pratinjau dibuka untuk event dengan id ini. Stream ini hanya mengaktifkan
          // delta agent.message; TryPick* mengembalikan false alih-alih throw,
          // jadi tipe pratinjau lain (termasuk yang ditambahkan nanti) dilewati.
          if (start.Event.TryPickAgentMessage(out var preview))
          {
              Console.WriteLine($"event_start             {preview.Type.Raw()} {preview.ID}");
          }
      }
      else if (streamEvent.TryPickDeltaEvent(out var delta))
      {
          // Sisipkan pada indeks baru, tambahkan pada indeks yang sudah ada
          if (!previews.TryGetValue(delta.EventID, out var fragments))
          {
              previews[delta.EventID] = fragments = [];
          }
          var index = delta.Delta.Index ?? 0;
          fragments[index] = fragments.GetValueOrDefault(index, "") + delta.Delta.Content.Text;
          Console.WriteLine($"event_delta             preview: {fragments[index]}");
      }
      else if (streamEvent.TryPickAgentMessageEvent(out var message))
      {
          // Delta bersifat best-effort: buang pratinjau dan gunakan event ter-buffer
          previews.Remove(message.ID);
          var text = string.Concat(message.Content.Select(block =>
              block.TryPickBetaManagedAgentsTextBlock(out var textBlock) ? textBlock.Text : ""));
          Console.WriteLine($"agent.message           {message.ID} {text}");
      }
      else if (streamEvent.TryPickSpanModelRequestEndEvent(out _))
      {
          // Tidak ada delta lagi; tutup pratinjau yang belum pernah direkonsiliasi.
          foreach (var eventId in previews.Keys)
          {
              Console.WriteLine($"span.model_request_end  closing preview for {eventId}");
          }
          previews.Clear();
      }
      else if (streamEvent.TryPickSessionStatusIdleEvent(out _))
      {
          break;
      }
  }
go
  	// Aktifkan pratinjau inkremental untuk event agent.message
  	stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, juglow.BetaSessionEventStreamParams{
  		EventDeltas: []juglow.BetaManagedAgentsDeltaType{
  			juglow.BetaManagedAgentsDeltaTypeAgentMessage,
  		},
  	})

  	if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  		Events: []juglow.BetaManagedAgentsEventParamsUnion{{
  			OfUserMessage: &juglow.BetaManagedAgentsUserMessageEventParams{
  				Type: juglow.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
  				Content: []juglow.BetaManagedAgentsUserMessageEventParamsContentUnion{{
  					OfText: &juglow.BetaManagedAgentsTextBlockParam{
  						Type: juglow.BetaManagedAgentsTextBlockTypeText,
  						Text: "Write a haiku about the ocean.",
  					},
  				}},
  			},
  		}},
  	}); err != nil {
  		panic(err)
  	}

  	// Akumulator menggabungkan fragmen event_start / event_delta menjadi
  	// snapshot agent.message per-event-id. Nilai nol (zero value) siap digunakan.
  	var previews juglow.BetaManagedAgentsEventAccumulator

  deltas:
  	for stream.Next() {
  		event := stream.Current()
  		previews.Accumulate(event)

  		switch event := event.AsAny().(type) {
  		case juglow.BetaManagedAgentsStartEvent:
  			fmt.Printf("event_start             %s %s\n", event.Event.Type, event.Event.ID)
  		case juglow.BetaManagedAgentsDeltaEvent:
  			fmt.Printf("event_delta             preview: %q\n", previews.AgentMessageText(event.EventID))
  		case juglow.BetaManagedAgentsAgentMessageEvent:
  			// Event yang di-buffer membawa konten lengkap: akumulator
  			// mengganti pratinjau dengannya
  			fmt.Printf("agent.message           %s %q\n", event.ID, previews.AgentMessageText(event.ID))
  		case juglow.BetaManagedAgentsSpanModelRequestEndEvent:
  			// Tidak ada delta lagi untuk permintaan ini. Akumulator
  			// membuang snapshot-nya di sini, menutup pratinjau yang tidak pernah
  			// direkonsiliasi oleh agent.message yang di-buffer.
  			fmt.Println("span.model_request_end  no more deltas for this request")
  		case juglow.BetaManagedAgentsSessionStatusIdleEvent:
  			break deltas
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
  	stream.Close()
java
  // Teks pratinjau, dikunci oleh ID event lalu indeks konten. agent.message yang di-buffer menggantikannya.
  Map<String, Map<Long, StringBuilder>> previews = new HashMap<>();

  // Aktifkan pratinjau agent.message pada koneksi ini
  try (var stream = client.beta().sessions().events().streamStreaming(
          session.id(),
          EventStreamParams.builder()
              .addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE)
              .build()
  )) {
      client.beta().sessions().events().send(
          session.id(),
          EventSendParams.builder()
              .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
                  .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
                  .addTextContent("Describe the repo in one sentence.")
                  .build())
              .build()
      );

      Iterable<BetaManagedAgentsStreamSessionEvents> events = stream.stream()::iterator;
      deltas:
      for (var event : events) {
          switch (event.type().value()) {
              case EVENT_START -> {
                  if (event.asEventStart().event().isAgentMessage()) {
                      var preview = event.asEventStart().event().asAgentMessage();
                      IO.println("event_start             " + preview.type().asString() + " " + preview.id());
                  }
              }
              case EVENT_DELTA -> {
                  var eventDelta = event.asEventDelta();
                  var fragment = eventDelta.delta();
                  var buffer = previews
                      .computeIfAbsent(eventDelta.eventId(), _ -> new HashMap<>())
                      .computeIfAbsent(fragment.index().orElse(0L), _ -> new StringBuilder());
                  buffer.append(fragment.content().text());
                  IO.println("event_delta             preview: " + buffer);
              }
              case AGENT_MESSAGE -> {
                  // Event yang di-buffer adalah catatannya: buang pratinjaunya, render kontennya
                  var message = event.asAgentMessage();
                  previews.remove(message.id());
                  var text = message.content().stream()
                      .flatMap(block -> block.text().stream())
                      .map(textBlock -> textBlock.text())
                      .collect(Collectors.joining());
                  IO.println("agent.message           " + message.id() + " " + text);
              }
              case SPAN_MODEL_REQUEST_END -> {
                  // Tidak ada delta lagi yang akan datang. Tutup pratinjau yang event buffer-nya tidak pernah tiba.
                  previews.keySet().forEach(eventId ->
                      IO.println("span.model_request_end  closing preview for " + eventId));
                  previews.clear();
              }
              case SESSION_STATUS_IDLE -> {
                  break deltas;
              }
          }
      }
  }
php
  // Di PHP, atur eventDeltas pada EventStreamParams dan akumulasikan dengan Juglow\Lib\Sessions\EventAccumulator.
ruby
  # Aktifkan delta event: pratinjau agent.message di-stream sebagai fragmen inkremental.
  stream = client.beta.sessions.events.stream_events(
    session.id,
    event_deltas: [Juglow::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE]
  )

  client.beta.sessions.events.send_(
    session.id,
    events: [{
      type: "user.message",
      content: [{type: "text", text: "Give a one-sentence project tagline."}]
    }]
  )

  # Akumulasikan fragmen pratinjau berdasarkan (event_id, index) ke dalam buffer yang
  # secara eksplisit mutable (`+""`) agar `<<` bisa menambahkan di tempat. agent.message ter-buffer dengan
  # id yang sama bersifat otoritatif dan menggantikan apa pun yang dibangun oleh delta.
  buffers = Hash.new do |by_event, event_id|
    by_event[event_id] = Hash.new { |fragments, index| fragments[index] = +"" }
  end

  stream.each do |event|
    case event
    when Juglow::Beta::BetaManagedAgentsStartEvent
      puts "event_start             #{event.event.type} #{event.event.id}"
    when Juglow::Beta::BetaManagedAgentsDeltaEvent
      delta = event.delta
      fragment = delta.content.text
      buffers[event.event_id][delta.index || 0] << fragment
      puts "event_delta             preview: #{buffers[event.event_id][delta.index || 0].inspect}"
    when Juglow::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
      # Ganti: buang pratinjau yang terakumulasi dan render event lengkapnya.
      buffers.delete(event.id)
      puts "agent.message           #{event.id} #{event.content.map(&:text).join.inspect}"
    when Juglow::Beta::Sessions::BetaManagedAgentsSpanModelRequestEndEvent
      # Tidak ada delta lagi yang akan datang. Tutup pratinjau yang belum pernah direkonsiliasi.
      buffers.each_key { |event_id| puts "span.model_request_end  closing preview for #{event_id}" }
      buffers.clear
    when Juglow::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
      break
    else
      # abaikan tipe event lainnya
    end
  end

Mempratinjau event thread sesi

Dalam sesi multiagen, setiap thread sesi memiliki aliran event sendiri di GET /v1/sessions/{session_id}/threads/{thread_id}/stream, dan menerima parameter event_deltas[] yang sama dengan nilai yang sama. Pratinjau dibatasi per thread secara desain: sebuah koneksi hanya mempratinjau thread yang sedang dibacanya. Pratinjau thread anak dikirimkan di stream milik anak itu sendiri dan tidak pernah diposting silang ke stream tingkat sesi, yang pratinjaunya tetap terbatas pada thread utama. Untuk melihat teks subagen saat model menghasilkannya, buka stream thread subagen tersebut.

Path stream thread mudah keliru: path-nya adalah /threads/{thread_id}/stream, bukan /events/stream (yang hanya ada di tingkat sesi), dan tidak ada endpoint /threads/{thread_id}/events/stream.

Event pratinjaunya sendiri tidak berubah. event_start dan event_delta memiliki bentuk yang sama di stream thread seperti di stream tingkat sesi, dan pola mengakumulasi dan merekonsiliasi berlaku sebagaimana tertulis. Satu-satunya penyesuaian adalah pembukuan: jalankan satu instance akumulator per koneksi stream.

bash
  # Tampilkan daftar thread sesi dan pilih satu anak: thread anak memiliki
  # parent_thread_id yang tidak null, dan parent_thread_id thread utama bernilai null.
  THREAD_ID=$(
    curl --fail-with-body -sS \
      "https://haijun.my.id/v1/sessions/$SESSION_ID/threads?beta=true" \
      -H "x-api-key: $JUGLOW_API_KEY" \
      -H "juglow-version: 2023-06-01" \
      -H "juglow-beta: managed-agents-2026-04-01" |
      jq -er 'first(.data[] | select(.parent_thread_id != null)).id'
  )

  # Stream thread anak menerima parameter event_deltas[] yang sama dengan
  # stream sesi. Lakukan percent-encode pada tanda kurung (%5B%5D) dan beri tanda kutip pada URL.
  exec {stream}< <(
    curl --fail-with-body -sS -N \
      "https://haijun.my.id/v1/sessions/$SESSION_ID/threads/$THREAD_ID/stream?beta=true&event_deltas%5B%5D=agent.message" \
      -H "x-api-key: $JUGLOW_API_KEY" \
      -H "juglow-version: 2023-06-01" \
      -H "juglow-beta: managed-agents-2026-04-01" \
      -H "accept: text/event-stream"
  )

  while IFS= read -r -u "$stream" event_line; do
    [[ $event_line == data:* ]] || continue
    event_json=${event_line#data: }
    case $(jq -r '.type' <<<"$event_json") in
      event_delta)
        jq -j '.delta.content.text' <<<"$event_json"
        ;;
      agent.message)
        # Event yang di-buffer adalah catatan otoritatif; render kontennya.
        printf '\n'
        jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
        printf '\n'
        ;;
      session.thread_status_idle)
        break
        ;;
    esac
  done
  exec {stream}<&-
bash
  # Daftarkan thread sesi dan pilih satu anak: thread anak membawa
  # parent_thread_id non-null, dan parent_thread_id thread utama bernilai null
  # (kueri #(parent_thread_id!=~null) milik --transform cocok dengan nilai non-null).
  THREAD_ID=$(ant beta:sessions:threads list \
    --session-id "$SESSION_ID" \
    --format raw --transform 'data.#(parent_thread_id!=~null).id' --raw-output)

  # Stream thread anak menerima parameter event_deltas yang sama dengan
  # stream sesi, satu flag --event-delta per jenis event untuk dipratinjau. @tostr
  # mengodekan ulang tiap field teks sebagai string JSON, sehingga setiap nilai tetap di satu
  # baris YAML dan fromjson milik jq memulihkan teks aslinya.
  transform='{type,frag:delta.content.text|@tostr,text:content.#(type=="text").text|@tostr}'
  exec {stream}< <(ant beta:sessions:threads:events stream \
    --session-id "$SESSION_ID" \
    --thread-id "$THREAD_ID" \
    --event-delta agent.message \
    --transform "$transform" \
    --format yaml)

  type=
  while IFS= read -r -u "$stream" line; do
    case "$line" in
      type:\ session.thread_status_idle) break ;;
      type:\ *) type=${line#type: } ;;
      frag:*)
        [[ $type == event_delta ]] || continue
        jq -j fromjson <<<"${line#frag: }" ;;
      text:*)
        [[ $type == agent.message ]] || continue
        # Event yang di-buffer adalah catatan otoritatif; render kontennya.
        printf '\n'
        jq -r fromjson <<<"${line#text: }" ;;
    esac
  done
  exec {stream}<&-
python
  # Daftar thread milik sesi dan pilih satu anak: thread anak membawa parent_thread_id
  # non-null, sedangkan parent_thread_id thread utama bernilai null.
  child_thread = next(
      thread
      for thread in client.beta.sessions.threads.list(session.id)
      if thread.parent_thread_id is not None
  )

  # Stream thread anak menerima parameter event_deltas yang sama dengan
  # stream sesi.
  with client.beta.sessions.threads.events.stream(
      child_thread.id,
      session_id=session.id,
      event_deltas=["agent.message"],
  ) as stream:
      for event in stream:
          match event.type:
              case "event_delta":
                  print(event.delta.content.text, end="")
              case "agent.message":
                  # Event yang di-buffer adalah catatan otoritatif; render kontennya
                  print()
                  for block in event.content:
                      if block.type == "text":
                          print(block.text, end="")
                  print()
              case "session.thread_status_idle":
                  break
typescript
  // Tampilkan daftar thread sesi dan pilih anak: thread anak membawa parent_thread_id
  // non-null, dan parent_thread_id thread utama bernilai null.
  let childThreadId: string | undefined;
  for await (const thread of client.beta.sessions.threads.list(session.id)) {
    if (thread.parent_thread_id !== null) {
      childThreadId = thread.id;
      break;
    }
  }
  if (!childThreadId) throw new Error("No child thread found");

  // Stream thread anak menerima parameter event_deltas yang sama dengan
  // stream sesi.
  const stream = await client.beta.sessions.threads.events.stream(childThreadId, {
    session_id: session.id,
    event_deltas: ["agent.message"],
  });

  threadDeltas: for await (const event of stream) {
    switch (event.type) {
      case "event_delta":
        process.stdout.write(event.delta.content.text);
        break;
      case "agent.message": {
        // Event yang di-buffer adalah catatan otoritatif; render kontennya.
        process.stdout.write("\n");
        const text = event.content
          .map((block) => (block.type === "text" ? block.text : ""))
          .join("");
        console.log(text);
        break;
      }
      case "session.thread_status_idle":
        break threadDeltas;
    }
  }
  stream.controller.abort();
csharp
  // Daftar thread sesi dan pilih satu anak: thread anak memiliki
  // parent_thread_id non-null, dan parent_thread_id thread utama adalah null.
  var threads = await client.Beta.Sessions.Threads.List(session.ID);
  var childThread = threads.Items.First(thread => thread.ParentThreadID is not null);

  // Stream thread anak menerima parameter event_deltas yang sama seperti
  // stream sesi.
  using var stream = await client.Beta.Sessions.Threads.Events.WithRawResponse.StreamStreaming(
      childThread.ID,
      new() { SessionID = session.ID, EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] }
  );

  await foreach (var streamEvent in stream.Enumerate())
  {
      if (streamEvent.TryPickDeltaEvent(out var delta))
      {
          Console.Write(delta.Delta.Content.Text);
      }
      else if (streamEvent.TryPickAgentMessageEvent(out var message))
      {
          // Event ter-buffer adalah catatan otoritatif; render kontennya.
          Console.WriteLine();
          var text = string.Concat(message.Content.Select(block =>
              block.TryPickBetaManagedAgentsTextBlock(out var textBlock) ? textBlock.Text : ""));
          Console.WriteLine(text);
      }
      else if (streamEvent.TryPickSessionThreadStatusIdleEvent(out _))
      {
          break;
      }
  }
go
  	// Daftar thread sesi dan pilih satu anak: thread anak memiliki
  	// parent_thread_id non-null, dan parent_thread_id thread utama adalah null.
  	var childThreadID string
  	threads := client.Beta.Sessions.Threads.ListAutoPaging(ctx, session.ID, juglow.BetaSessionThreadListParams{})
  	for threads.Next() {
  		if thread := threads.Current(); thread.ParentThreadID != "" {
  			childThreadID = thread.ID
  			break
  		}
  	}
  	if err := threads.Err(); err != nil {
  		panic(err)
  	}

  	// Stream thread anak menerima parameter event_deltas yang sama seperti
  	// stream sesi; jalankan satu loop baca per koneksi stream.
  	stream := client.Beta.Sessions.Threads.Events.StreamEvents(ctx, childThreadID, juglow.BetaSessionThreadEventStreamParams{
  		SessionID: session.ID,
  		EventDeltas: []juglow.BetaManagedAgentsDeltaType{
  			juglow.BetaManagedAgentsDeltaTypeAgentMessage,
  		},
  	})

  threadDeltas:
  	for stream.Next() {
  		switch event := stream.Current().AsAny().(type) {
  		case juglow.BetaManagedAgentsDeltaEvent:
  			fmt.Print(event.Delta.Content.Text)
  		case juglow.BetaManagedAgentsAgentMessageEvent:
  			// Event yang di-buffer adalah catatan otoritatif; render kontennya.
  			fmt.Println()
  			// daftar bertipe konkret: BetaManagedAgentsTextBlock
  			for _, block := range event.Content {
  				fmt.Print(block.Text)
  			}
  			fmt.Println()
  		case juglow.BetaManagedAgentsSessionThreadStatusIdleEvent:
  			break threadDeltas
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
  	stream.Close()
java
  // Daftar thread sesi dan pilih satu child: child thread membawa parent_thread_id
  // yang non-null, dan parent_thread_id thread utama bernilai null.
  var childThread = client.beta().sessions().threads().list(session.id()).autoPager().stream()
      .filter(thread -> thread.parentThreadId().isPresent())
      .findFirst()
      .orElseThrow();

  // Stream child thread menerima parameter event_deltas yang sama dengan stream sesi.
  // Kelas params-nya berbagi nama sederhana dengan yang level sesi, jadi kualifikasikan.
  try (var stream = client.beta().sessions().threads().events().streamStreaming(
          childThread.id(),
          com.juglow.models.beta.sessions.threads.events.EventStreamParams.builder()
              .sessionId(session.id())
              .addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE)
              .build()
  )) {
      Iterable<BetaManagedAgentsStreamSessionThreadEvents> events = stream.stream()::iterator;
      threadDeltas:
      for (var event : events) {
          switch (event.type().value()) {
              case EVENT_DELTA -> IO.print(event.asEventDelta().delta().content().text());
              case AGENT_MESSAGE -> {
                  // Event yang di-buffer adalah catatan otoritatif; render kontennya.
                  IO.println();
                  event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())));
                  IO.println();
              }
              case SESSION_THREAD_STATUS_IDLE -> {
                  break threadDeltas;
              }
          }
      }
  }
php
  // Di PHP, atur eventDeltas pada EventStreamParams thread dan akumulasikan dengan Juglow\Lib\Sessions\EventAccumulator.
ruby
  # Daftarkan thread milik sesi dan pilih satu anak: thread anak memiliki
  # parent_thread_id non-null, dan parent_thread_id thread utama bernilai null.
  child_thread = client.beta.sessions.threads.list(session.id).to_enum.find { it.parent_thread_id }

  # Stream thread anak menerima parameter event_deltas yang sama dengan
  # stream sesi.
  stream = client.beta.sessions.threads.events.stream_events(
    child_thread.id,
    session_id: session.id,
    event_deltas: [Juglow::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE]
  )

  stream.each do |event|
    case event
    when Juglow::Beta::BetaManagedAgentsDeltaEvent
      print event.delta.content.text
    when Juglow::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
      # Event yang ter-buffer adalah catatan otoritatif; render kontennya.
      puts
      event.content.each { print it.text }
      puts
    when Juglow::Beta::Sessions::BetaManagedAgentsSessionThreadStatusIdleEvent
      break
    else
      # abaikan tipe event lainnya
    end
  end

Loop pembacaan keluar pada session.thread_status_idle, event yang dipancarkan ketika giliran thread sesi selesai dan thread menjadi idle.

Keterbatasan

Pratinjau disetel untuk responsivitas. Bangun dengan memperhatikan batasan berikut:

  • Best effort: Saat beban tinggi, server mungkin membuang delta untuk suatu event. Ketika itu terjadi, Anda menerima prefiks teks yang bersambung lalu tidak ada delta lebih lanjut untuk event tersebut. agent.message yang di-buffer tetap tiba secara lengkap. Jangan pernah memperlakukan pratinjau yang terakumulasi sebagai final.
  • Tidak ada replay saat menyambung kembali: Delta hanya dikirimkan ke koneksi yang memilih ikut serta, selama koneksi itu terbuka. Ini berlaku sama untuk stream tingkat sesi maupun setiap stream thread sesi, dan koneksi yang dibuka setelah permintaan model dimulai tidak menerima delta untuk event yang sedang berlangsung tersebut. Jika stream terputus, ikuti prosedur penyambungan kembali di tab Streaming event: buka kembali stream dan daftarkan riwayat event. Riwayat tersebut mencakup event ter-buffer apa pun yang dipancarkan selagi Anda terputus, termasuk agent.message yang ditunggu pratinjau Anda. Tidak ada cara untuk meminta ulang delta yang terlewat.
  • Satu thread, hanya teks: Pratinjau mencakup teks asisten pada thread yang sedang dibaca koneksi. Penggunaan alat, hasil alat, hasil MCP, dan aktivitas pada thread sesi lain mana pun tidak pernah dipratinjau pada koneksi tersebut.
  • agent.thinking hanya start: Pratinjau agent.thinking hanya memancarkan event_start sebagai sinyal bahwa blok thinking telah dimulai; tidak ada event event_delta yang mengikutinya.
  • Tidak pernah dipersistensi: event_start dan event_delta hanya ada di stream langsung. Keduanya tidak muncul di riwayat event sesi (GET /v1/sessions/{session_id}/events) atau di riwayat event thread sesi mana pun.

Memecahkan masalah pratinjau

Jika stream tidak berperilaku seperti yang Anda harapkan:

Yang Anda lihatArtinya
Stream dengan event ter-buffer tetapi tanpa event_start atau event_deltaKoneksi yang Anda baca tidak memilih ikut serta (event_deltas[] berlaku per koneksi, bukan per sesi), atau giliran tersebut tidak pernah menyentuh thread yang Anda stream. Pratinjau dibatasi per thread, jadi daftarkan thread sesi (GET /v1/sessions/{session_id}/threads) untuk menemukan thread mana yang berjalan.
404 pada URL streamPath atau salah satu ID salah, atau permintaan tidak membawa header beta managed-agents sama sekali. Endpoint thread dibatasi beta, jadi tanpa header tersebut endpoint itu tidak ada.
400 yang menyebut event_deltasHanya agent.message dan agent.thinking yang diterima.

Skenario tambahan

Menangani pemanggilan alat kustom

Ketika agen memanggil alat kustom:

  1. Sesi memancarkan event agent.custom_tool_use yang berisi nama alat dan input.
  1. Sesi berhenti sejenak dengan event session.status_idle yang berisi stop_reason: requires_action. ID event yang memblokir ada di array stop_reason.event_ids.
  1. Jalankan alat di sistem Anda dan kirim event user.custom_tool_result untuk masing-masing, dengan meneruskan ID event di parameter custom_tool_use_id bersama konten hasilnya.
  1. Setelah semua event yang memblokir terselesaikan, sesi bertransisi kembali ke running.
bash
  exec {stream_fd}< <(curl --fail-with-body -sS -N \
    "https://haijun.my.id/v1/sessions/$SESSION_ID/events/stream?beta=true" \
    -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" \
    -H "accept: text/event-stream")

  while IFS= read -r -u "$stream_fd" line; do
    [[ $line == data:* ]] || continue
    event_json="${line#data: }"
    stop_reason=$(jq -r 'select(.type == "session.status_idle") | .stop_reason.type // empty' <<<"$event_json")
    case "$stop_reason" in
      requires_action)
        while IFS= read -r event_id; do
          # Jalankan alat dan kirim hasilnya kembali
          result=$(call_tool "$event_id")
          jq -n --arg id "$event_id" --arg result "$result" \
            '{events: [{type: "user.custom_tool_result", custom_tool_use_id: $id, content: [{type: "text", text: $result}]}]}' |
            curl --fail-with-body -sS \
              "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
              -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 @-
        done < <(jq -r '.stop_reason.event_ids[]' <<<"$event_json")
        ;;
      end_turn)
        break
        ;;
    esac
  done
  exec {stream_fd}<&-
bash
  # Alur kerja ini tidak cocok diterjemahkan ke perintah shell sekali jalan.
  # Gunakan salah satu contoh SDK dalam grup kode ini sebagai gantinya.
python
  with client.beta.sessions.events.stream(session.id) as stream:
      for event in stream:
          if event.type == "session.status_idle" and (stop_reason := event.stop_reason):
              match stop_reason.type:
                  case "requires_action":
                      for event_id in stop_reason.event_ids:
                          # Cari event custom tool use dan jalankan
                          tool_event = events_by_id[event_id]
                          result = call_tool(tool_event.name, tool_event.input)

                          # Kirim hasilnya kembali
                          client.beta.sessions.events.send(
                              session.id,
                              events=[
                                  {
                                      "type": "user.custom_tool_result",
                                      "custom_tool_use_id": event_id,
                                      "content": [{"type": "text", "text": result}],
                                  },
                              ],
                          )
                  case "end_turn":
                      break
typescript
  const stream = await client.beta.sessions.events.stream(session.id);

  for await (const event of stream) {
    if (event.type !== "session.status_idle") continue;
    if (event.stop_reason.type === "end_turn") break;
    if (event.stop_reason.type !== "requires_action") continue;

    for (const eventId of event.stop_reason.event_ids) {
      // Cari event custom tool use dan jalankan
      const toolEvent = eventsById.get(eventId);
      if (!toolEvent) continue;
      const result = await callTool(toolEvent.name, toolEvent.input);

      // Kirim hasilnya kembali
      await client.beta.sessions.events.send(session.id, {
        events: [
          {
            type: "user.custom_tool_result",
            custom_tool_use_id: eventId,
            content: [{ type: "text", text: result }],
          },
        ],
      });
    }
  }
csharp
  await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID))
  {
      if (streamEvent.Value is not BetaManagedAgentsSessionStatusIdleEvent idle) continue;

      if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction)
      {
          foreach (var eventId in requiresAction.EventIds)
          {
              // Cari event penggunaan alat kustom dan jalankan
              var toolEvent = eventsById[eventId];
              var result = await CallTool(toolEvent.Name, toolEvent.Input);

              // Kirim hasilnya kembali
              await client.Beta.Sessions.Events.Send(session.ID, new()
              {
                  Events =
                  [
                      new BetaManagedAgentsUserCustomToolResultEventParams
                      {
                          Type = BetaManagedAgentsUserCustomToolResultEventParamsType.UserCustomToolResult,
                          CustomToolUseID = eventId,
                          Content =
                          [
                              new BetaManagedAgentsTextBlock
                              {
                                  Type = BetaManagedAgentsTextBlockType.Text,
                                  Text = result,
                              },
                          ],
                      },
                  ],
              });
          }
      }
      else if (idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn)
      {
          break;
      }
  }
go
  	stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, juglow.BetaSessionEventStreamParams{})
  	defer stream.Close()

  loop:
  	for stream.Next() {
  		event, ok := stream.Current().AsAny().(juglow.BetaManagedAgentsSessionStatusIdleEvent)
  		if !ok {
  			continue
  		}
  		switch stopReason := event.StopReason.AsAny().(type) {
  		case juglow.BetaManagedAgentsSessionRequiresAction:
  			for _, eventID := range stopReason.EventIDs {
  				// Cari event custom tool use dan jalankan
  				toolEvent := eventsByID[eventID]
  				result := callTool(toolEvent.Name, toolEvent.Input)
  				// Kirim hasilnya kembali
  				if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  					Events: []juglow.BetaManagedAgentsEventParamsUnion{{
  						OfUserCustomToolResult: &juglow.BetaManagedAgentsUserCustomToolResultEventParams{
  							Type:            juglow.BetaManagedAgentsUserCustomToolResultEventParamsTypeUserCustomToolResult,
  							CustomToolUseID: eventID,
  							Content: []juglow.BetaManagedAgentsUserCustomToolResultEventParamsContentUnion{{
  								OfText: &juglow.BetaManagedAgentsTextBlockParam{
  									Type: juglow.BetaManagedAgentsTextBlockTypeText,
  									Text: result,
  								},
  							}},
  						},
  					}},
  				}); err != nil {
  					panic(err)
  				}
  			}
  		case juglow.BetaManagedAgentsSessionEndTurn:
  			break loop
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
java
  try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
      stream.stream()
          .filter(BetaManagedAgentsStreamSessionEvents::isSessionStatusIdle)
          .map(idleEvent -> idleEvent.asSessionStatusIdle().stopReason())
          .takeWhile(stopReason -> !stopReason.isEndTurn())
          .filter(stopReason -> stopReason.isRequiresAction())
          .flatMap(stopReason -> stopReason.asRequiresAction().eventIds().stream())
          .forEach(eventId -> {
              // Cari event custom tool use dan jalankan
              var toolEvent = eventsById.get(eventId);
              var result = callTool(toolEvent.name(), toolEvent.input());

              // Kirim hasilnya kembali
              client.beta().sessions().events().send(
                  session.id(),
                  EventSendParams.builder()
                      .addEvent(BetaManagedAgentsUserCustomToolResultEventParams.builder()
                          .type(BetaManagedAgentsUserCustomToolResultEventParams.Type.USER_CUSTOM_TOOL_RESULT)
                          .customToolUseId(eventId)
                          .addTextContent(result)
                          .build())
                      .build());
          });
  }
php
  $stream = $client->beta->sessions->events->streamStream($session->id);

  foreach ($stream as $event) {
      if ($event instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsSessionStatusIdleEvent && $event->stopReason) {
          switch (true) {
              case $event->stopReason instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsSessionRequiresAction:
                  foreach ($event->stopReason->eventIDs as $eventId) {
                      // Cari event penggunaan alat kustom lalu jalankan
                      $toolEvent = $eventsById[$eventId];
                      $result = callTool($toolEvent->name, $toolEvent->input);

                      // Kirim hasilnya kembali
                      $client->beta->sessions->events->send(
                          $session->id,
                          events: [
                              [
                                  'type' => 'user.custom_tool_result',
                                  'custom_tool_use_id' => $eventId,
                                  'content' => [['type' => 'text', 'text' => $result]],
                              ],
                          ],
                      );
                  }
                  break;
              case $event->stopReason instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsSessionEndTurn:
                  break 2;
          }
      }
  }
ruby
  client.beta.sessions.events.stream_events(session.id).each do |event|
    case event
    when Juglow::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
      stop_reason = event.stop_reason
      case stop_reason
      when Juglow::Beta::Sessions::BetaManagedAgentsSessionRequiresAction
        stop_reason.event_ids.each do |event_id|
          # Cari event penggunaan custom tool lalu jalankan
          tool_event = events_by_id[event_id]
          result = call_tool.call(tool_event.name, tool_event.input)
          # Kirim hasilnya kembali
          client.beta.sessions.events.send_(
            session.id,
            events: [
              {
                type: "user.custom_tool_result",
                custom_tool_use_id: event_id,
                content: [{type: "text", text: result}]
              }
            ]
          )
        end
      when Juglow::Beta::Sessions::BetaManagedAgentsSessionEndTurn
        break
      end
    end
  end

Konfirmasi alat

Panggilan alat menunggu konfirmasi Anda di bawah kebijakan izin always_ask, atau di bawah auto saat server tidak mencapai keputusan. Saat hal itu terjadi:

  1. Sesi memancarkan event agent.tool_use atau agent.mcp_tool_use.
  1. Sesi dijeda dengan event session.status_idle yang stop_reason.type-nya adalah requires_action. ID event yang memblokir ada di array stop_reason.event_ids.
  1. Kirim event user.tool_confirmation untuk masing-masing, dengan meneruskan ID event di parameter tool_use_id. Atur result ke "allow" atau "deny". Gunakan deny_message untuk menjelaskan penolakan.
  1. Setelah semua event yang memblokir diselesaikan, sesi kembali beralih ke running.

Setiap event agent.tool_use dan agent.mcp_tool_use membawa evaluated_permission (allow, ask, atau deny), dan hanya event yang evaluated_permission-nya "ask" yang menunggu konfirmasi. Sebagian besar event juga membawa objek evaluation yang mencatat kebijakan mana yang menghasilkan keputusan tersebut, seperti dijelaskan di Melihat bagaimana setiap panggilan dievaluasi. Misalnya, panggilan bash yang dijeda di bawah kebijakan always_ask muncul di stream sebagai berikut:

json
{
  "type": "agent.tool_use",
  "id": "sevt_01def...",
  "name": "bash",
  "input": {
    "command": "pip install -r requirements.txt"
  },
  "evaluated_permission": "ask",
  "evaluation": {
    "type": "always_ask"
  },
  "processed_at": "2026-03-25T14:01:45Z"
}
bash
  exec {stream_fd}< <(curl --fail-with-body -sS -N \
    "https://haijun.my.id/v1/sessions/$SESSION_ID/events/stream?beta=true" \
    -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" \
    -H "accept: text/event-stream")

  while IFS= read -r -u "$stream_fd" line; do
    [[ $line == data:* ]] || continue
    event_json="${line#data: }"
    stop_reason=$(jq -r 'select(.type == "session.status_idle") | .stop_reason.type // empty' <<<"$event_json")
    case "$stop_reason" in
      requires_action)
        while IFS= read -r event_id; do
          # Setujui panggilan alat yang tertunda
          jq -n --arg id "$event_id" \
            '{events: [{type: "user.tool_confirmation", tool_use_id: $id, result: "allow"}]}' |
            curl --fail-with-body -sS \
              "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
              -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 @-
        done < <(jq -r '.stop_reason.event_ids[]' <<<"$event_json")
        ;;
      end_turn)
        break
        ;;
    esac
  done
  exec {stream_fd}<&-
bash
  # Alur kerja ini tidak cocok diterjemahkan ke perintah shell sekali jalan.
  # Gunakan salah satu contoh SDK dalam grup kode ini sebagai gantinya.
python
  with client.beta.sessions.events.stream(session.id) as stream:
      for event in stream:
          if event.type == "session.status_idle" and (stop_reason := event.stop_reason):
              match stop_reason.type:
                  case "requires_action":
                      for event_id in stop_reason.event_ids:
                          # Setujui panggilan alat yang tertunda
                          client.beta.sessions.events.send(
                              session.id,
                              events=[
                                  {
                                      "type": "user.tool_confirmation",
                                      "tool_use_id": event_id,
                                      "result": "allow",
                                  },
                              ],
                          )
                  case "end_turn":
                      break
typescript
  const stream = await client.beta.sessions.events.stream(session.id);

  for await (const event of stream) {
    if (event.type !== "session.status_idle") continue;
    if (event.stop_reason.type === "end_turn") break;
    if (event.stop_reason.type !== "requires_action") continue;

    for (const eventId of event.stop_reason.event_ids) {
      // Setujui panggilan alat yang tertunda
      await client.beta.sessions.events.send(session.id, {
        events: [
          {
            type: "user.tool_confirmation",
            tool_use_id: eventId,
            result: "allow",
          },
        ],
      });
    }
  }
csharp
  await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID))
  {
      if (streamEvent.Value is not BetaManagedAgentsSessionStatusIdleEvent idle) continue;

      if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction)
      {
          foreach (var eventId in requiresAction.EventIds)
          {
              // Setujui panggilan alat yang tertunda
              await client.Beta.Sessions.Events.Send(session.ID, new()
              {
                  Events =
                  [
                      new BetaManagedAgentsUserToolConfirmationEventParams
                      {
                          Type = BetaManagedAgentsUserToolConfirmationEventParamsType.UserToolConfirmation,
                          ToolUseID = eventId,
                          Result = BetaManagedAgentsUserToolConfirmationEventParamsResult.Allow,
                      },
                  ],
              });
          }
      }
      else if (idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn)
      {
          break;
      }
  }
go
  	stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, juglow.BetaSessionEventStreamParams{})
  	defer stream.Close()

  loop:
  	for stream.Next() {
  		event, ok := stream.Current().AsAny().(juglow.BetaManagedAgentsSessionStatusIdleEvent)
  		if !ok {
  			continue
  		}
  		switch stopReason := event.StopReason.AsAny().(type) {
  		case juglow.BetaManagedAgentsSessionRequiresAction:
  			for _, eventID := range stopReason.EventIDs {
  				// Setujui panggilan alat yang tertunda
  				if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  					Events: []juglow.BetaManagedAgentsEventParamsUnion{{
  						OfUserToolConfirmation: &juglow.BetaManagedAgentsUserToolConfirmationEventParams{
  							Type:      juglow.BetaManagedAgentsUserToolConfirmationEventParamsTypeUserToolConfirmation,
  							ToolUseID: eventID,
  							Result:    juglow.BetaManagedAgentsUserToolConfirmationEventParamsResultAllow,
  						},
  					}},
  				}); err != nil {
  					panic(err)
  				}
  			}
  		case juglow.BetaManagedAgentsSessionEndTurn:
  			break loop
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
java
  try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
      stream.stream()
          .filter(BetaManagedAgentsStreamSessionEvents::isSessionStatusIdle)
          .map(idleEvent -> idleEvent.asSessionStatusIdle().stopReason())
          .takeWhile(stopReason -> !stopReason.isEndTurn())
          .filter(stopReason -> stopReason.isRequiresAction())
          .flatMap(stopReason -> stopReason.asRequiresAction().eventIds().stream())
          // Setujui setiap panggilan alat yang tertunda
          .forEach(toolUseId -> client.beta().sessions().events().send(
              session.id(),
              EventSendParams.builder()
                  .addEvent(BetaManagedAgentsUserToolConfirmationEventParams.builder()
                      .type(BetaManagedAgentsUserToolConfirmationEventParams.Type.USER_TOOL_CONFIRMATION)
                      .toolUseId(toolUseId)
                      .result(BetaManagedAgentsUserToolConfirmationEventParams.Result.ALLOW)
                      .build())
                  .build()));
  }
php
  $stream = $client->beta->sessions->events->streamStream($session->id);

  foreach ($stream as $event) {
      if ($event instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsSessionStatusIdleEvent && $event->stopReason) {
          switch (true) {
              case $event->stopReason instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsSessionRequiresAction:
                  foreach ($event->stopReason->eventIDs as $eventId) {
                      // Setujui panggilan alat yang tertunda
                      $client->beta->sessions->events->send(
                          $session->id,
                          events: [
                              [
                                  'type' => 'user.tool_confirmation',
                                  'tool_use_id' => $eventId,
                                  'result' => 'allow',
                              ],
                          ],
                      );
                  }
                  break;
              case $event->stopReason instanceof \Juglow\Beta\Sessions\Events\ManagedAgentsSessionEndTurn:
                  break 2;
          }
      }
  }
ruby
  client.beta.sessions.events.stream_events(session.id).each do |event|
    case event
    when Juglow::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
      stop_reason = event.stop_reason
      case stop_reason
      when Juglow::Beta::Sessions::BetaManagedAgentsSessionRequiresAction
        stop_reason.event_ids.each do |event_id|
          # Setujui panggilan alat yang tertunda
          client.beta.sessions.events.send_(
            session.id,
            events: [
              {type: "user.tool_confirmation", tool_use_id: event_id, result: "allow"}
            ]
          )
        end
      when Juglow::Beta::Sessions::BetaManagedAgentsSessionEndTurn
        break
      end
    end
  end

Melanjutkan sesi yang idle

Sesi bertahan di antara interaksi. Riwayat percakapan dipertahankan kecuali sesi dihapus secara eksplisit. Ketika sesi menjadi idle, sandbox-nya di-checkpoint, mempertahankan status sandbox lengkap, termasuk filesystem, paket yang terinstal, dan file apa pun yang dibuat agen. Ini memungkinkan Anda melanjutkan dengan bersih dari ketidakaktifan.

Note: Meskipun riwayat sesi dipersistensi hingga dihapus, status sandbox hanya dipertahankan selama 30 hari setelah sandbox dibuat. Aktivitas tidak memperpanjang jendela ini: setelah 30 hari status sandbox (file, alat yang terinstal, dan sebagainya) tidak dapat dipulihkan, dan sesi yang dilanjutkan dimulai dari sandbox baru. Jika alur kerja Anda bergantung pada isi sandbox, minta agen menulis artefak penting ke outputs sebelum jendela tersebut berakhir.

Untuk melanjutkan sesi, kirim event user.message ke sesi tersebut seperti biasa:

bash
  # Di produksi, berikan ID tersimpan dari sesi yang ingin Anda lanjutkan.
  curl --fail-with-body -sS "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
    -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'
  {
    "events": [
      {
        "type": "user.message",
        "content": [
          {"type": "text", "text": "Now run the tests against the changes you made earlier."}
        ]
      }
    ]
  }
  EOF
bash
  # Di produksi, berikan ID tersimpan dari sesi yang ingin Anda lanjutkan.
  ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
  events:
    - type: user.message
      content:
        - type: text
          text: Now run the tests against the changes you made earlier.
  YAML
python
  # Lanjutkan sesi yang dibuat sebelumnya dengan mengirimkan event user.message baru.
  # Di produksi, berikan ID tersimpan dari sesi yang ingin Anda lanjutkan.
  client.beta.sessions.events.send(
      session.id,
      events=[
          {
              "type": "user.message",
              "content": [
                  {
                      "type": "text",
                      "text": "Now run the tests against the changes you made earlier.",
                  },
              ],
          },
      ],
  )
typescript
  // Lanjutkan sesi yang dibuat sebelumnya dengan mengirimkan event pengguna baru.
  // Di produksi, berikan ID tersimpan dari sesi yang ingin Anda lanjutkan.
  await client.beta.sessions.events.send(session.id, {
    events: [
      {
        type: "user.message",
        content: [
          {
            type: "text",
            text: "Now run the tests against the changes you made earlier.",
          },
        ],
      },
    ],
  });
csharp
  // Lanjutkan sesi yang dibuat sebelumnya berdasarkan ID. Di produksi, berikan
  // ID sesi yang Anda simpan saat sesi dibuat.
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsUserMessageEventParams
          {
              Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
              Content =
              [
                  new BetaManagedAgentsTextBlock
                  {
                      Type = BetaManagedAgentsTextBlockType.Text,
                      Text = "Now run the tests against the changes you made earlier.",
                  },
              ],
          },
      ],
  });
go
  // Lanjutkan sesi yang dibuat sebelumnya dengan mengirimkan event user.message
  // baru. Di produksi, berikan ID tersimpan dari sesi yang akan dilanjutkan.
  if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  	Events: []juglow.BetaManagedAgentsEventParamsUnion{{
  		OfUserMessage: &juglow.BetaManagedAgentsUserMessageEventParams{
  			Type: juglow.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
  			Content: []juglow.BetaManagedAgentsUserMessageEventParamsContentUnion{{
  				OfText: &juglow.BetaManagedAgentsTextBlockParam{
  					Type: juglow.BetaManagedAgentsTextBlockTypeText,
  					Text: "Now run the tests against the changes you made earlier.",
  				},
  			}},
  		},
  	}},
  }); err != nil {
  	panic(err)
  }
java
  // Lanjutkan sesi yang dibuat sebelumnya berdasarkan ID. Di produksi, teruskan
  // ID sesi yang Anda simpan saat sesi dibuat.
  client.beta().sessions().events().send(
      session.id(),
      EventSendParams.builder()
          .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
              .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
              .addTextContent("Now run the tests against the changes you made earlier.")
              .build())
          .build());
php
  // Lanjutkan sesi yang dibuat sebelumnya dengan mengirimkan event user.message baru.
  // Di produksi, berikan ID sesi yang Anda simpan saat sesi dibuat.
  $client->beta->sessions->events->send(
      $session->id,
      events: [
          [
              'type' => 'user.message',
              'content' => [
                  [
                      'type' => 'text',
                      'text' => 'Now run the tests against the changes you made earlier.',
                  ],
              ],
          ],
      ],
  );
ruby
  # Melanjutkan sesi cukup dengan mengirim event berikutnya ke sesi tersebut. Di produksi,
  # teruskan ID sesi yang Anda simpan saat sesi dibuat.
  client.beta.sessions.events.send_(
    session.id,
    events: [
      {
        type: "user.message",
        content: [
          {type: "text", text: "Now run the tests against the changes you made earlier."}
        ]
      }
    ]
  )

Mencapai anggaran sesi

Sesi yang dibuat dengan anggaran berhenti sejenak alih-alih membelanjakan berlebih. Ketika biaya daftar terlacak sesi mencapai batas, platform menghentikan sejenak setiap thread sebelum permintaan model berikutnya, dan sesi menjadi idle dengan stop_reason berupa budget_reached alih-alih dihentikan. Permintaan yang membawa total melewati batas berjalan hingga selesai, sehingga list_cost yang dilaporkan oleh snapshot session.usage dapat terbaca tepat di batas atau sedikit melewati batas. Di stream, jeda tersebut tiba sebagai tiga event, secara berurutan:

  1. session.thread_status_idle dengan stop_reason: budget_reached, untuk setiap thread saat thread tersebut berhenti sejenak.
  1. session.usage, snapshot penggunaan kumulatif sesi dan biaya daftar terlacak.
  1. session.status_idle dengan stop_reason: budget_reached. Event session.usage selalu tepat mendahului idle ini.

Thread yang permintaan terakhirnya sekaligus melewati batas dan menyelesaikan gilirannya melaporkan end_turn pada event session.thread_status_idle miliknya sendiri sementara sesi tetap melaporkan budget_reached; gunakan stop_reason tingkat sesi sebagai kunci untuk mendeteksi jeda.

Selama sesi berada di batasnya, sesi hanya menerima event yang menyelesaikan pekerjaan yang sudah berlangsung: user.tool_confirmation, user.tool_result, user.custom_tool_result, dan user.interrupt. Event apa pun yang akan memulai pekerjaan baru, termasuk user.message, ditolak dengan error 400 yang menyebutkan daftar tersebut. Ketika sesi memiliki thread yang menunggu permintaan alat sekaligus thread yang dijeda di batas, stop_reason tingkat sesi adalah requires_action, bukan budget_reached: menyelesaikan permintaan tersebut tidak memicu permintaan model, jadi tanggapi seperti biasa.

Tidak ada event yang melanjutkan sesi yang dijeda di batasnya. Sebagai gantinya, perbarui anggaran sesi: mengubah batas ke nilai apa pun di atas biaya daftar yang telah terpakai, atau menghapus anggaran dengan memperbarui sesi menggunakan "budget": null, akan melanjutkan pekerjaan yang dijeda secara otomatis. Lihat Anggaran sesi untuk cara biaya daftar dilacak dan semantik pembaruan anggaran selengkapnya.

Mengirim pesan sistem

Note: system.message didukung oleh Haijun Fable 5.1, Haijun Mythos 5.1, Haijun Fable 5, Haijun Mythos 5, Haijun Opus 5.5, Haijun Opus 5, dan Haijun Opus 4.8. Jika model utama agen tidak mendukung injeksi sistem di tengah percakapan, event akan ditolak dengan error validasi model_does_not_support_mid_conversation_system. Model subagen tidak diperiksa, karena system.message hanya masuk ke thread utama.

Kirim event system.message untuk memberi agen konteks tingkat sistem yang diistimewakan yang berlaku untuk giliran yang menyertainya dan semua giliran berikutnya. Tidak seperti field system pada definisi agen (yang menetapkan prompt sistem tingkat atas), konten system.message ditambahkan ke konteks sistem sesi sebagai giliran role: "system" alih-alih menggantikan prompt tersebut. Gunakan ketika agen memerlukan panduan tingkat sistem yang diperbarui di tengah sesi: persona yang berbeda, batasan yang direvisi, atau konteks yang diambil saat runtime yang seharusnya membentuk perilaku model ke depannya.

bash
  curl --fail-with-body -sS "https://haijun.my.id/v1/sessions/$SESSION_ID/events?beta=true" \
    -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'
  {
    "events": [
      {
        "type": "system.message",
        "content": [
          {"type": "text", "text": "The user's current timezone is America/New_York."}
        ]
      }
    ]
  }
  EOF
bash
  ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
  events:
    - type: system.message
      content:
        - type: text
          text: "The user's current timezone is America/New_York."
  YAML
python
  client.beta.sessions.events.send(
      session.id,
      events=[
          {
              "type": "system.message",
              "content": [
                  {
                      "type": "text",
                      "text": "The user's current timezone is America/New_York.",
                  },
              ],
          },
      ],
  )
typescript
  await client.beta.sessions.events.send(session.id, {
    events: [
      {
        type: "system.message",
        content: [
          {
            type: "text",
            text: "The user's current timezone is America/New_York.",
          },
        ],
      },
    ],
  });
csharp
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsSystemMessageEventParams
          {
              Type = BetaManagedAgentsSystemMessageEventParamsType.SystemMessage,
              Content =
              [
                  new BetaManagedAgentsSystemContentBlock
                  {
                      Type = BetaManagedAgentsSystemContentBlockType.Text,
                      Text = "The user's current timezone is America/New_York.",
                  },
              ],
          },
      ],
  });
go
  if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, juglow.BetaSessionEventSendParams{
  	Events: []juglow.BetaManagedAgentsEventParamsUnion{{
  		OfSystemMessage: &juglow.BetaManagedAgentsSystemMessageEventParams{
  			Type: juglow.BetaManagedAgentsSystemMessageEventParamsTypeSystemMessage,
  			Content: []juglow.BetaManagedAgentsSystemContentBlockParam{{
  				Type: juglow.BetaManagedAgentsSystemContentBlockTypeText,
  				Text: "The user's current timezone is America/New_York.",
  			}},
  		},
  	}},
  }); err != nil {
  	panic(err)
  }
java
  client.beta().sessions().events().send(
      session.id(),
      EventSendParams.builder()
          .addEvent(BetaManagedAgentsSystemMessageEventParams.builder()
              .type(BetaManagedAgentsSystemMessageEventParams.Type.SYSTEM_MESSAGE)
              .addTextContent("The user's current timezone is America/New_York.")
              .build())
          .build());
php
  $client->beta->sessions->events->send(
      $session->id,
      events: [
          [
              'type' => 'system.message',
              'content' => [
                  [
                      'type' => 'text',
                      'text' => "The user's current timezone is America/New_York.",
                  ],
              ],
          ],
      ],
  );
ruby
  client.beta.sessions.events.send_(
    session.id,
    events: [
      {
        type: "system.message",
        content: [
          {type: "text", text: "The user's current timezone is America/New_York."}
        ]
      }
    ]
  )

Selama sesi idle dengan stop_reason: requires_action, system.message hanya diterima ketika mengikuti event hasil alat dalam permintaan yang sama; jika dikirim sendiri atau bersama user.message, event tersebut ditolak hingga event alat yang tertunda terselesaikan. content menerima 1–1000 item teks.

Melacak penggunaan

Objek sesi menyertakan field usage dengan penggunaan kumulatif sesi: jumlah token, penggunaan alat server, waktu aktif, dan biaya daftar yang dilacak. Ambil sesi setelah sesi tersebut menjadi idle untuk membaca total terbaru.

json
{
  "id": "sesn_01...",
  "status": "idle",
  "usage": {
    "input_tokens": 5000,
    "output_tokens": 3200,
    "cache_read_input_tokens": 20000,
    "cache_creation": {
      "ephemeral_5m_input_tokens": 2000,
      "ephemeral_1h_input_tokens": 0
    },
    "list_cost": {
      "amount": "187",
      "currency": "USD"
    },
    "active_seconds": 342.5,
    "server_tool_use": {
      "web_search_requests": 3,
      "web_fetch_requests": 0
    }
  }
}

input_tokens melaporkan token input yang tidak di-cache dan output_tokens melaporkan total token output di seluruh panggilan model dalam sesi. Field cache_read_input_tokens melaporkan token yang dibaca dari cache prompt, dan objek cache_creation merinci token pembuatan cache berdasarkan masa berlaku cache (ephemeral_5m_input_tokens dan ephemeral_1h_input_tokens). Entri cache menggunakan TTL 5 menit secara default, sehingga giliran yang berurutan dalam jendela waktu tersebut mendapat manfaat dari pembacaan cache, yang mengurangi biaya per token.

list_cost adalah konsumsi kumulatif sesi yang dihargai berdasarkan tarif daftar publik, sebagai bilangan bulat sen dalam bentuk string, dengan kode mata uang. active_seconds adalah waktu kumulatif selama sesi memiliki setidaknya satu thread yang berjalan; aktivitas yang tumpang tindih dari thread konkuren dihitung sekali, berbeda dengan active_seconds dalam objek stats sesi, yang menjumlahkan waktu aktif masing-masing thread. Angka yang telah dideduplikasi ini adalah durasi yang menjadi dasar penetapan harga biaya runtime sesi. server_tool_use menghitung permintaan alat yang dieksekusi server untuk penetapan harga: permintaan pencarian web dihargai ke dalam biaya daftar per permintaan, dan permintaan web fetch tidak dikenai biaya per permintaan dan tidak diukur, sehingga web_fetch_requests bernilai 0. usage milik setiap thread sesi juga memuat list_cost dan active_seconds. Angka per thread dibulatkan secara independen dan tidak mencakup biaya waktu berjalan sesi, sehingga jumlahnya tidak persis sama dengan list_cost sesi; angka sesi adalah angka yang otoritatif.

Anda tidak perlu melakukan polling pada sesi untuk mengamati total ini. Event session.usage membawa snapshot kumulatif yang sama (objek usage, ditambah budget sesi, yang bernilai null ketika sesi tidak memilikinya) pada stream sesi dan dalam riwayat event. Event ini dipancarkan pada transisi idle, bukan berdasarkan timer: sesi memancarkan satu event tepat sebelum menjadi idle, apa pun alasan berhentinya, dan satu event ketika sebuah thread dijeda pada anggaran sesi. Oleh karena itu, pembaca stream melihat biaya akhir dari suatu giliran, atau dari pekerjaan yang mencapai anggaran, tanpa pengambilan tambahan.

Untuk menerapkan batas pengeluaran, tetapkan anggaran sesi alih-alih melakukan polling penggunaan dan menghentikan sesi sendiri. Platform menghargai konsumsi sesi secara terus-menerus dan menjeda setiap thread sebelum permintaan model berikutnya begitu biaya daftar sesi mencapai batas; lihat Mencapai anggaran sesi untuk mengetahui tampilannya pada stream.

Observabilitas Console

Haijun Console menyertakan penampil sesi untuk memeriksa apa yang dilakukan agen tanpa menulis kode apa pun. Di sidebar Console, di bawah Managed Agents, pilih Sessions untuk melihat setiap sesi di workspace beserta status, agen, penggunaan token, biaya, dan waktu pembuatannya, lalu pilih sebuah sesi untuk membukanya. Penampil sesi hanya dapat diakses oleh Developer dan Admin. Penampil ini menampilkan:

  • Minimap timeline: Ikhtisar aktivitas sesi dari waktu ke waktu yang dapat diperbesar, dengan satu jalur per thread dalam sesi multiagen. Pilih sebuah jalur untuk melihat thread tersebut, atau pilih sebuah penanda untuk melompat ke event-nya.
  • Transkrip: Percakapan yang dikelompokkan berdasarkan permintaan model, termasuk pemikiran, panggilan alat beserta input dan hasilnya, serta teks pesan saat di-streaming. Anda dapat memfilter event dan menyalin atau mengunduhnya sebagai JSON.
  • Inspector: Panel samping yang dapat diubah ukurannya dengan detail tentang sesi, dalam lima tab:
  • Session menampilkan detail dan metadata sesi, biaya kumulatifnya dari waktu ke waktu, dan pengeluaran terhadap anggaran sesi jika ditetapkan.
  • Events mencantumkan setiap event mentah pada thread saat ini sesuai urutan pengirimannya oleh server; pilih sebuah event untuk melihat JSON-nya. Pesan yang di-streaming saat halaman terbuka juga memiliki tampilan Deltas dari delta event-nya.
  • Tools mencantumkan alat yang dikonfigurasikan untuk agen-agen sesi, beserta jumlah panggilan, kegagalan, dan durasi median; pilih sebuah alat untuk melihat panggilannya dan melompat ke salah satunya di transkrip.
  • Resources mencantumkan file, repositori, dan penyimpanan memori yang di-mount pada path container-nya, termasuk memori di setiap penyimpanan dan perubahan yang dibuat sesi ini terhadapnya, ditambah file yang ditulis agen ke /mnt/session/outputs dan track yang dilampirkan ke agen-agen sesi.
  • Threads mencantumkan setiap thread beserta status, ukuran konteks, dan biayanya. Pilih sebuah thread untuk melihat detailnya, seperti agen, model, penggunaan konteks, dan biaya.

Tambahkan ?event={event_id} ke URL sesi untuk membuka sesi pada event tertentu.

Dengan ant beta:sessions connect, Anda dapat membuka penampil yang sama dari CLI ant atau mengikuti sesi di terminal Anda. Lihat Menghubungkan ke sesi Managed Agents dari terminal Anda.

Tips debugging

  • Periksa event sesi: Error sesi disampaikan melalui event session.error
  • Tinjau hasil alat: Kegagalan eksekusi alat sering kali menjelaskan perilaku agen yang tidak terduga
  • Lacak penggunaan token: Pantau konsumsi token untuk mengoptimalkan prompt dan mengurangi biaya
  • Gunakan prompt sistem: Tambahkan instruksi logging ke prompt sistem agar agen menjelaskan penalarannya
  • Pecahkan masalah pratinjau: Jika stream yang memilih ikut serta dalam delta event tidak berperilaku seperti yang Anda harapkan, lihat Pecahkan masalah pratinjau
On this page
Jenis eventMengintegrasikan eventDelta eventMemilih ikut serta dalam pratinjauMengakumulasi dan merekonsiliasiMempratinjau event thread sesiKeterbatasanMemecahkan masalah pratinjauSkenario tambahanMenangani pemanggilan alat kustomKonfirmasi alatMelanjutkan sesi yang idleMencapai anggaran sesiMengirim pesan sistemMelacak penggunaanObservabilitas ConsoleTips debugging