Skip to content

Add OHS endpoints - #1021

Open
SSharma-10 wants to merge 43 commits into
mainfrom
OHS_endpoints
Open

Add OHS endpoints#1021
SSharma-10 wants to merge 43 commits into
mainfrom
OHS_endpoints

Conversation

@SSharma-10

Copy link
Copy Markdown
Contributor

No description provided.

Comment thread hosted_agents.go
Comment thread hosted_agents.go
if create == nil {
return nil, nil, errors.New("hosted agents: create request is required")
}
if create.AgentKind == "" || create.AgentKind == HostedAgentKindUnspecified {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will be moving to something like doctl agents start --spec /path/to/agent-spec.yaml for v0, so this arg will be specified in the agent spec file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, doctl parses the spec and fills agent_kind before calling godo. so this stays as-is since the API still expects it on CreateSession

Comment thread hosted_agents.go Outdated
if opt != nil {
q := url.Values{}
if opt.ReplayFrom != "" {
q.Set("replay_from", opt.ReplayFrom)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to store last_seen_event_id in doctl cache (in a local file)? Just wanted to confirm for our backend design, no implementation required as of yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can do that

Comment thread hosted_agents.go
Comment thread hosted_agents.go Outdated
Comment thread hosted_agents.go
}

// ExecInSandbox runs a command inside the session sandbox.
func (s *HostedAgentsServiceOp) ExecInSandbox(ctx context.Context, sessionID string, body *HostedAgentSandboxExecRequest) (*HostedAgentSandboxExecResponse, *Response, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Details on this from a backend perspective are a bit unclear so, this might require another review later.

}

client := godo.NewFromToken(token)
if baseURL := os.Getenv("DIGITALOCEAN_API_URL"); baseURL != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this will get removed from examples once api.digitalocean.com/v2/... endpoints are available, because default URL domain is api.digitalocean.com

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Comment thread hosted_agents.go Outdated
Comment thread hosted_agents.go
type HostedAgentHITLActionKind string

const (
HostedAgentHITLActionUnspecified HostedAgentHITLActionKind = "HITL_ACTION_KIND_UNSPECIFIED"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see these enum values are not enforced, should we even do it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the existing pattern since godo does not enforce every enum on the client

SSharma-10 and others added 23 commits June 3, 2026 15:14
Per PR review, OAuth is out of scope for v0 (GitHub creds come from the
agent spec). Also simplify sessions base path to /v2/agents/sessions.

Co-authored-by: Cursor <[email protected]>
The session SSE stream serializes the SPI canonical event envelope
(type/data/timestamp/tenant_id with dot-separated event names), not the
proto-style kind/payload/at/team_id shape HostedAgentEvent previously
expected. Decode the real wire via a custom UnmarshalJSON and switch the
HostedAgentEventKind constants to the dot-separated SPI names, so streamed
events are mapped onto Kind/Payload/At/TeamID instead of being dropped.

Co-authored-by: Cursor <[email protected]>
Some services (e.g. the hosted-agents harness-api) return errors as
{"error":{"code":...,"message":...}} rather than the top-level
{"message":...} shape. Capture that nested envelope on ErrorResponse and
fall back to its message in Error() so callers see the server's reason
instead of a bare status code.

Co-authored-by: Cursor <[email protected]>
nveerdixit and others added 7 commits July 28, 2026 16:34
…1067)

* hosted agents: add Session.origin product provenance

Port MARSOHS-551 origin types onto OHS_endpoints so create/get/list
can carry direct|simulation|evaluation provenance. Document that
ListSessions omits sim/eval server-side.

Co-authored-by: Cursor <[email protected]>

* Remove Unreleased CHANGELOG entry for OHS beta work

CHANGELOG is reserved for GA releases; OHS ships via the beta process.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Live session streaming moved off the control plane's
`/v2/agents/sessions/{id}/stream` onto the data plane's
`/v2/agents/sessions/{id}/events`, so point StreamSession there.

The two surfaces are not interchangeable, so the move is a split rather than
a rename:

  - Live reads go to `/events`. Delivery is forward-only from the moment of
    attach, and the resume cursor rides in the standard `Last-Event-ID`
    header instead of a query parameter.
  - `ReplayOnly` reads stay on `/stream?replay_only=true`. The control plane
    owns the stored event history; the data-plane endpoint holds none, so
    sending a replay-only read there would hang on a stream that never
    yields the history it was asked for.

The live stream also carries `stream.state` transport control frames
reporting connection health (live / catching_up / degraded / superseded).
They arrive in the same canonical envelope as an event, so they decode
through the same parser; name the kind and its payload so callers can
identify and skip them rather than rendering them as session activity.

Co-Authored-By: Claude Opus 5 <[email protected]>
…dpoint

The previous commit moved live streaming onto the data plane but left
replay-only reads (`doctl agents logs`) on the control plane's
`/stream?replay_only=true`, because the data-plane endpoint held no history
of its own -- it delivered forward-only from the moment of attach.

The data plane now stores and serves that history, so both reads move onto
`/v2/agents/sessions/{id}/events` and the split disappears:

  - Live is unchanged: forward-only from attach, connection held open,
    ReplayFrom sent as Last-Event-ID.
  - ReplayOnly adds `?replay_only=true`. The server writes the session's
    stored history and then ends the stream, so the read terminates on its
    own rather than blocking on a connection that never closes.

ReplayFrom keeps riding the `replay_from` query parameter on a replay-only
read rather than moving to Last-Event-ID with the live lane. The two are not
the same cursor: on a live attach it is a resume hint the server is free to
widen for context, while on a history read it is an explicit pagination
point, and collapsing them would let a history query silently return events
the caller already has.

`stream.state` frames now appear on both reads. A replay-only read reports
`catching_up` and then simply ends -- it never reaches `live`, since there is
no live tail to join.

Co-Authored-By: Claude Opus 5 <[email protected]>
Co-authored-by: Cursor <[email protected]>
SSharma-10 and others added 10 commits August 7, 2026 11:11
* hosted agents: page session history backwards from a cursor

harness-api now bounds a cursorless replay to the newest window of a
session's history, so a replay-only attach no longer returns everything.
Reading older events means walking backwards a page at a time.

StreamSession gains Before and Limit, and HasMore reports whether the
walk can continue. The server ends a page with a "has_more" SSE comment,
which SSEReader dropped along with every other comment, so it grows an
OnComment hook to make comment lines observable without dispatching them
as events.

Co-authored-by: Cursor <[email protected]>

* hosted agents: exercise history paging from the stream-session example

The example could only replay forwards, so there was no way to try the
backward walk by hand. It now reads BEFORE and LIMIT, and closes with the
event count, the oldest id seen, and has_more, so the printed cursor can
be fed straight back in as the next BEFORE.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
* hosted agents: add provider OAuth connect endpoints

Add StartProviderAuth (POST /v2/agents/auth/{provider}) and
PollProviderAuth (GET /v2/agents/auth/{provider}/poll) to the
HostedAgents service, with response types, tests, and an example.
These support the team-scoped external-provider (e.g. GitHub) connect
flow used to authorize git operations in hosted agent sessions.

Co-authored-by: Cursor <[email protected]>

* hosted agents: use *Timestamp for provider-auth ExpiresAt

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: SSharma-10 <[email protected]>
…urface (#1083)

The canonical event and input surface is a lossy rendering of what the agent
runtimes actually speak, which is the right trade for clients that just want
"the agent said something" and the wrong one for a client that speaks the
session's own protocol and is trying to reproduce its behaviour faithfully.
Three additions let such a client opt into the native frames without changing
anything for callers that don't.

Outbound: HostedAgentEvent gains SourceEventID, SourceEventType and
SourceRaw — the runtime's own event id, its type label, and the exact
pre-mapping bytes. Raw payloads meaningfully fatten every event, so they are
opt-in behind StreamSessionOptions.IncludeRaw (include_raw=true).

Inbound: SendInputRequest and ResolveHITLRequest gain SourceRaw, so the frame
an input was reduced from, and a reply the outcome enum cannot express (an
elicitation's content, a tool's requested input), survive the trip. Text and
Outcome stay required alongside them — they remain what non-native clients
send and what the audit trail records.

New: RelayRequest (POST .../request) forwards one native request frame and
returns the agent's reply verbatim. Where SendInput carries the single
message with a canonical meaning, this carries everything else a protocol
client needs to ask — for codex, the requests behind interrupts, slash
commands and model pickers. A JSON-RPC error object is the agent answering
and arrives as a normal reply; an empty reply means the in-sandbox adapter
declined the method, which callers must not mistake for an answer.

Every field is additive and omitempty, so existing callers see no change on
the wire.

Co-authored-by: Cursor <[email protected]>
…nt (stack on OHS_endpoints) (#1072)

* hosted agents: stream live events from the data-plane /events endpoint

Live session streaming moved off the control plane's
`/v2/agents/sessions/{id}/stream` onto the data plane's
`/v2/agents/sessions/{id}/events`, so point StreamSession there.

The two surfaces are not interchangeable, so the move is a split rather than
a rename:

  - Live reads go to `/events`. Delivery is forward-only from the moment of
    attach, and the resume cursor rides in the standard `Last-Event-ID`
    header instead of a query parameter.
  - `ReplayOnly` reads stay on `/stream?replay_only=true`. The control plane
    owns the stored event history; the data-plane endpoint holds none, so
    sending a replay-only read there would hang on a stream that never
    yields the history it was asked for.

The live stream also carries `stream.state` transport control frames
reporting connection health (live / catching_up / degraded / superseded).
They arrive in the same canonical envelope as an event, so they decode
through the same parser; name the kind and its payload so callers can
identify and skip them rather than rendering them as session activity.

Co-Authored-By: Claude Opus 5 <[email protected]>

* hosted agents: serve replay-only reads from the data-plane /events endpoint

The previous commit moved live streaming onto the data plane but left
replay-only reads (`doctl agents logs`) on the control plane's
`/stream?replay_only=true`, because the data-plane endpoint held no history
of its own -- it delivered forward-only from the moment of attach.

The data plane now stores and serves that history, so both reads move onto
`/v2/agents/sessions/{id}/events` and the split disappears:

  - Live is unchanged: forward-only from attach, connection held open,
    ReplayFrom sent as Last-Event-ID.
  - ReplayOnly adds `?replay_only=true`. The server writes the session's
    stored history and then ends the stream, so the read terminates on its
    own rather than blocking on a connection that never closes.

ReplayFrom keeps riding the `replay_from` query parameter on a replay-only
read rather than moving to Last-Event-ID with the live lane. The two are not
the same cursor: on a live attach it is a resume hint the server is free to
widen for context, while on a history read it is an explicit pagination
point, and collapsing them would let a history query silently return events
the caller already has.

`stream.state` frames now appear on both reads. A replay-only read reports
`catching_up` and then simply ends -- it never reaches `live`, since there is
no live tail to join.

Co-Authored-By: Claude Opus 5 <[email protected]>
Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: SSharma-10 <[email protected]>
… (MARSOHS-741) (#1086)

* Add Hosted Agents Agent Configs client and config-backed session create

Add the Agent Configs client (list/get/create/delete + list-sessions-by-config)
under /v2/agents/configs, matching the harness-api REST contracts:

- Create body is {name, manifest_yaml}; credentials are declared inline in the
  manifest under spec.secrets and the API rejects the retired secrets /
  oauth_assignments maps.
- HostedAgentConfig carries create-time warnings; credential slots echo only
  the manifest declaration ({name, source, provider}).

Also add CreateSessionFromConfig, which creates a session from an existing
immutable config by posting a JSON {name, config_id} body to
POST /v2/agents/sessions (the JSON content type selects the server's
config-backed create path).

Co-authored-by: Cursor <[email protected]>

* Address PR review: add ConfigID to HostedAgentSession, tidy CreateAgentConfig doc

- Add ConfigID field to HostedAgentSession; harness-api returns config_id on
  session create/get/list (the durable Agent Config the session was created from)
- Reword CreateAgentConfig godoc to reflect that secrets live in the manifest
  (spec.secrets[].value), not as separate request fields
- Assert config_id round-trips in the config-backed session create test

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants