pool

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package pool provides SandboxPool, an agentloop.SandboxBuilder that reuses one long-lived *sandbox.Sandbox per session across every Run, instead of paying goja.New() + pack-Register cost (which for some packs includes compiling JS module wrappers — e.g. require('http'), require('secret')) on every single message.

The stale-context problem this solves

A Capability's Build closes over BuildContext.Ctx once (see agentloop.Capability's doc comment) — DefaultCapabilities' ai capability and every ext pack do this. That ctx is the *per-Run* context Loop.Run received, typically derived from a request (an HTTP handler's context, say) and cancelled once that Run returns. A naively cached Sandbox — built once, its packs' closures holding the first Run's ctx forever — would use an already-cancelled context for every fetch()/ai()/secret()/etc. call on every subsequent Run.

SandboxPool solves this by giving the delegate builder a swappable context.Context (swapCtx, in context.go) in place of the real one: a value that implements context.Context but forwards every call to whichever context was most recently swapped in. Every Build call — cache hit or miss — swaps in the current Run's ctx before returning, so a pack that captured the swappable value at session-creation time still observes the *current* Run's cancellation and deadline.

This only fixes Ctx. BuildContext has no other field that legitimately varies per Run for one session: Session.MessageID is documented as set once at session creation, and Scope is the tenant boundary, not expected to change mid-session. A capability that somehow captures per-Run state some other way won't be covered by this fix.

What doesn't need fixing

sb.SetOnEvent and sb.SetPolicy are plain setters on the returned Sandbox, called fresh on every Run by SandboxPool.Build (SetOnEvent) and by Loop.Run itself (SetPolicy) — both already rebind correctly on a reused sandbox with no extra work.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Options

type Options struct {
	// IdleTimeout is how long a session's cached sandbox may sit unused
	// before the background reaper evicts it (running its real cleanup
	// func). Zero uses a 30-minute default.
	IdleTimeout time.Duration

	// ReapInterval is how often the background reaper sweeps for idle
	// entries. Zero uses a 1-minute default.
	ReapInterval time.Duration
}

Options configures a SandboxPool. The zero value is a usable default.

type SandboxPool

type SandboxPool struct {
	// contains filtered or unexported fields
}

SandboxPool is an agentloop.SandboxBuilder that reuses one long-lived *sandbox.Sandbox per session, built once via a delegate SandboxBuilder and returned again — with its context swapped to the current Run's, see the package doc comment — on every later Build call for the same session. A background goroutine evicts sandboxes idle longer than Options.IdleTimeout.

Concurrent Runs for one session serialize

A goja.Runtime is not safe for concurrent use from multiple goroutines — this is a goja-level rule, not one SandboxPool invented. A fresh-per-Run SandboxBuilder never had to think about it, because two concurrent Runs for the same session got two separate Runtimes. SandboxPool's whole point is to hand out the *same* Runtime across Runs, which means it also has to guarantee only one Run is ever actually using it at a time.

It does this with a per-session mutex, not a comment: Build blocks until any Run currently using that session's sandbox has returned its cleanup func, and the cleanup func Build returns is that mutex's Unlock. Loop.Run already calls `defer cleanup()` right after Build succeeds (see run.go), so this requires no change on the caller's side — but it does mean a second Run for a session already in flight now *waits* for the first to finish, rather than running in parallel the way two fresh sandboxes would have. For a typical single-threaded chat session (no second message before the first one's response) this is invisible; an application that intentionally runs concurrent Runs for one session will see them serialize.

func New

func New(delegate agentloop.SandboxBuilder, opts Options) *SandboxPool

New wraps delegate in a SandboxPool and starts its background reaper. Call Close to stop the reaper and clean up every cached sandbox.

func (*SandboxPool) Build

func (p *SandboxPool) Build(ctx context.Context, sess agentloop.Session, scope agentloop.Scope, onEvent sandbox.OnEvent) (*sandbox.Sandbox, func(), error)

Build implements agentloop.SandboxBuilder. A cache hit checks out the existing sandbox for sess.ID — swapping in ctx and rebinding onEvent, blocking first if another Run for this session is still in flight — and returns it with a cleanup that checks it back in. A cache miss builds via the delegate, with the delegate seeing a swappable context in place of ctx, and caches the result under sess.ID.

func (*SandboxPool) Close

func (p *SandboxPool) Close()

Close stops the background reaper and cleans up every cached sandbox, waiting out any Run still in flight for each. The pool remains usable afterward — Build simply starts caching fresh entries again, same as a newly-constructed pool — but a stopped reaper never restarts, so idle entries added after Close only get cleaned up via Evict or a later Close.

func (*SandboxPool) Evict

func (p *SandboxPool) Evict(sessionID string)

Evict removes and cleans up one session's cached sandbox, if any — for an application-level session end (logout, explicit close) rather than idle eviction. Blocks until any Run currently using it finishes.

func (*SandboxPool) EvictIdle

func (p *SandboxPool) EvictIdle()

EvictIdle runs one eviction sweep immediately, removing and cleaning up every session idle longer than Options.IdleTimeout. The background reaper calls this on ReapInterval; exported so callers can drive eviction on their own schedule (or deterministically in tests) instead of waiting on the timer.

A session whose Run is taking longer than IdleTimeout looks idle by this sweep's clock (lastUsed is stamped at checkout, not continuously) and gets queued for eviction — but evict() blocks on the entry's lock, so the sweep waits out that Run rather than tearing down its sandbox. The session simply gets a fresh sandbox on its next Build.

func (*SandboxPool) Len

func (p *SandboxPool) Len() int

Len returns the number of sessions currently cached.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL