Help us shape the future of Go! Take the 2026 Go Developer Survey

worker

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package worker provides shared command-parsing and execution primitives used by the play_as REPL and the headless worker agent.

Index

Constants

View Source
const (
	// MissionMaxStack is the server's concurrent-mission cap per player (the
	// Mission Runner guide's "accept 5 simultaneous missions" stacking play).
	MissionMaxStack = 5
	// DefaultMissionMaxJumps caps how far (jumps) a mission destination may be,
	// matching the haul fleet's reposition philosophy (DefaultHaulMaxJumps):
	// several nearby runs net more than one distant payday.
	DefaultMissionMaxJumps = 5
)
View Source
const AutopilotRefuelThreshold = 0.5

AutopilotRefuelThreshold: when find_route fuel estimates are unavailable, the pre-route station refuel triggers if current fuel is below this fraction of capacity.

View Source
const DefaultAgentsDir = "data/agents"

DefaultAgentsDir is WorkerDispatch's default location for agent credential files (data/agents/<agent-id>/credentials.json), used by Deliver to resolve a gift recipient's agent id to the in-game username send_gift requires.

View Source
const DefaultExploreStaleTicks int64 = 8640

DefaultExploreStaleTicks is the age, in game ticks, past which a visited system is worth re-capturing. ~1 day at 10s/tick (the KB system-freshness convention): 86400s / 10s = 8640 ticks.

View Source
const DefaultHaulMaxJumps = 5

DefaultHaulMaxJumps caps how far (in jumps from the hauler's current system) a buy station may be for its opportunity to be considered. Repositioning halfway across the galaxy for one large payday wastes more time and fuel than several nearby hauls of comparable margin, so the fleet nets more from many short hops. 0 in a request means "use this default"; the pure ranker treats maxJumps<=0 as no cap (for tests).

View Source
const DefaultHaulPoolLimit = 500

DefaultHaulPoolLimit caps how many available opportunities a hauler considers. GetOpportunities orders by gross_profit DESC, so a small limit returns only the highest-gross (and typically most distant) opportunities. With a reposition-distance cap (DefaultHaulMaxJumps), nearby-but-lower-gross opportunities must be in the pool or the cap would drop the whole pool and the hauler would idle — so the pool is generous.

View Source
const DefaultShuttleMaxJumps = 40

DefaultShuttleMaxJumps bounds how far (in jumps) a shuttle will ferry a passenger batch. Passenger fares scale with distance (and first-class long-hauls pay a large lump plus a speedy-delivery bonus), and the ranking already favors the biggest total fare, so this is a generous safety bound — effectively "any reachable, non-stronghold destination" for this galaxy — not a turnover throttle.

View Source
const HaulMaxHaulJumps = 40

HaulMaxHaulJumps is the hard backstop on the haul leg (buy->sell) jump count. Fuel cost already penalizes long hauls economically; this caps the tail for when price/graph data is thin (median ~ 0). Set at 40 so legitimate long-haul routes (e.g. empire capital-to-capital runs, several of which exceed 20 jumps) are not dropped; it only trims the far tail. The approach leg keeps DefaultHaulMaxJumps.

View Source
const MineQtyMaxDryPasses = 5

MineQtyMaxDryPasses bounds how many consecutive Mine() calls that grow the target item's cargo count by zero MineQty tolerates before it gives up on the belt and delivers whatever it has aboard. A pass that DOES grow cargo resets this counter back to zero.

View Source
const (
	// TreasuryProfitCut is the share of a job's realized profit deposited into the
	// faction treasury on completion.
	TreasuryProfitCut = 0.05
)

Faction treasury policy. Mobile roles deposit a slice of each completed job's realized profit into their faction's shared treasury, building a buffer that rescues members who run themselves broke. Deposits are open to any member; withdrawals require the manage_treasury permission (the server rejects them otherwise, which we treat as a harmless no-op so un-permissioned agents simply idle until rescued by a human or granted the permission).

Variables

View Source
var ValidFrequencies = map[string]bool{"ten_minutely": true, "quarter_hourly": true, "half_hourly": true, "hourly": true, "daily": true, "weekly": true}

ValidFrequencies is the closed set of supported frequencies.

Functions

func Assist

func Assist(ctx context.Context, deps AssistDeps) error

Assist runs one pass of the assist standing behavior (the standing loop re-invokes it): resume an owned claim, else claim the nearest pending rescue, else make sure we are docked at home with a full tank.

func Autopilot

func Autopilot(ctx context.Context, deps AutopilotDeps, targetSystem, targetPOI string) error

Autopilot executes a multi-jump route to targetSystem, then travels to targetPOI within the destination system when targetPOI != "". It uses FindRoute for the route, jumps each hop (attempting fuel-cell use / refuel when fuel runs short), invokes OnWaypoint after each arrival, and performs the final in-system Travel. Returns on FindRoute failure, a jump that fails after fuel attempts, a jump interruption, or ctx cancellation.

func BlockPreview

func BlockPreview(stmts []Statement) string

BlockPreview returns a short, single-line summary of a block body suitable for inclusion in a "🔁 Repeating {...}" status message. Newlines within a statement's Raw are collapsed to spaces; statements are joined with '; '; the result is truncated to roughly 60 runes with a trailing ellipsis if longer.

func CaptureMarket

func CaptureMarket(ctx context.Context, client game.GameClient, kb knowledge.Base)

CaptureMarket persists the full source-classified market data from the client's most recent (full, no-item_id) view_market response: buy-order demand into the demand tables and sell-order supply into the supply tables, each replacing the station's entire order set. Best-effort: silently no-ops when the KB is absent, there is no market data, or the player is not at a station. A single freshness gate (keyed on the demand ledger) guards both sides, since they come from the same read.

func CurrentBoundary

func CurrentBoundary(freq string, now time.Time) time.Time

CurrentBoundary returns the most recent wall-clock boundary (UTC) for freq at or before now: the most recent ten-minute mark (:00, :10, …), quarter hour, half hour (:00 or :30), the top of the hour, midnight, or the most recent Sunday midnight. Returns the zero time for an unknown frequency.

func ExecuteLoop

func ExecuteLoop(
	ctx context.Context,
	out io.Writer,
	count int,
	force bool,
	body []Statement,
	depth int,
	runStatement func(tokens []string) error,
) error

ExecuteLoop runs count iterations of body. For each statement whose first token is "loop", ParseLoopHeader + ParseStatements is applied and ExecuteLoop recurses; otherwise runStatement is called. Each loop enforces errors according to its own force flag: a loop with force continues past errors and returns nil; a loop without force returns the first error. depth controls indentation of status lines.

func Explore

func Explore(ctx context.Context, deps ExploreDeps) error

Explore performs one exploration step: resolve the current system, choose the next target via NextExploreTarget, and autopilot to it (capturing each hop via the worker's plain KB capture). When there is no KB, no current system, or no reachable frontier, it logs and returns nil so the worker idles and retries on the next pass.

func FormatDuration

func FormatDuration(seconds int) string

FormatDuration formats seconds as "Xm Ys", "Xm", or "Xs".

func GetLocationPOI

func GetLocationPOI(ctx context.Context, client game.GameClient) (game.POI, error)

GetLocationPOI fetches the current POI via the get_location command and returns it as a fully-populated game.POI.

The server retired get_poi (2026-06-24); resource richness/remaining is now only available from get_location, while structural fields (class, description, position, base linkage) come from get_system. This helper reads the live resources from get_location and enriches the static structural fields from the cached system data in client state (populated by a prior get_system, which the capture flows always run first). Fields absent from both commands — per-POI description (when not already cached), hidden, reveal_difficulty — are left zero and preserved across updates by the knowledge base's merge-UPSERT.

func HasTopLevelOpenBrace

func HasTopLevelOpenBrace(s string) bool

HasTopLevelOpenBrace reports whether s contains a '{' outside of any '"..."' or "'...'" quoted string.

func Haul

func Haul(ctx context.Context, deps HaulDeps) error

Haul performs one hauling step: load available opportunities (scanning if the pool is empty), rank them for the current system, claim the best reachable one, and run it (buy -> transit -> sell -> complete). On any mid-run failure it logs and returns nil so the worker idles and retries; the claimed row is left claimed (harmless — the spread regenerates on the next scan).

func KBUpdateAll

func KBUpdateAll(ctx context.Context, client game.GameClient, kb knowledge.Base, mc *market.Collector, detectedBy string) error

KBUpdateAll runs update_system, update_poi, and (if docked) update_station, update_facilities, and update_missions. detectedBy records which agent observed the system/POI data (provenance). mc is optional: when non-nil, the market snapshot is written via the collector.

func KBUpdateFacilities

func KBUpdateFacilities(ctx context.Context, client game.GameClient, kb knowledge.Base) error

KBUpdateFacilities fetches facility details via 'facility list' and saves enriched data (description, active, maintenance, service, recipe_id) to the knowledge base.

func KBUpdateMissions

func KBUpdateMissions(ctx context.Context, client game.GameClient, kb knowledge.Base) error

KBUpdateMissions fetches the mission board at the current station and upserts each hand-authored entry into the KB mission catalog. Procedural missions (empty template_id) are skipped. Ported from play_as so the worker fleet's hourly kb_update captures mission boards fleet-wide.

func KBUpdatePOI

func KBUpdatePOI(ctx context.Context, client game.GameClient, kb knowledge.Base, detectedBy string) error

KBUpdatePOI fetches current POI data and saves it to the knowledge base. detectedBy records which agent observed the data (POI provenance).

func KBUpdatePOIData

func KBUpdatePOIData(ctx context.Context, client game.GameClient, kb knowledge.Base, detectedBy string) (game.POI, error)

KBUpdatePOIData is like KBUpdatePOI but also returns the captured POI so callers can reuse it (e.g. play_as's faction-intel file side effect) without issuing a second get_location query.

func KBUpdateStation

func KBUpdateStation(ctx context.Context, client game.GameClient, kb knowledge.Base, mc *market.Collector, source string) error

KBUpdateStation fetches base details, market listings, and ship listings at the current station and saves them to the knowledge base. source is the tool tag recorded as the origin of the captured data (e.g. "play_as" or "worker"). mc is optional: when non-nil, the market snapshot is also written to the market collector DB via mc.WriteSnapshot.

func KBUpdateSystem

func KBUpdateSystem(ctx context.Context, client game.GameClient, kb knowledge.Base, detectedBy string) error

KBUpdateSystem fetches the current system data and saves it to the knowledge base. detectedBy records which agent observed the data (POI provenance).

func ListScripts

func ListScripts(agentID string) (perAgent, shared []string)

ListScripts returns the sorted script names (without extension) in the per-agent and shared directories.

func LoadRoles

func LoadRoles(path string) (map[string]Role, error)

LoadRoles parses the roles config at path. Every schedule entry must name a valid frequency.

func Missions

func Missions(ctx context.Context, deps MissionDeps) error

Missions performs one mission-runner pass: complete any resumable active missions, read the local board, accept + provision a stackable deliver set, run it to the shared destination system, and record every outcome. Mirrors Haul's resilience contract: mid-run failures log and return nil so the worker idles and retries; the pass never kills the worker loop.

func NextBoundary

func NextBoundary(freq string, now time.Time) time.Time

NextBoundary returns the next time freq will fire strictly after now, used for the "next due" column in view_scheduled.

func NextExploreTarget

func NextExploreTarget(ctx context.Context, kb knowledge.Base, currentSystem string, staleTicks, nowTick int64) (string, bool, error)

NextExploreTarget picks the next system an explorer should visit from currentSystem, ranked by jump distance, preferring (1) undiscovered frontier systems — connection endpoints not yet in the KB's systems table — then (2) known-but-unvisited systems (LastVisitedTick == 0), then (3) stale known systems (nowTick-LastUpdatedTick > staleTicks). ok is false when nothing within reach is worth visiting, in which case target is "".

func ParseLoopHeader

func ParseLoopHeader(stmt Statement) (count int, force bool, body string, isBlock bool, err error)

ParseLoopHeader parses the header of a "loop" statement. It accepts the existing single-command form (e.g. "loop 5 mine") and the new block form (e.g. "loop 5 { mine; refuel }"). For the block form the returned body is the content between the outermost matching braces; for the single form it is the concatenation of remaining args.

func PayRescueDebt

func PayRescueDebt(ctx context.Context, c GiftClient, out io.Writer, agentsDir, agentID string)

PayRescueDebt pays the head outstanding rescue debt for agentID when the worker is docked (send_gift credits requires a base with storage) and can afford it. One debt per call respects the 1-gift-per-tick rate limit; the rest wait for the next docked pass. Best-effort: every failure logs and leaves the debt in place.

func RankHaulOpportunities

func RankHaulOpportunities(opps []market.ArbitrageOpportunity, currentSystemID string, nameToID map[string]string, graph navigation.JumpGraph, maxJumps int, fuelPerJump int, priceOf func(stationID string) float64) []market.ArbitrageOpportunity

RankHaulOpportunities orders available opportunities best-first for a hauler at currentSystemID. Primary order is net-of-fuel profit descending — gross profit minus the fuel cost of the approach leg (current->buy) and the haul leg (buy->sell), priced at fuelPerJump*priceOf(buy station) — lifted by the route's stability boost. fuelPerJump<=0 or a nil priceOf disables the fuel model (net == effective gross), the graceful-degradation path for callers with no fuel data. Opportunities within haulNearTieFraction of the top net are instead ordered by reposition cost (jumps current->buy), then a chaining bonus (sell at/adjacent to another opp's buy), then id. Opportunities whose buy-system name does not resolve to a known system id, or whose buy-system is unreachable, are dropped. When maxJumps > 0, any opportunity whose buy-system is more than maxJumps from currentSystemID is also dropped (maxJumps <= 0 disables the cap). Independently, any opportunity whose haul leg (buy->sell) exceeds HaulMaxHaulJumps is dropped as a hard backstop, regardless of maxJumps.

func ResolveScriptArg

func ResolveScriptArg(arg, agentID string) (string, bool)

ResolveScriptArg maps a `run` argument to a file path. An explicit path (see isExplicitScriptPath) is used verbatim if it exists. A bare name is resolved as "<dir>/<name>.smolt" against scriptSearchPaths in order.

func ResolveTokens

func ResolveTokens(args []string, state *game.State) ([]string, error)

ResolveTokens replaces every $TOKEN$ occurrence in each argument with a value derived from live game state. It returns the substituted arguments, or a *TokenError if any token cannot be resolved.

func RunStanding

func RunStanding(ctx context.Context, role Role, deps StandingDeps) error

RunStanding drives a worker's default standing behavior until ctx is cancelled: it registers the role's scheduled commands with the Scheduler and runs the role's idle script in a loop, both serialized on deps.ExecMu so they never interleave on the single game connection. It returns when ctx is cancelled, after any in-flight idle pass completes.

func SaveScript

func SaveScript(name, content string) error

SaveScript writes content to the shared scripts dir as "<name>.smolt", creating the directory if needed. A trailing newline is appended.

func ScanBraceDepth

func ScanBraceDepth(s string) (depth int, inQuote bool)

ScanBraceDepth reports the net brace depth of s and whether the scan ended inside a quoted string. Braces inside '"..."' or "'...'" are ignored. A negative depth indicates more '}' than '{' in s.

func ScriptSearchPaths

func ScriptSearchPaths(agentID string) []string

ScriptSearchPaths returns the script directories searched by bare-name resolution, in precedence order: per-agent first (shadows shared), then the shared library.

func SelectMissionSet

func SelectMissionSet(cands []missionCandidate, credits, cargoFree float64, maxJumps int) []missionCandidate

SelectMissionSet picks up to MissionMaxStack candidates to run as one trip: the best-net candidate anchors the trip, and only candidates sharing its destination system stack onto it (cross-system banding is a phase-2 upgrade). The greedy fill respects the buy budget (missionBuyBudgetFraction of credits) and free cargo space; anchors farther than maxJumps are skipped entirely.

func Shuttle

func Shuttle(ctx context.Context, deps ShuttleDeps) error

Shuttle performs one passenger-ferry step: ensure docked, survey the station's waiting passengers, pick the best-paying reachable destination (excluding pirate strongholds and anything beyond the jump budget), board whoever fits, fly them there, and collect the auto-paid fares on docking — depositing the treasury's cut. On any mid-run failure it logs and returns nil so the worker idles and retries; aboard passengers auto-deliver whenever the ship next docks at their destination, so a partial run is never a hard loss.

func SplitArgs

func SplitArgs(s string) []string

SplitArgs splits a command string into arguments, respecting double and single quotes. e.g. `create_faction "Covenant of the Eternal Spark" SPRK` → ["create_faction", "Covenant of the Eternal Spark", "SPRK"]

func SplitScriptCommands

func SplitScriptCommands(content string) ([]string, error)

SplitScriptCommands splits a script file's content into logical commands. Top-level blank lines and '#' comment lines are skipped; a multi-line block (e.g. a loop { ... }) is kept together until its braces balance, using the same brace/quote scanning the REPL uses for multi-line prompt input.

func SubstituteParams

func SubstituteParams(lines []string, params map[string]string) []string

SubstituteParams replaces every $KEY$ occurrence in each line with params[KEY], for keys present in params. Keys absent from params (e.g. live state tokens like $ASTEROID_BELT$) are left untouched so they pass through to ResolveTokens. Returns a new slice; the input is not mutated. A nil/empty params map is a no-op.

func UsernameFor

func UsernameFor(agentsDir, agentID string) (string, error)

UsernameFor resolves agentID to its in-game username by reading <agentsDir>/<agentID>/credentials.json (the same file every worker/agent process already carries its own login credentials in). agentsDir defaults to DefaultAgentsDir when empty.

Types

type AssignedTask

type AssignedTask struct {
	ID     string
	Script string
	Params map[string]string
}

AssignedTask is a one-shot task handed to the worker by the overmind. It is a local mirror of control.Assign so pkg/worker does not import pkg/overmind/control.

type AssistDeps

type AssistDeps struct {
	Client      game.GameClient
	KB          knowledge.Base
	Queue       RescueQueue
	Out         io.Writer
	AgentID     string
	HomeStation string // station POI id at the home capital (fleet yaml `station`)
	// Navigate overrides Autopilot in tests; nil uses the real thing.
	Navigate func(ctx context.Context, system, poi string) error
	// SetActivity publishes the status-page "current activity" string (nil in
	// tests). Set while rescuing a target, cleared ("") when idle at home.
	SetActivity func(string)
}

AssistDeps wires one assist worker: fly rescue fuel to quarantined workers from the shared queue, then return to the home capital and re-tank.

type AutopilotDeps

type AutopilotDeps struct {
	Client     game.GameClient
	Out        io.Writer   // progress lines; nil -> io.Discard
	OnWaypoint CaptureFunc // per-arrival capture; nil -> no-op
	// WaypointCheck is an optional per-arrival early-stop decision (re-route hook);
	// nil -> never stops. It runs after OnWaypoint on each jump arrival.
	WaypointCheck WaypointCheckFunc
}

AutopilotDeps are the injected dependencies for Autopilot.

type CaptureFunc

type CaptureFunc func(ctx context.Context) error

CaptureFunc records knowledge-base state at a waypoint. Autopilot calls it once after each jump arrival; it is best-effort — a returned error is logged to the autopilot's writer and never aborts the route. nil means no capture.

type CommandRunner

type CommandRunner interface {
	Run(ctx context.Context, tokens []string) error
}

CommandRunner executes one already-tokenized command line. tokens[0] is the command name; the remaining elements are its arguments. It is the seam shared by the play_as REPL (rich dispatch) and the headless worker (lean dispatch).

type ExploreDeps

type ExploreDeps struct {
	Client     game.GameClient
	KB         knowledge.Base
	Out        io.Writer // progress; nil -> io.Discard
	StaleTicks int64     // 0 -> DefaultExploreStaleTicks
}

ExploreDeps are the injected collaborators for one Explore step.

type FuelPriceSource

type FuelPriceSource interface {
	GetStationFuelPrice(ctx context.Context, stationID string) (allIn int, capturedAt time.Time, ok bool, err error)
	MedianStationFuelAllIn(ctx context.Context) (median int, ok bool, err error)
}

FuelPriceSource supplies captured station fuel prices for net-of-fuel haul economics. Satisfied by *market.Collector. Optional on HaulDeps: a nil source makes every fuel cost 0, so ranking/gating fall back to gross-only behavior.

type GiftClient

type GiftClient interface {
	GetState() *game.State
	SendGift(ctx context.Context, payload map[string]any) error
}

GiftClient is the slice of game.GameClient the rescue-fee payment needs.

type HaulDeps

type HaulDeps struct {
	Client    game.GameClient
	KB        knowledge.Base
	Market    OpportunityStore
	Out       io.Writer // nil -> io.Discard
	AgentID   string    // claim owner
	PoolLimit int       // 0 -> DefaultHaulPoolLimit
	MaxJumps  int       // 0 -> DefaultHaulMaxJumps; reposition-distance cap (jumps) to a buy station
	// RecaptureBuyMarket refreshes the current (buy) station's market into the store
	// so the pre-buy gate prices against a real-time ask. nil skips the live capture
	// (tests seed prices directly via the store).
	RecaptureBuyMarket func(ctx context.Context) error
	// Now returns the current wall-clock time (nil -> time.Now). Injected for deterministic tests.
	Now func() time.Time
	// Treasury rate-limits faction-treasury rescue withdrawals across haul passes
	// (held per worker process). nil disables the rescue path.
	Treasury *treasuryRescue
	// FuelPrices supplies captured station fuel prices for net-of-fuel ranking and
	// gating. nil disables fuel accounting (ranking/gating fall back to gross-only).
	FuelPrices FuelPriceSource
	// SetActivity publishes a short human-readable "current activity" string for
	// the fleet status page (nil in tests). Best-effort; set when a haul is
	// claimed, cleared ("") when the pass finds no work.
	SetActivity func(string)
}

HaulDeps are the injected collaborators for one Haul step.

type MissionDeps

type MissionDeps struct {
	Client  game.GameClient
	KB      knowledge.Base
	Market  MissionStore
	Out     io.Writer // nil -> io.Discard
	AgentID string
	// MaxJumps caps mission-destination distance (0 -> DefaultMissionMaxJumps).
	MaxJumps int
	// Categories is the board-category allowlist (nil/empty -> delivery only).
	// The learning pool runs {"delivery", "exploration"}.
	Categories []string

	// Treasury rate-limits faction-treasury rescue withdrawals (nil disables).
	Treasury *treasuryRescue
	// FuelPrices supplies captured station fuel prices for the fuel-cost model.
	// nil disables fuel accounting (net == reward - item cost).
	FuelPrices FuelPriceSource
	// Now returns wall-clock time (nil -> time.Now); injected for tests.
	Now func() time.Time
	// State carries the cross-pass dry-streak/reposition memory (nil disables
	// repositioning — tests that don't care simply omit it).
	State *missionRunState

	// SetActivity publishes the status-page "current activity" string (nil in
	// tests). Set when a mission is accepted or a held one resumed, cleared
	// ("") on a dry pass.
	SetActivity func(string)
	// EnableFreight opts this worker into the /shipping carrier path, evaluated
	// co-equally with the mission board. Default false so existing fleets are
	// unchanged until the canary opts in explicitly.
	EnableFreight bool
	// FreightMaxPackages caps concurrent freight contracts (sub-project C
	// multi-package trips). 0 and 1 both mean the v1 single-contract
	// behavior; the cap layers UNDER the server/cargo headroom gates and is
	// never a target. Canary: fighter-4 at 3.
	FreightMaxPackages int
	// FreightBootstrap enables the probationary loss-leader floor relaxation
	// (effectiveFreightFloor). Defaults on wherever EnableFreight is set; a
	// false value forces the normal 500 floor — the kill switch. Inert unless
	// EnableFreight is also true (freightCandidate only runs on the freight path).
	FreightBootstrap bool
	// contains filtered or unexported fields
}

MissionDeps are the injected collaborators for one Missions pass.

type MissionStore

type MissionStore interface {
	RecordMissionResult(ctx context.Context, r market.MissionResult) error
	GetReferenceAsk(ctx context.Context, itemID string) (market.ReferenceAsk, bool, error)
	// GetReferencePrice returns a robust "cheap" cross-station reference
	// (20th-percentile over lookback), used as the surge-ceiling basis for
	// the local depth-walk gate — see missionSurgeMult.
	GetReferencePrice(ctx context.Context, itemID string, lookback time.Duration) (float64, bool, error)
	// RecordFreightResult persists one settled shipping-contract outcome.
	RecordFreightResult(ctx context.Context, r market.FreightResult) error
}

MissionStore is the subset of *market.Collector the mission runner needs (result telemetry + item pricing for the cost gate).

type OpportunityStore

type OpportunityStore interface {
	GetOpportunities(ctx context.Context, status string, limit int) ([]market.ArbitrageOpportunity, error)
	GetClaimedByAgent(ctx context.Context, agentID string) ([]market.ArbitrageOpportunity, error)
	ClaimOpportunity(ctx context.Context, id int, agentID string) (bool, error)
	ReleaseOpportunity(ctx context.Context, id int, agentID string) (bool, error)
	CompleteOpportunity(ctx context.Context, id int, agentID string) (bool, error)
	ScanArbitrage(ctx context.Context, opts market.ScanOptions) (market.ScanResult, error)
	AdmitBookClaim(ctx context.Context, book market.BookKey, candidates []market.BookCandidate, agentID string, capN int, expiresIn time.Duration) (market.AdmitResult, error)
	SettleBookClaim(ctx context.Context, claimID int64, agentID string, boughtUnits float64) error
	CompleteBookClaim(ctx context.Context, claimID int64, agentID string) error
	ReleaseBookClaim(ctx context.Context, claimID int64, agentID string) error
	GetActiveBookClaim(ctx context.Context, itemID, fromStation, agentID string) (int64, bool, error)
	InvalidateBook(ctx context.Context, itemID, fromStation, agentID, reason string) error
	ReapExpiredBookClaims(ctx context.Context) (int, error)
	GetItemStationPrices(ctx context.Context, itemID string) ([]market.ItemStationPrice, error)
	GetAskLadder(ctx context.Context, itemID, stationID string) ([]market.AskLevel, error)
	GetStationOrders(ctx context.Context, stationID, itemID string) ([]market.Order, error)
	FindBestPrices(ctx context.Context, itemID, side string, limit int) ([]market.BestPrice, error)
	RecordHaulResult(ctx context.Context, r market.HaulResult) error
}

OpportunityStore is the subset of *market.Collector the hauler needs. Defining it here keeps the engine testable with a fake and leaves pkg/market unmodified.

type RescueQueue

type RescueQueue interface {
	List() ([]rescue.Record, error)
	Transition(agentID string, from, to rescue.Status, mutate func(*rescue.Record)) (bool, error)
}

RescueQueue is the slice of rescue.Queue the assist behavior consumes.

type Role

type Role struct {
	Schedule   []ScheduleEntry   `yaml:"schedule"`
	Idle       string            `yaml:"idle"`        // bare script name (data/scripts)
	IdleParams map[string]string `yaml:"idle_params"` // substituted into the idle script
}

Role is a worker's default standing behavior: recurring scheduled commands plus an idle script run on idle cycles.

type ScheduleEntry

type ScheduleEntry struct {
	Every   string `yaml:"every"`   // hourly | daily | weekly
	Command string `yaml:"command"` // a command line; may contain $TOKEN$s
}

ScheduleEntry is one recurring command in a role's standing behavior.

type ScheduledTask

type ScheduledTask struct {
	ID        int       `json:"id"`
	Frequency string    `json:"frequency"` // ten_minutely | quarter_hourly | half_hourly | hourly | daily | weekly
	Command   string    `json:"command"`
	CreatedAt time.Time `json:"created_at"`
	LastRun   time.Time `json:"last_run,omitzero"` // UTC; zero = never run
}

ScheduledTask is a recurring command the user has registered. The command is a full play_as command line (e.g. "view_storage --station_id frontier") run through executeLogicalCommand at the task's frequency.

type Scheduler

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

Scheduler owns the persisted set of scheduled tasks for one agent.

func LoadScheduler

func LoadScheduler(path string) (*Scheduler, error)

LoadScheduler reads the scheduler state at path. A missing file yields an empty scheduler with no error.

func (*Scheduler) Add

func (s *Scheduler) Add(freq, command string, now time.Time) (ScheduledTask, error)

Add validates and appends a new task. With run-immediately semantics the caller runs the command at add time, so LastRun is stamped to now: the task will not fire again until the next boundary. Persists before returning.

func (*Scheduler) Due

func (s *Scheduler) Due(now time.Time) []ScheduledTask

Due returns the tasks whose current period boundary is newer than their last run — i.e. they are owed a run as of now. It does not mutate state.

func (*Scheduler) List

func (s *Scheduler) List() []ScheduledTask

List returns a copy of the scheduled tasks, ordered by id.

func (*Scheduler) Remove

func (s *Scheduler) Remove(id int) bool

Remove deletes the task with the given id, persisting the change. Returns false if no such task exists.

func (*Scheduler) StartLoop

func (s *Scheduler) StartLoop(ctx context.Context, interval time.Duration, execMu *sync.Mutex, run func(ScheduledTask), nowFn func() time.Time)

StartLoop runs the scheduler in a background goroutine until ctx is cancelled. It performs an immediate catch-up pass (logging any backfill), then fires due tasks at each tick. run executes one task; it is invoked under execMu so scheduled commands never interleave with foreground REPL commands. nowFn supplies the current time (injectable for tests).

type ShuttleDeps

type ShuttleDeps struct {
	Client   game.GameClient
	KB       knowledge.Base
	Out      io.Writer // nil -> io.Discard
	AgentID  string
	MaxJumps int // 0 -> DefaultShuttleMaxJumps
	// Now returns the current wall-clock time (nil -> time.Now). Injected for tests.
	Now func() time.Time
	// Treasury rate-limits faction-treasury rescue withdrawals across passes.
	Treasury *treasuryRescue
	// State carries cross-pass shuttle memory (dry-pass streak + reposition
	// cursor). nil disables repositioning (the shuttle just idles in place).
	State *shuttleState
	// SetActivity publishes the status-page "current activity" string (nil in
	// tests). Set when passengers are boarded, cleared ("") on an idle pass.
	SetActivity func(string)
}

ShuttleDeps are the injected collaborators for one Shuttle step.

type StandingDeps

type StandingDeps struct {
	Runner     CommandRunner                       // executes a tokenized command (WorkerDispatch)
	Scheduler  *Scheduler                          // per-agent recurring tasks
	Client     interface{ GetState() *game.State } // for token resolution
	ExecMu     *sync.Mutex                         // serializes scheduled + idle work on the one game conn
	Paused     func() bool                         // gate from the control reader's paused flag
	Draining   func() bool                         // second gate (drain): finish current pass, take no new work
	SetDrained func(bool)                          // publishes whether the worker is held idle due to drain
	Out        io.Writer                           // worker stdout / logs
	NowFn      func() time.Time                    // injectable clock

	IdleInterval     time.Duration // between idle passes (0 → game.SleepShort)
	ScheduleInterval time.Duration // scheduler tick (0 → game.SleepLong)
	AgentID          string        // for script resolution search paths

	// Task hooks (nil when the worker has no control channel). NextTask returns
	// and consumes the pending assigned task (or nil); OnTaskResult reports a
	// finished task's id and error (nil = success).
	NextTask     func() *AssignedTask
	OnTaskResult func(taskID string, err error)

	// PayDebts, when set, runs once per non-drained idle pass under ExecMu to
	// pay any outstanding rescue-fee debt. nil for workers with no fee wiring.
	PayDebts func(context.Context)

	// Handoffs, when set, runs once per non-drained idle pass under ExecMu to
	// fulfill pending stock-handoff records for this agent. nil when the worker
	// has no handoff queue configured.
	Handoffs func(context.Context)
}

StandingDeps are the collaborators RunStanding needs. All are injectable so the driver is testable without a game connection.

type Statement

type Statement struct {
	Raw    string
	Tokens []string
}

Statement is one top-level command inside a loop body. Raw is the original string (with surrounding whitespace trimmed); Tokens is the SplitArgs result over Raw.

func ParseStatements

func ParseStatements(body string) ([]Statement, error)

ParseStatements splits body into top-level statements, separating on ';' and '\n' at brace-depth 0 outside quotes. '#' begins a line comment (to end-of-line) outside quotes. Nested '{...}' content is preserved verbatim in the enclosing Statement's Raw.

type TokenError

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

TokenError indicates a $TOKEN$ in a command could not be resolved from game state. Inside a loop it is fatal: the loop aborts immediately, even under -f.

func (*TokenError) Error

func (e *TokenError) Error() string

type WaypointCheckFunc

type WaypointCheckFunc func(ctx context.Context) (stop bool, err error)

WaypointCheckFunc is an optional per-arrival decision hook (distinct from the capture-only CaptureFunc). Autopilot calls it after each jump arrival; returning stop=true makes Autopilot halt at the current system and return errAutopilotStopped so the caller can re-plan. A returned error is logged and treated as "do not stop" (best-effort — the watchdog never aborts a route on its own failure). nil means no check.

type WorkerDispatch

type WorkerDispatch struct {
	Client  game.GameClient
	KB      knowledge.Base
	Market  *market.Collector
	Out     io.Writer
	AgentID string      // claim owner for opportunity-claiming roles (e.g. hauler)
	Station string      // home station POI id, used by the assist role
	Rescue  RescueQueue // shared stranded-worker rescue queue, used by the assist role
	// AgentsDir is where agent credential files live (<AgentsDir>/<agent-id>/
	// credentials.json), used by Deliver to resolve a gift recipient's agent
	// id to its in-game username. Empty -> DefaultAgentsDir ("data/agents").
	AgentsDir string
	// EnableFreight opts the missions role into the /shipping carrier path
	// (freight evaluated co-equally with the mission board). Default false =
	// the freight code is fully inert.
	EnableFreight bool
	// FreightMaxPackages caps concurrent freight contracts; see
	// MissionDeps.FreightMaxPackages. 0/1 = single-contract v1 behavior.
	FreightMaxPackages int
	// FreightBootstrap enables the probationary loss-leader floor relaxation for
	// the missions role. Defaults on when EnableFreight is set; false is the
	// kill switch. See MissionDeps.FreightBootstrap.
	FreightBootstrap bool
	// MissionCategories is the mission-board category allowlist for the
	// missions role (nil/empty -> delivery only; the learning pool adds
	// "exploration").
	MissionCategories []string
	// contains filtered or unexported fields
}

WorkerDispatch is the lean, headless command dispatch used by cmd/worker. It covers the curated worker-script vocabulary only; each command maps directly to a game.GameClient method, plus shared KB-capture for tracking commands. Unknown commands return an error (never silently ignored). KB may be nil, in which case tracking commands degrade to a no-op capture (movement/mining still work).

func NewWorkerDispatch

func NewWorkerDispatch(client game.GameClient, kb knowledge.Base, mc *market.Collector, out io.Writer) *WorkerDispatch

NewWorkerDispatch builds a dispatch over the given client, KB, and optional market collector. out receives human-readable progress lines (worker stdout / logs).

func (*WorkerDispatch) BuyDirected

func (d *WorkerDispatch) BuyDirected(ctx context.Context, itemID string, qty int, station string, maxUnit float64, recipient string) error

BuyDirected buys ITEM xQTY at STATION with a per-unit price ceiling (0 = no ceiling), then hands to RECIPIENT (gift, or deposit when self). Recompute-remaining: counts recipient-bound goods already in cargo first. The live ask is checked once against maxUnit right after docking; buying then proceeds in cargo-capacity-sized chunks, handing each chunk off to RECIPIENT before buying the next (freeing cargo room) — no second travel leg, since the handoff happens at STATION where the buy already occurred.

func (*WorkerDispatch) CraftOutputs

func (d *WorkerDispatch) CraftOutputs(ctx context.Context, recipeID string, numOutputs int, station, facility string, estFee float64) error

CraftOutputs crafts NUM_OUTPUTS units of RECIPE's output item at STATION. facility "hand" hand-crafts at the docked station's local workshop (the server auto-routes when facility_id is omitted from the payload — Task 0 findings, 2026-07-10, confirmed hand/workshop dry-run and craft ARE supported live); any other value is passed as an explicit facility instance id (crafting requires being docked at that facility's station — a remote facility_id errors no_facility). NUM_OUTPUTS is passed directly as the craft command's `quantity`: the server does the ceil-divide into production runs itself (live-verified: quantity 6 on a 3-per-run recipe produced "runs": 2), so no output-per-run math is needed for the craft call itself.

Recompute-remaining: before crafting, any of the recipe's output item already sitting in the worker's own storage at STATION (or already carried in cargo) counts against NUM_OUTPUTS, so a resumed/retried node does not over-produce. The recipe's output item id is looked up from the KB (recipe_outputs) — the craft_node task's params (RECIPE, NUM_OUTPUTS, STATION, FACILITY) never carry it directly.

Budget gate: a live dry-run always runs first — it is both the have_inputs/have_credits preflight and the 2x-replan gate. If the dry-run reports !HaveInputs or !HaveCredits, this fails immediately (no craft call) with an error naming which precondition failed plus the server's Message verbatim — live probes show the message names the nearest facility that CAN make the recipe when this station can't (Task 0 findings #6), which operators need in park details. Separately, the plan runner's budget admission already approved estFee (the planner's catalog fee estimate for this node); if the live dry-run's credits_total comes back more than 2x that, the catalog is stale and this fails with an error naming "replan" instead of queuing a possibly much more expensive job. estFee <= 0 disables the fee gate (no estimate to compare against).

Crafting is asynchronous: the server queues the job and delivers output directly to the worker's own storage at STATION over subsequent ticks. Hand-crafts (the "hand" facility default) run at the Station Workshop — they advance only while docked there and PAUSE on undock (Task 0 findings #7). Since the plan runner marks a node done the moment this verb returns and may then have the worker undock for its next task, this verb does NOT return once the job is merely queued: it polls `craft action=queue` (via RawCommand — the only client-side surface that reports job progress without inventing a new server command) at game.SleepTick cadence, staying put, until the job reports complete for BOTH the hand and facility paths. The wait is bounded by the dry-run's EstCompletionTick plus a generous margin (GameClock only drifts forward — memory: reference_gameclock_forward_drift — so a tight bound risks a false-positive timeout); on timeout this returns an error, never a silent success.

func (*WorkerDispatch) Deliver

func (d *WorkerDispatch) Deliver(ctx context.Context, itemID string, qty int, from, to, recipient string) error

Deliver moves ITEM xQTY from the worker's own storage at FROM to RECIPIENT at TO. recipient "self" (or the worker's own agent id) means deposit into own storage at TO instead of gifting. Gifts are cargo-only (live-verified, 2026-07-10): the verb withdraws into cargo at FROM, then travels to TO and gifts/deposits from cargo, repeating in cargo-capacity-sized batches until qty is satisfied. If FROM held less than qty, it delivers what existed and returns nil with a short-delivery note written to d.Out instead of looping forever.

from == "" means the goods are already in cargo (e.g. just-mined ore handed off by MineQty): the withdraw leg AND the FROM travel leg are both skipped entirely — Deliver goes straight to TO with whatever is aboard. If cargo holds less than qty in this mode, that is a shortfall (mirroring the "source exhausted" case below, since there is no source to make a second withdraw pass against): Deliver delivers what's on hand and returns nil.

func (*WorkerDispatch) HandoffPass

func (d *WorkerDispatch) HandoffPass(ctx context.Context, q *handoff.Queue) error

HandoffPass fulfills pending handoff records owned by this agent (d.AgentID) at its current docked station: withdraw the staged stock from storage into cargo, gift it to the resolved recipient, and mark the record done with however much actually moved. Records held by another agent, or staged at a station this worker isn't currently docked at, are left untouched — those belong to a different pass (a different agent's, or this agent's next visit). A worker that isn't currently docked anywhere is a no-op pass, not an error.

Every matching record is processed; a single record's failure (bad recipient, invalid plan state) transitions that record to failed and moves on to the next one. The returned error is reserved for a failure of the pass itself — e.g. the queue file could not be read — not a per-record outcome.

func (*WorkerDispatch) MineQty

func (d *WorkerDispatch) MineQty(ctx context.Context, itemID string, qty int, to, recipient string) error

MineQty mines ITEM at its nearest known resource POI until QTY units are in cargo (recomputed against whatever is already aboard, so a resumed run doesn't over-mine) or the belt stops yielding, then hands the haul off to RECIPIENT at TO via Deliver's empty-FROM mode (the goods are already in cargo — Deliver skips the withdraw leg and travels straight to TO).

Bounded: stops mining after MineQtyMaxDryPasses consecutive Mine() calls that grow the item's cargo count by zero, or as soon as the cargo hold has no free space left — either way, whatever got mined is delivered and this returns nil (partial success is success; the plan runner's DoneQty semantics handle shortfalls). Only a hard failure (no known resource POI at all, a Mine() error, or a Deliver error such as a bad recipient) returns an error.

func (*WorkerDispatch) Run

func (d *WorkerDispatch) Run(ctx context.Context, tokens []string) error

Run dispatches one tokenized command. Token resolution ($SYSTEM$, $STATION$, POI-type tokens) is the caller's responsibility (RunStanding resolves before calling) — Run treats tokens as literal.

func (*WorkerDispatch) SetActivitySink

func (d *WorkerDispatch) SetActivitySink(p *atomic.Pointer[string])

SetActivitySink wires the shared atomic the worker heartbeat reads so role commands can publish a "current activity" string for the fleet status page. The caller (cmd/worker) owns the pointer; the dispatch only writes through it.

func (*WorkerDispatch) Supports

func (d *WorkerDispatch) Supports(cmd string) bool

Supports reports whether cmd is in the curated worker vocabulary.

Jump to

Keyboard shortcuts

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