Skip to content

DocsGuides

Agent

Each session has one agent task: the lasting record of Pine's agent's work in that session. Every run starts a new turn on it. The agent's conversation carries over from turn to turn while the Computer's runtime is running. It isn't restored after you stop the Computer and start it again.

The examples use the Ruby SDK.

What you can do

  • session.agent.run(goal, context:, skills:, constraints:, secrets:) starts a turn and returns at once with the session's task as a plain Hash (task["task_id"], task["state"]), not an object. current_turn_id appears in status once the turn has started. The work arrives on events. context is background for this turn only. skills selects learned skills (see Skills). constraints sets max_tokens and max_wall_ms; working time defaults to 60 minutes, and time spent waiting for your answer doesn't count.
  • session.agent.steer(text, expected_turn_id:, turn_attempt:) adds instructions to the running turn without stopping it, for example "use the Q4 row instead". Pass expected_turn_id (the task's current_turn_id) so a late steer can't land on a later turn.
  • session.agent.answer(request_id, answer, expected_turn_id:) answers a needs_input question. Send the event's payload["request_id"], and the paused turn continues with your answer.
  • session.agent.cancel stops the turn in progress. The task and the agent's memory remain, so the next run continues the same conversation.
  • session.agent.reset clears the agent's memory, so the next run starts fresh. Use it before you reuse a session for unrelated work. The browser, tabs, cookies, files and event history are untouched. It works only between turns.
  • session.agent.status returns the task: its state (idle, running or paused), the current turn and the usage so far.
  • session.agent.result returns the latest finished turn's result. While a turn is in progress it raises a conflict (409) rather than return the previous turn's result.
  • session.agent.events(last_event_id:) streams everything the agent emits to the block you pass. Calling it without a block, to get an Enumerator that opens the stream when you iterate, is coming soon in the next Ruby SDK release.

A result's status and terminal_reason say how the turn ended, not whether the goal succeeded. Read its summary for the outcome, and its artifacts for the verified files the turn produced.

run, steer and answer also take secrets:, a Hash of names and values for the agent to use without putting them in the model's input. They reach the session as a file, and as a backstop their values are removed from events and results. Secrets belong to the session: each call adds to them, and they last until the session is destroyed or the Computer's services restart. Refer to a secret as $NAME in your goal or context, never by its value. SessionSecrets in the Computer API has the naming rules and limits.

The HTTP routes are in the Computer API, under Agent.

One driver at a time

A session has one driver at a time: Pine's agent or a person.

  1. A turn in progress keeps the session busy. While a turn is running, or paused on a question, another run returns 409 /errors/session-busy. Steer the turn, answer it or cancel it first. The Ruby SDK raises PineSandbox::ConflictError with problem_type set to /errors/session-busy.
  2. A person in control blocks the agent. While a person holds control, run, steer and answer return 409 /errors/control-not-held. The Ruby SDK raises PineSandbox::ControlNotHeld, a subclass of ConflictError. Rescue the class, not the status code.
  3. Skill authoring runs beside the agent. learn drafts from a completed turn, so it returns 409 /errors/session-busy while a turn is in progress. Each session runs one learn, teach or refine at a time. Author runs report on their own stream, not on agent.events (see Skill author runs).

Watching needs no control: a person can watch the desktop while the agent works. Taking control changes who can send input; see Web SDK for how a person takes and releases control.

The event stream

events replays the session's event log from your cursor, then follows it live. It is one log across all the session's turns. Each event has an event_id that increases (it can skip numbers) and a type: input, status, reasoning, command, step, screenshot, needs_input, usage, usage.finalized, result or controller_changed. Ignore types you don't know.

Delivery is at least once. Save each event_id together with the effects you apply, pass it back as last_event_id: on the next subscription, and skip events you have already applied. Applications usually follow one of three patterns:

ruby
# Pattern A: one worker waits for the next stopping point.
cursor = session_progress.last_agent_event_id
boundary = nil
session.agent.events(last_event_id: cursor) do |event|
  apply_event_and_commit_cursor!(event)
  boundary = :terminal if event.terminal?
  boundary = :needs_input if event.type == "needs_input"
  break if boundary
end
case boundary
when :needs_input then enqueue_answer_flow
when :terminal then consume_completed_turn
else requeue_agent_event_worker
end
# After a dropped connection, resume from the saved cursor:
# session.agent.events(last_event_id: session_progress.last_agent_event_id) { |event| ... }

A needs_input event pauses the turn; it is not a final result, and its terminal is false. A worker can save the question and return, then resume reading after another request answers it. An interactive application can answer inline and keep listening. A result event with terminal: true ends the turn, and the stream closes after the latest turn's result.

When you reuse a session, the stream also replays earlier turns' results. Before you call run, make sure your saved cursor is past the latest result while the task is idle. Otherwise a lagging cursor can replay an older result ahead of the new turn's events. If you can't establish that starting point, record that you started a run and ignore earlier results until you have confirmed the new turn. A new session starts with an empty cursor.

ruby
# Pattern B: send the run, and let a separate worker read the events.
# On a reused session, this is the cursor saved at the latest result.
cursor = session_progress.last_agent_event_id
task = session.agent.run("Summarise the Q3 board minutes.")
enqueue_agent_event_worker(session.name, cursor)
# task is a snapshot of the session's task, not a handle to the turn.
ruby
# Pattern C: several turns that build on each other.
session.agent.run("Find this week's customer report.")
wait_for_completed_turn # answer any needs_input questions before continuing
session.agent.run("Extract the top three risks from that report.")
# The second turn remembers what the agent found in the first.

The SDK doesn't retry a run whose response was lost, because the turn may have started. Check the task and your saved events before you send another run; identical goal text doesn't identify the earlier request. Reconnecting to events resumes reading, not work.

To show past turns without replaying the whole stream, the Computer API also has a turns index (/v1/sessions/{name}/agent/turns) and a paged event history (/v1/sessions/{name}/agent/events/page).

Keep-alive

Every event stream the Computer serves sends a : ka comment every 20 seconds. The Ruby SDK reads the agent and control event streams with no read timeout, and the keep-alive shows the connection is still open. If you read a stream without the SDK, turn off the read timeout on that connection.

The same keep-alive applies to /v1/sessions/{name}/agent/events, /v1/sessions/{name}/skills/author/{author_id}/events, /v1/sessions/{name}/control/events and /sessions/{name}/exec, so one transport serves all four.

Skill author runs

learn, teach and refine (see Skills) each start an author run, separate from the session's agent task. Follow one at /v1/sessions/{name}/skills/author/{author_id}/events. It uses the same event format and keep-alive as the agent stream, and ends with one result event.

Errors

The Ruby SDK turns RFC 9457 problem responses into typed exceptions under PineSandbox::. HTTP errors descend from PineSandbox::ApiError, which carries status, problem_type and request_id. TokenRejectedError is a PineSandbox::BindError. These are the ones to handle:

Error Cause What to do
PineSandbox::ConflictError, and its subclass PineSandbox::ControlNotHeld /errors/session-busy: a turn is in progress, or learn found no completed turn. /errors/author-in-flight: another author run is in progress. /errors/no-active-task: steer, answer or cancel with no turn in progress. /errors/stale-turn: the turn you named is no longer current. ControlNotHeld: a person holds control. Read error.problem_type, then steer, answer or cancel the turn, or wait.
PineSandbox::NotFoundError, and its subclasses PineSandbox::SessionNotFound and PineSandbox::SandboxGoneError /errors/task-not-found: the session hasn't run the agent yet. SessionNotFound: the session was destroyed before your call arrived. SandboxGoneError: the Computer's runtime is gone. After task-not-found, the session is ready for its first run. Create a destroyed session again. Start the Computer again only when its runtime is gone.
PineSandbox::TokenRejectedError (Computer, skills and authoring calls) or PineSandbox::UnauthorizedError (session.agent.* calls) The Computer rejected the ct_ or ps_ token (401). A ct_ token lasts as long as the attach, so on a running Computer this means the attach lost its authorization. Otherwise the token is wrong or the runtime is gone. The SDK doesn't fetch new tokens for you: starting the Computer again lands on a new runtime and ends every session token. Treat the error as a report (in the Go SDK it's pinesandbox.TokenRejectedError). Check the Computer's state, and start it again only once you've confirmed its runtime is gone; then save the new credentials and retry.

Rescue the class, not the HTTP status. The status can change; the class stays the same across releases.

Private beta Approved developers get the integration guides, the API reference and the SDKs.