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_idappears instatusonce the turn has started. The work arrives onevents.contextis background for this turn only.skillsselects learned skills (see Skills).constraintssetsmax_tokensandmax_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". Passexpected_turn_id(the task'scurrent_turn_id) so a late steer can't land on a later turn.session.agent.answer(request_id, answer, expected_turn_id:)answers aneeds_inputquestion. Send the event'spayload["request_id"], and the paused turn continues with your answer.session.agent.cancelstops the turn in progress. The task and the agent's memory remain, so the nextruncontinues the same conversation.session.agent.resetclears the agent's memory, so the nextrunstarts 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.statusreturns the task: its state (idle,runningorpaused), the current turn and the usage so far.session.agent.resultreturns 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 anEnumeratorthat 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.
- A turn in progress keeps the session busy. While a turn is running, or
paused on a question, another
runreturns409 /errors/session-busy. Steer the turn, answer it or cancel it first. The Ruby SDK raisesPineSandbox::ConflictErrorwithproblem_typeset to/errors/session-busy. - A person in control blocks the agent. While a person holds control,
run,steerandanswerreturn409 /errors/control-not-held. The Ruby SDK raisesPineSandbox::ControlNotHeld, a subclass ofConflictError. Rescue the class, not the status code. - Skill authoring runs beside the agent.
learndrafts from a completed turn, so it returns409 /errors/session-busywhile a turn is in progress. Each session runs onelearn,teachorrefineat a time. Author runs report on their own stream, not onagent.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:
# 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.
# 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.
# 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.