From f6d8db8d559112d3066706f2d7dec6fd891308ba Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 19:27:07 +0800
Subject: [PATCH] feat(sdk): expose stable usage and managed control APIs
Add Forward credential rotation and hourly Usage aggregation, plus Managed Session cancellation and deployment-scoped Run queries.
Include typed public exports, contract fixtures, live scenarios, generated API documentation, and changelog entries.
---
CHANGELOG.md | 6 +
CONTRIBUTING.md | 14 +
README.md | 37 +-
docs/api/reference.md | 620 ++++++++++++++++++++++
forward/api_expansion_live_test.go | 93 ++++
forward/api_expansion_test.go | 148 ++++++
forward/client.go | 2 +
forward/client_test.go | 4 +-
forward/test_helpers_test.go | 7 +-
forward/testdata/api-contracts.json | 96 ++++
forward/testdata/api-operation-cases.json | 158 +++++-
forward/usage.go | 125 +++++
forward/vault_live_test.go | 27 +-
forward/vaultcredential.go | 109 ++++
managed/api_expansion_test.go | 77 +++
managed/client_test.go | 2 +-
managed/deployment.go | 2 +
managed/deployment_execution_live_test.go | 15 +
managed/deployment_live_test.go | 6 +
managed/deploymentrun_live_test.go | 35 ++
managed/deploymentscopedrun.go | 100 ++++
managed/session.go | 43 ++
managed/session_cancel_live_test.go | 50 ++
managed/session_live_test.go | 7 +
managed/test_helpers_test.go | 2 +-
managed/testdata/api-contracts.json | 72 +++
26 files changed, 1848 insertions(+), 9 deletions(-)
create mode 100644 forward/api_expansion_live_test.go
create mode 100644 forward/api_expansion_test.go
create mode 100644 forward/usage.go
create mode 100644 managed/api_expansion_test.go
create mode 100644 managed/deploymentscopedrun.go
create mode 100644 managed/session_cancel_live_test.go
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 572d5bc..16b7440 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ existing `0.1.0` release; earlier development prereleases are not listed.
## [Unreleased]
+### Added
+
+- Forward Vault Credential updates for secret rotation and metadata merge patches, with automatic retries disabled for write-only updates.
+- Forward Usage aggregation by Identity and Template using hourly `start_at` / `end_at` windows in Asia/Shanghai, with fractional `active_seconds` and multi-ID filters. Legacy timestamp parameters are not exposed.
+- Managed Session cancellation with the lightweight acknowledgement for both active and idle sessions, plus deployment-scoped Run listing and retrieval.
+
## [0.2.0] - 2026-09-24
### Changed
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a0b3e21..49595aa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,6 +36,20 @@ go build ./examples/...
Never commit `.env.live`, tokens, credentials, generated logs, or test output. Live tests must register cleanup immediately after creating a resource.
+`make test-live-all` includes Forward hourly Usage, Credential merge patches
+and secret redaction, and Managed Session cancellation before and after sending
+a turn. Managed deployment tests cover scoped Run listing/retrieval. Active
+cancellation and scoped Run execution require the corresponding
+`LIVE_ALLOW_WRITE=true` and `LIVE_ALLOW_EXECUTION=true` gates. Use a separate
+`LIVE_ENV_FILE` with matching URL and PAT for CN and Global.
+
+Usage queries the last 24 completed whole hours in Asia/Shanghai in both regions;
+empty pages verify only the collection. A Session may finish before cancellation
+and return HTTP 200 instead of 202; tests record which response occurred. Those
+responses do not prove active cancellation occurred. Cleanup failures remain
+failures; offline replay exercises assertions and cleanup without account
+credentials.
+
## API and contract changes
When adding or changing an endpoint:
diff --git a/README.md b/README.md
index 1ac877c..ca57b9e 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,13 @@ The SDK ships two clients that share authentication, transport, retries, error h
| Mode | Package | Resources |
| --- | --- | --- |
-| Forward | `forward` | Identity, Template, Session, Schedule, Batch, Channel, Environment, File, Skill, Vault, MemoryStore, Model |
+| Forward | `forward` | Identity, Template, Session, Schedule, Batch, Channel, Environment, File, Skill, Vault, MemoryStore, Model, Usage |
| Managed | `managed` | Agent, Session, Deployment, Dream, Environment, File, Skill, Vault, MemoryStore, Model |
- [Installation](#installation)
- [Requirements](#requirements)
- [Usage](#usage)
+- [Usage and cancellation](#usage-and-cancellation)
- [Conversations](#conversations)
- [System prompts and tools](#system-prompts-and-tools)
- [Streaming](#streaming)
@@ -115,6 +116,40 @@ The remaining examples are function fragments meant to be dropped into an applic
| `option` | `github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option` |
| `param` | `github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param` |
+## Usage and cancellation
+
+Forward Usage requires PAT or Admin SAT.
+
+```go
+usage, err := client.Usage.ListIdentities(ctx, forward.UsageListParams{
+ StartAt: "2026-09-14T09:00:00",
+ EndAt: "2026-09-14T12:00:00",
+ IdentityIDs: []string{"idn_one", "idn_two"},
+})
+if err != nil {
+ return err
+}
+for _, item := range usage.Data {
+ fmt.Println(item.IdentityID, item.ActiveSeconds, item.Credits)
+}
+```
+
+`Usage.ListTemplates` uses the same filters. Both bounds are whole hours in
+Asia/Shanghai, including Global: the start is inclusive, the end exclusive, and
+the maximum window is 744 hours. Use `ActiveSeconds` without truncating fractions.
+Legacy `start_time`, `end_time`, and `duration_seconds` are not exposed.
+
+Rotate Forward secrets with `client.Vaults.Credentials.Update`; only `auth` and
+`metadata` are patched, and omitted fields are preserved. Use
+`param.NullMap[map[string]any]()` to clear metadata, or a map value of `nil` to remove
+a key. Updates never retry automatically; secret values are not returned by GET.
+
+Managed adds `client.Sessions.Cancel(ctx, sessionID, managed.SessionCancelParams{})`,
+which returns a lightweight `canceling` acknowledgement (HTTP 202 for active work,
+200 for an idle no-op). It also exposes `client.Deployments.Runs.List` and `Get`,
+with the deployment ID as a path argument; the existing global `DeploymentRuns`
+resource remains available.
+
## Conversations
Forward creates a Session from an Identity and a Template. The fragments below assume both already exist, with a usable execution environment configured on the Template; [the Forward session example](examples/forward/session/main.go) shows how to create them.
diff --git a/docs/api/reference.md b/docs/api/reference.md
index 641251b..df68d84 100644
--- a/docs/api/reference.md
+++ b/docs/api/reference.md
@@ -185,6 +185,9 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [type EnvironmentVariableOverrideParam](<#EnvironmentVariableOverrideParam>)
- [func \(r EnvironmentVariableOverrideParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#EnvironmentVariableOverrideParam.MarshalJSON>)
- [func \(r \*EnvironmentVariableOverrideParam\) UnmarshalJSON\(data \[\]byte\) error](<#EnvironmentVariableOverrideParam.UnmarshalJSON>)
+- [type EnvironmentVariableUpdateParam](<#EnvironmentVariableUpdateParam>)
+ - [func \(r EnvironmentVariableUpdateParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#EnvironmentVariableUpdateParam.MarshalJSON>)
+ - [func \(r \*EnvironmentVariableUpdateParam\) UnmarshalJSON\(data \[\]byte\) error](<#EnvironmentVariableUpdateParam.UnmarshalJSON>)
- [type EnvironmentVariablesUnionParam](<#EnvironmentVariablesUnionParam>)
- [func \(r EnvironmentVariablesUnionParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#EnvironmentVariablesUnionParam.MarshalJSON>)
- [func \(r \*EnvironmentVariablesUnionParam\) UnmarshalJSON\(data \[\]byte\) error](<#EnvironmentVariablesUnionParam.UnmarshalJSON>)
@@ -288,12 +291,21 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [type IdentityUpdateParams](<#IdentityUpdateParams>)
- [func \(r IdentityUpdateParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityUpdateParams.MarshalJSON>)
- [func \(r \*IdentityUpdateParams\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityUpdateParams.UnmarshalJSON>)
+- [type IdentityUsage](<#IdentityUsage>)
+ - [func \(r IdentityUsage\) RawJSON\(\) string](<#IdentityUsage.RawJSON>)
+ - [func \(r \*IdentityUsage\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityUsage.UnmarshalJSON>)
- [type ImageSource](<#ImageSource>)
- [func \(r ImageSource\) RawJSON\(\) string](<#ImageSource.RawJSON>)
- [func \(r \*ImageSource\) UnmarshalJSON\(data \[\]byte\) error](<#ImageSource.UnmarshalJSON>)
- [type ImageSourceParam](<#ImageSourceParam>)
- [func \(r ImageSourceParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#ImageSourceParam.MarshalJSON>)
- [func \(r \*ImageSourceParam\) UnmarshalJSON\(data \[\]byte\) error](<#ImageSourceParam.UnmarshalJSON>)
+- [type MCPOAuthRefreshUpdateParam](<#MCPOAuthRefreshUpdateParam>)
+ - [func \(r MCPOAuthRefreshUpdateParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#MCPOAuthRefreshUpdateParam.MarshalJSON>)
+ - [func \(r \*MCPOAuthRefreshUpdateParam\) UnmarshalJSON\(data \[\]byte\) error](<#MCPOAuthRefreshUpdateParam.UnmarshalJSON>)
+- [type MCPOAuthUpdateParam](<#MCPOAuthUpdateParam>)
+ - [func \(r MCPOAuthUpdateParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#MCPOAuthUpdateParam.MarshalJSON>)
+ - [func \(r \*MCPOAuthUpdateParam\) UnmarshalJSON\(data \[\]byte\) error](<#MCPOAuthUpdateParam.UnmarshalJSON>)
- [type MCPServer](<#MCPServer>)
- [func \(r MCPServer\) RawJSON\(\) string](<#MCPServer.RawJSON>)
- [func \(r \*MCPServer\) UnmarshalJSON\(data \[\]byte\) error](<#MCPServer.UnmarshalJSON>)
@@ -630,6 +642,9 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [func \(r \*SkillVersionService\) List\(ctx context.Context, id string, params SkillVersionListParams, opts ...option.RequestOption\) \(res \*pagination.PageCursor\[SkillVersion\], err error\)](<#SkillVersionService.List>)
- [func \(r \*SkillVersionService\) ListAutoPaging\(ctx context.Context, id string, params SkillVersionListParams, opts ...option.RequestOption\) \*pagination.PageCursorAutoPager\[SkillVersion\]](<#SkillVersionService.ListAutoPaging>)
- [func \(r \*SkillVersionService\) New\(ctx context.Context, id string, params SkillVersionNewParams, opts ...option.RequestOption\) \(res \*SkillVersion, err error\)](<#SkillVersionService.New>)
+- [type StaticBearerUpdateParam](<#StaticBearerUpdateParam>)
+ - [func \(r StaticBearerUpdateParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#StaticBearerUpdateParam.MarshalJSON>)
+ - [func \(r \*StaticBearerUpdateParam\) UnmarshalJSON\(data \[\]byte\) error](<#StaticBearerUpdateParam.UnmarshalJSON>)
- [type SystemOverride](<#SystemOverride>)
- [func \(r SystemOverride\) RawJSON\(\) string](<#SystemOverride.RawJSON>)
- [func \(r \*SystemOverride\) UnmarshalJSON\(data \[\]byte\) error](<#SystemOverride.UnmarshalJSON>)
@@ -661,6 +676,9 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [type TemplateUpdateParams](<#TemplateUpdateParams>)
- [func \(r TemplateUpdateParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#TemplateUpdateParams.MarshalJSON>)
- [func \(r \*TemplateUpdateParams\) UnmarshalJSON\(data \[\]byte\) error](<#TemplateUpdateParams.UnmarshalJSON>)
+- [type TemplateUsage](<#TemplateUsage>)
+ - [func \(r TemplateUsage\) RawJSON\(\) string](<#TemplateUsage.RawJSON>)
+ - [func \(r \*TemplateUsage\) UnmarshalJSON\(data \[\]byte\) error](<#TemplateUsage.UnmarshalJSON>)
- [type Tool](<#Tool>)
- [func \(r Tool\) RawJSON\(\) string](<#Tool.RawJSON>)
- [func \(r \*Tool\) UnmarshalJSON\(data \[\]byte\) error](<#Tool.UnmarshalJSON>)
@@ -679,6 +697,15 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [type ToolParam](<#ToolParam>)
- [func \(r ToolParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#ToolParam.MarshalJSON>)
- [func \(r \*ToolParam\) UnmarshalJSON\(data \[\]byte\) error](<#ToolParam.UnmarshalJSON>)
+- [type UsageListParams](<#UsageListParams>)
+ - [func \(r UsageListParams\) URLQuery\(\) \(url.Values, error\)](<#UsageListParams.URLQuery>)
+ - [func \(r \*UsageListParams\) UnmarshalJSON\(data \[\]byte\) error](<#UsageListParams.UnmarshalJSON>)
+- [type UsageService](<#UsageService>)
+ - [func NewUsageService\(opts ...option.RequestOption\) UsageService](<#NewUsageService>)
+ - [func \(r \*UsageService\) ListIdentities\(ctx context.Context, params UsageListParams, opts ...option.RequestOption\) \(res \*pagination.Page\[IdentityUsage\], err error\)](<#UsageService.ListIdentities>)
+ - [func \(r \*UsageService\) ListIdentitiesAutoPaging\(ctx context.Context, params UsageListParams, opts ...option.RequestOption\) \*pagination.PageAutoPager\[IdentityUsage\]](<#UsageService.ListIdentitiesAutoPaging>)
+ - [func \(r \*UsageService\) ListTemplates\(ctx context.Context, params UsageListParams, opts ...option.RequestOption\) \(res \*pagination.Page\[TemplateUsage\], err error\)](<#UsageService.ListTemplates>)
+ - [func \(r \*UsageService\) ListTemplatesAutoPaging\(ctx context.Context, params UsageListParams, opts ...option.RequestOption\) \*pagination.PageAutoPager\[TemplateUsage\]](<#UsageService.ListTemplatesAutoPaging>)
- [type Vault](<#Vault>)
- [func \(r Vault\) RawJSON\(\) string](<#Vault.RawJSON>)
- [func \(r \*Vault\) UnmarshalJSON\(data \[\]byte\) error](<#Vault.UnmarshalJSON>)
@@ -700,6 +727,14 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [func \(r \*VaultCredentialService\) List\(ctx context.Context, id string, params VaultCredentialListParams, opts ...option.RequestOption\) \(res \*pagination.PageCursor\[VaultCredential\], err error\)](<#VaultCredentialService.List>)
- [func \(r \*VaultCredentialService\) ListAutoPaging\(ctx context.Context, id string, params VaultCredentialListParams, opts ...option.RequestOption\) \*pagination.PageCursorAutoPager\[VaultCredential\]](<#VaultCredentialService.ListAutoPaging>)
- [func \(r \*VaultCredentialService\) New\(ctx context.Context, id string, params VaultCredentialNewParams, opts ...option.RequestOption\) \(res \*VaultCredential, err error\)](<#VaultCredentialService.New>)
+ - [func \(r \*VaultCredentialService\) Update\(ctx context.Context, id string, credID string, params VaultCredentialUpdateParams, opts ...option.RequestOption\) \(res \*VaultCredential, err error\)](<#VaultCredentialService.Update>)
+- [type VaultCredentialUpdateAuthUnionParam](<#VaultCredentialUpdateAuthUnionParam>)
+ - [func \(r VaultCredentialUpdateAuthUnionParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#VaultCredentialUpdateAuthUnionParam.MarshalJSON>)
+ - [func \(r \*VaultCredentialUpdateAuthUnionParam\) UnmarshalJSON\(data \[\]byte\) error](<#VaultCredentialUpdateAuthUnionParam.UnmarshalJSON>)
+- [type VaultCredentialUpdateParams](<#VaultCredentialUpdateParams>)
+ - [func \(r VaultCredentialUpdateParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#VaultCredentialUpdateParams.MarshalJSON>)
+ - [func \(r VaultCredentialUpdateParams\) URLQuery\(\) \(url.Values, error\)](<#VaultCredentialUpdateParams.URLQuery>)
+ - [func \(r \*VaultCredentialUpdateParams\) UnmarshalJSON\(data \[\]byte\) error](<#VaultCredentialUpdateParams.UnmarshalJSON>)
- [type VaultListParams](<#VaultListParams>)
- [func \(r VaultListParams\) URLQuery\(\) \(url.Values, error\)](<#VaultListParams.URLQuery>)
- [type VaultNewParams](<#VaultNewParams>)
@@ -2000,6 +2035,7 @@ type Client struct {
Skills SkillService
Vaults VaultService
MemoryStores MemoryStoreService
+ Usage UsageService
Models ModelService
}
```
@@ -3011,6 +3047,37 @@ func (r *EnvironmentVariableOverrideParam) UnmarshalJSON(data []byte) error
+
+## type [EnvironmentVariableUpdateParam]()
+
+
+
+```go
+type EnvironmentVariableUpdateParam struct {
+ Type string `json:"type" api:"required"`
+ SecretValue param.Opt[string] `json:"secret_value,omitzero"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(EnvironmentVariableUpdateParam\) [MarshalJSON]()
+
+```go
+func (r EnvironmentVariableUpdateParam) MarshalJSON() ([]byte, error)
+```
+
+
+
+
+### func \(\*EnvironmentVariableUpdateParam\) [UnmarshalJSON]()
+
+```go
+func (r *EnvironmentVariableUpdateParam) UnmarshalJSON(data []byte) error
+```
+
+
+
## type [EnvironmentVariablesUnionParam]()
@@ -4300,6 +4367,48 @@ func (r *IdentityUpdateParams) UnmarshalJSON(data []byte) error
+
+## type [IdentityUsage]()
+
+
+
+```go
+type IdentityUsage struct {
+ Type string `json:"type"`
+ IdentityID string `json:"identity_id"`
+ SessionCount int64 `json:"session_count"`
+ ActiveSeconds float64 `json:"active_seconds"`
+ Credits float64 `json:"credits"`
+ JSON struct {
+ Type respjson.Field
+ IdentityID respjson.Field
+ SessionCount respjson.Field
+ ActiveSeconds respjson.Field
+ Credits respjson.Field
+ ExtraFields map[string]respjson.Field
+ // contains filtered or unexported fields
+ } `json:"-"`
+}
+```
+
+
+### func \(IdentityUsage\) [RawJSON]()
+
+```go
+func (r IdentityUsage) RawJSON() string
+```
+
+
+
+
+### func \(\*IdentityUsage\) [UnmarshalJSON]()
+
+```go
+func (r *IdentityUsage) UnmarshalJSON(data []byte) error
+```
+
+
+
## type [ImageSource]()
@@ -4376,6 +4485,71 @@ func (r *ImageSourceParam) UnmarshalJSON(data []byte) error
+
+## type [MCPOAuthRefreshUpdateParam]()
+
+
+
+```go
+type MCPOAuthRefreshUpdateParam struct {
+ RefreshToken param.Opt[string] `json:"refresh_token,omitzero"`
+ Scope param.Opt[string] `json:"scope,omitzero"`
+ TokenEndpointAuth map[string]any `json:"token_endpoint_auth,omitzero"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(MCPOAuthRefreshUpdateParam\) [MarshalJSON]()
+
+```go
+func (r MCPOAuthRefreshUpdateParam) MarshalJSON() ([]byte, error)
+```
+
+
+
+
+### func \(\*MCPOAuthRefreshUpdateParam\) [UnmarshalJSON]()
+
+```go
+func (r *MCPOAuthRefreshUpdateParam) UnmarshalJSON(data []byte) error
+```
+
+
+
+
+## type [MCPOAuthUpdateParam]()
+
+
+
+```go
+type MCPOAuthUpdateParam struct {
+ Type string `json:"type" api:"required"`
+ AccessToken param.Opt[string] `json:"access_token,omitzero"`
+ ExpiresAt param.Opt[string] `json:"expires_at,omitzero"`
+ Refresh MCPOAuthRefreshUpdateParam `json:"refresh,omitzero"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(MCPOAuthUpdateParam\) [MarshalJSON]()
+
+```go
+func (r MCPOAuthUpdateParam) MarshalJSON() ([]byte, error)
+```
+
+
+
+
+### func \(\*MCPOAuthUpdateParam\) [UnmarshalJSON]()
+
+```go
+func (r *MCPOAuthUpdateParam) UnmarshalJSON(data []byte) error
+```
+
+
+
## type [MCPServer]()
@@ -8653,6 +8827,37 @@ func (r *SkillVersionService) New(ctx context.Context, id string, params SkillVe
Create Skill version
+
+## type [StaticBearerUpdateParam]()
+
+
+
+```go
+type StaticBearerUpdateParam struct {
+ Type string `json:"type" api:"required"`
+ Token param.Opt[string] `json:"token,omitzero"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(StaticBearerUpdateParam\) [MarshalJSON]()
+
+```go
+func (r StaticBearerUpdateParam) MarshalJSON() ([]byte, error)
+```
+
+
+
+
+### func \(\*StaticBearerUpdateParam\) [UnmarshalJSON]()
+
+```go
+func (r *StaticBearerUpdateParam) UnmarshalJSON(data []byte) error
+```
+
+
+
## type [SystemOverride]()
@@ -9098,6 +9303,50 @@ func (r *TemplateUpdateParams) UnmarshalJSON(data []byte) error
+
+## type [TemplateUsage]()
+
+
+
+```go
+type TemplateUsage struct {
+ Type string `json:"type"`
+ TemplateID string `json:"template_id"`
+ ActiveIdentities int64 `json:"active_identities"`
+ SessionCount int64 `json:"session_count"`
+ ActiveSeconds float64 `json:"active_seconds"`
+ Credits float64 `json:"credits"`
+ JSON struct {
+ Type respjson.Field
+ TemplateID respjson.Field
+ ActiveIdentities respjson.Field
+ SessionCount respjson.Field
+ ActiveSeconds respjson.Field
+ Credits respjson.Field
+ ExtraFields map[string]respjson.Field
+ // contains filtered or unexported fields
+ } `json:"-"`
+}
+```
+
+
+### func \(TemplateUsage\) [RawJSON]()
+
+```go
+func (r TemplateUsage) RawJSON() string
+```
+
+
+
+
+### func \(\*TemplateUsage\) [UnmarshalJSON]()
+
+```go
+func (r *TemplateUsage) UnmarshalJSON(data []byte) error
+```
+
+
+
## type [Tool]()
@@ -9320,6 +9569,100 @@ func (r *ToolParam) UnmarshalJSON(data []byte) error
+
+## type [UsageListParams]()
+
+UsageListParams accepts only hourly parameters. StartAt is inclusive; EndAt is exclusive, with a maximum span of 744 hours. Both use YYYY\-MM\-DDTHH:00:00 in Asia/Shanghai for CN and Global.
+
+```go
+type UsageListParams struct {
+ StartAt string `query:"start_at" json:"-" api:"required"`
+ EndAt string `query:"end_at" json:"-" api:"required"`
+ Limit param.Opt[int64] `query:"limit,omitzero" json:"-"`
+ AfterID param.Opt[string] `query:"after_id,omitzero" json:"-"`
+ BeforeID param.Opt[string] `query:"before_id,omitzero" json:"-"`
+ IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"`
+ // Repeated query values; an entry may also contain comma-separated IDs.
+ IdentityIDs []string `query:"identity_ids,omitzero" json:"-"`
+ TemplateID param.Opt[string] `query:"template_id,omitzero" json:"-"`
+ // Repeated query values; an entry may also contain comma-separated IDs.
+ TemplateIDs []string `query:"template_ids,omitzero" json:"-"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(UsageListParams\) [URLQuery]()
+
+```go
+func (r UsageListParams) URLQuery() (url.Values, error)
+```
+
+
+
+
+### func \(\*UsageListParams\) [UnmarshalJSON]()
+
+```go
+func (r *UsageListParams) UnmarshalJSON(data []byte) error
+```
+
+
+
+
+## type [UsageService]()
+
+
+
+```go
+type UsageService struct{ Options []option.RequestOption }
+```
+
+
+### func [NewUsageService]()
+
+```go
+func NewUsageService(opts ...option.RequestOption) UsageService
+```
+
+
+
+
+### func \(\*UsageService\) [ListIdentities]()
+
+```go
+func (r *UsageService) ListIdentities(ctx context.Context, params UsageListParams, opts ...option.RequestOption) (res *pagination.Page[IdentityUsage], err error)
+```
+
+ListIdentities aggregates an hourly Asia/Shanghai window. PAT or Admin SAT required.
+
+
+### func \(\*UsageService\) [ListIdentitiesAutoPaging]()
+
+```go
+func (r *UsageService) ListIdentitiesAutoPaging(ctx context.Context, params UsageListParams, opts ...option.RequestOption) *pagination.PageAutoPager[IdentityUsage]
+```
+
+
+
+
+### func \(\*UsageService\) [ListTemplates]()
+
+```go
+func (r *UsageService) ListTemplates(ctx context.Context, params UsageListParams, opts ...option.RequestOption) (res *pagination.Page[TemplateUsage], err error)
+```
+
+ListTemplates aggregates an hourly Asia/Shanghai window. PAT or Admin SAT required.
+
+
+### func \(\*UsageService\) [ListTemplatesAutoPaging]()
+
+```go
+func (r *UsageService) ListTemplatesAutoPaging(ctx context.Context, params UsageListParams, opts ...option.RequestOption) *pagination.PageAutoPager[TemplateUsage]
+```
+
+
+
## type [Vault]()
@@ -9392,6 +9735,8 @@ type VaultCredential struct {
DisplayName string `json:"display_name"`
// Credential metadata.
Metadata map[string]any `json:"metadata"`
+ // Archive time, null while active.
+ ArchivedAt time.Time `json:"archived_at" api:"nullable" format:"date-time"`
// Creation time in RFC 3339 format.
CreatedAt time.Time `json:"created_at" format:"date-time"`
// Last update time in RFC 3339 format.
@@ -9403,6 +9748,7 @@ type VaultCredential struct {
Auth respjson.Field
DisplayName respjson.Field
Metadata respjson.Field
+ ArchivedAt respjson.Field
CreatedAt respjson.Field
UpdatedAt respjson.Field
ExtraFields map[string]respjson.Field
@@ -9438,9 +9784,11 @@ func (r *VaultCredential) UnmarshalJSON(data []byte) error
type VaultCredentialAuth struct {
Type string `json:"type"`
MCPServerURL string `json:"mcp_server_url"`
+ SecretName string `json:"secret_name"`
JSON struct {
Type respjson.Field
MCPServerURL respjson.Field
+ SecretName respjson.Field
ExtraFields map[string]respjson.Field
// contains filtered or unexported fields
} `json:"-"`
@@ -9602,6 +9950,88 @@ func (r *VaultCredentialService) New(ctx context.Context, id string, params Vaul
Create Credential
+
+### func \(\*VaultCredentialService\) [Update]()
+
+```go
+func (r *VaultCredentialService) Update(ctx context.Context, id string, credID string, params VaultCredentialUpdateParams, opts ...option.RequestOption) (res *VaultCredential, err error)
+```
+
+Update merges auth or metadata. Write\-only secret rotation is never automatically retried.
+
+
+## type [VaultCredentialUpdateAuthUnionParam]()
+
+
+
+```go
+type VaultCredentialUpdateAuthUnionParam struct {
+ OfStaticBearer *StaticBearerUpdateParam `json:",omitzero,inline"`
+ OfMCPOAuth *MCPOAuthUpdateParam `json:",omitzero,inline"`
+ OfEnvironmentVariable *EnvironmentVariableUpdateParam `json:",omitzero,inline"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(VaultCredentialUpdateAuthUnionParam\) [MarshalJSON]()
+
+```go
+func (r VaultCredentialUpdateAuthUnionParam) MarshalJSON() ([]byte, error)
+```
+
+
+
+
+### func \(\*VaultCredentialUpdateAuthUnionParam\) [UnmarshalJSON]()
+
+```go
+func (r *VaultCredentialUpdateAuthUnionParam) UnmarshalJSON(data []byte) error
+```
+
+
+
+
+## type [VaultCredentialUpdateParams]()
+
+
+
+```go
+type VaultCredentialUpdateParams struct {
+ Auth VaultCredentialUpdateAuthUnionParam `json:"auth,omitzero"`
+ Metadata map[string]any `json:"metadata,omitzero"`
+ IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(VaultCredentialUpdateParams\) [MarshalJSON]()
+
+```go
+func (r VaultCredentialUpdateParams) MarshalJSON() ([]byte, error)
+```
+
+
+
+
+### func \(VaultCredentialUpdateParams\) [URLQuery]()
+
+```go
+func (r VaultCredentialUpdateParams) URLQuery() (url.Values, error)
+```
+
+
+
+
+### func \(\*VaultCredentialUpdateParams\) [UnmarshalJSON]()
+
+```go
+func (r *VaultCredentialUpdateParams) UnmarshalJSON(data []byte) error
+```
+
+
+
## type [VaultListParams]()
@@ -10080,6 +10510,16 @@ Qoder managed API definitions.
- [func \(r \*DeploymentRunService\) Get\(ctx context.Context, deploymentRunID string, query DeploymentRunGetParams, opts ...option.RequestOption\) \(res \*ManagedAgentsDeploymentRun, err error\)](<#DeploymentRunService.Get>)
- [func \(r \*DeploymentRunService\) List\(ctx context.Context, params DeploymentRunListParams, opts ...option.RequestOption\) \(res \*pagination.PageCursor\[ManagedAgentsDeploymentRun\], err error\)](<#DeploymentRunService.List>)
- [func \(r \*DeploymentRunService\) ListAutoPaging\(ctx context.Context, params DeploymentRunListParams, opts ...option.RequestOption\) \*pagination.PageCursorAutoPager\[ManagedAgentsDeploymentRun\]](<#DeploymentRunService.ListAutoPaging>)
+- [type DeploymentScopedRunGetParams](<#DeploymentScopedRunGetParams>)
+ - [func \(r \*DeploymentScopedRunGetParams\) UnmarshalJSON\(data \[\]byte\) error](<#DeploymentScopedRunGetParams.UnmarshalJSON>)
+- [type DeploymentScopedRunListParams](<#DeploymentScopedRunListParams>)
+ - [func \(r DeploymentScopedRunListParams\) URLQuery\(\) \(url.Values, error\)](<#DeploymentScopedRunListParams.URLQuery>)
+ - [func \(r \*DeploymentScopedRunListParams\) UnmarshalJSON\(data \[\]byte\) error](<#DeploymentScopedRunListParams.UnmarshalJSON>)
+- [type DeploymentScopedRunService](<#DeploymentScopedRunService>)
+ - [func NewDeploymentScopedRunService\(opts ...option.RequestOption\) DeploymentScopedRunService](<#NewDeploymentScopedRunService>)
+ - [func \(r \*DeploymentScopedRunService\) Get\(ctx context.Context, deploymentID string, runID string, params DeploymentScopedRunGetParams, opts ...option.RequestOption\) \(res \*ManagedAgentsDeploymentRun, err error\)](<#DeploymentScopedRunService.Get>)
+ - [func \(r \*DeploymentScopedRunService\) List\(ctx context.Context, deploymentID string, params DeploymentScopedRunListParams, opts ...option.RequestOption\) \(res \*pagination.PageCursor\[ManagedAgentsDeploymentRun\], err error\)](<#DeploymentScopedRunService.List>)
+ - [func \(r \*DeploymentScopedRunService\) ListAutoPaging\(ctx context.Context, deploymentID string, params DeploymentScopedRunListParams, opts ...option.RequestOption\) \*pagination.PageCursorAutoPager\[ManagedAgentsDeploymentRun\]](<#DeploymentScopedRunService.ListAutoPaging>)
- [type DeploymentService](<#DeploymentService>)
- [func NewDeploymentService\(opts ...option.RequestOption\) \(r DeploymentService\)](<#NewDeploymentService>)
- [func \(r \*DeploymentService\) Archive\(ctx context.Context, deploymentID string, body DeploymentArchiveParams, opts ...option.RequestOption\) \(res \*ManagedAgentsDeployment, err error\)](<#DeploymentService.Archive>)
@@ -12497,6 +12937,11 @@ Qoder managed API definitions.
- [func \(u \*ServerToolUseBlockParamCallerUnion\) UnmarshalJSON\(data \[\]byte\) error](<#ServerToolUseBlockParamCallerUnion.UnmarshalJSON>)
- [type ServerToolUseBlockParamName](<#ServerToolUseBlockParamName>)
- [type SessionArchiveParams](<#SessionArchiveParams>)
+- [type SessionCancelParams](<#SessionCancelParams>)
+ - [func \(r \*SessionCancelParams\) UnmarshalJSON\(data \[\]byte\) error](<#SessionCancelParams.UnmarshalJSON>)
+- [type SessionCancelResponse](<#SessionCancelResponse>)
+ - [func \(r SessionCancelResponse\) RawJSON\(\) string](<#SessionCancelResponse.RawJSON>)
+ - [func \(r \*SessionCancelResponse\) UnmarshalJSON\(data \[\]byte\) error](<#SessionCancelResponse.UnmarshalJSON>)
- [type SessionDeleteParams](<#SessionDeleteParams>)
- [type SessionEventListParams](<#SessionEventListParams>)
- [func \(r SessionEventListParams\) URLQuery\(\) \(v url.Values, err error\)](<#SessionEventListParams.URLQuery>)
@@ -12586,6 +13031,7 @@ Qoder managed API definitions.
- [type SessionService](<#SessionService>)
- [func NewSessionService\(opts ...option.RequestOption\) \(r SessionService\)](<#NewSessionService>)
- [func \(r \*SessionService\) Archive\(ctx context.Context, sessionID string, body SessionArchiveParams, opts ...option.RequestOption\) \(res \*ManagedAgentsSession, err error\)](<#SessionService.Archive>)
+ - [func \(r \*SessionService\) Cancel\(ctx context.Context, sessionID string, params SessionCancelParams, opts ...option.RequestOption\) \(res \*SessionCancelResponse, err error\)](<#SessionService.Cancel>)
- [func \(r \*SessionService\) Delete\(ctx context.Context, sessionID string, body SessionDeleteParams, opts ...option.RequestOption\) \(res \*ManagedAgentsDeletedSession, err error\)](<#SessionService.Delete>)
- [func \(r \*SessionService\) Get\(ctx context.Context, sessionID string, query SessionGetParams, opts ...option.RequestOption\) \(res \*ManagedAgentsSession, err error\)](<#SessionService.Get>)
- [func \(r \*SessionService\) List\(ctx context.Context, params SessionListParams, opts ...option.RequestOption\) \(res \*pagination.BidirectionalPageCursor\[ManagedAgentsSession\], err error\)](<#SessionService.List>)
@@ -16279,6 +16725,110 @@ func (r *DeploymentRunService) ListAutoPaging(ctx context.Context, params Deploy
List Deployment Runs
+
+## type [DeploymentScopedRunGetParams]()
+
+
+
+```go
+type DeploymentScopedRunGetParams struct {
+ WorkspaceID param.Opt[string] `header:"qoder-workspace-id,omitzero" json:"-"`
+ Betas []QoderBeta `header:"x-qoder-beta,omitzero" json:"-"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(\*DeploymentScopedRunGetParams\) [UnmarshalJSON]()
+
+```go
+func (r *DeploymentScopedRunGetParams) UnmarshalJSON(data []byte) error
+```
+
+
+
+
+## type [DeploymentScopedRunListParams]()
+
+
+
+```go
+type DeploymentScopedRunListParams struct {
+ Limit param.Opt[int64] `query:"limit,omitzero" json:"-"`
+ Page param.Opt[string] `query:"page,omitzero" json:"-"`
+ AfterID param.Opt[string] `query:"after_id,omitzero" json:"-"`
+ BeforeID param.Opt[string] `query:"before_id,omitzero" json:"-"`
+ TriggeredAfter param.Opt[string] `query:"triggered_after,omitzero" json:"-"`
+ TriggeredBefore param.Opt[string] `query:"triggered_before,omitzero" json:"-"`
+ WorkspaceID param.Opt[string] `header:"qoder-workspace-id,omitzero" json:"-"`
+ Betas []QoderBeta `header:"x-qoder-beta,omitzero" json:"-"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(DeploymentScopedRunListParams\) [URLQuery]()
+
+```go
+func (r DeploymentScopedRunListParams) URLQuery() (url.Values, error)
+```
+
+
+
+
+### func \(\*DeploymentScopedRunListParams\) [UnmarshalJSON]()
+
+```go
+func (r *DeploymentScopedRunListParams) UnmarshalJSON(data []byte) error
+```
+
+
+
+
+## type [DeploymentScopedRunService]()
+
+
+
+```go
+type DeploymentScopedRunService struct{ Options []option.RequestOption }
+```
+
+
+### func [NewDeploymentScopedRunService]()
+
+```go
+func NewDeploymentScopedRunService(opts ...option.RequestOption) DeploymentScopedRunService
+```
+
+
+
+
+### func \(\*DeploymentScopedRunService\) [Get]()
+
+```go
+func (r *DeploymentScopedRunService) Get(ctx context.Context, deploymentID string, runID string, params DeploymentScopedRunGetParams, opts ...option.RequestOption) (res *ManagedAgentsDeploymentRun, err error)
+```
+
+Get DeploymentScopedRun.
+
+
+### func \(\*DeploymentScopedRunService\) [List]()
+
+```go
+func (r *DeploymentScopedRunService) List(ctx context.Context, deploymentID string, params DeploymentScopedRunListParams, opts ...option.RequestOption) (res *pagination.PageCursor[ManagedAgentsDeploymentRun], err error)
+```
+
+List DeploymentScopedRun.
+
+
+### func \(\*DeploymentScopedRunService\) [ListAutoPaging]()
+
+```go
+func (r *DeploymentScopedRunService) ListAutoPaging(ctx context.Context, deploymentID string, params DeploymentScopedRunListParams, opts ...option.RequestOption) *pagination.PageCursorAutoPager[ManagedAgentsDeploymentRun]
+```
+
+
+
## type [DeploymentService]()
@@ -16289,6 +16839,7 @@ Note, unlike clients, this service does not read variables from the environment
```go
type DeploymentService struct {
Options []option.RequestOption
+ Runs DeploymentScopedRunService
}
```
@@ -49395,6 +49946,66 @@ type SessionArchiveParams struct {
}
```
+
+## type [SessionCancelParams]()
+
+
+
+```go
+type SessionCancelParams struct {
+ WorkspaceID param.Opt[string] `header:"qoder-workspace-id,omitzero" json:"-"`
+ Betas []QoderBeta `header:"x-qoder-beta,omitzero" json:"-"`
+ // contains filtered or unexported fields
+}
+```
+
+
+### func \(\*SessionCancelParams\) [UnmarshalJSON]()
+
+```go
+func (r *SessionCancelParams) UnmarshalJSON(data []byte) error
+```
+
+
+
+
+## type [SessionCancelResponse]()
+
+
+
+```go
+type SessionCancelResponse struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Status string `json:"status"`
+ JSON struct {
+ ID respjson.Field
+ Type respjson.Field
+ Status respjson.Field
+ ExtraFields map[string]respjson.Field
+ // contains filtered or unexported fields
+ } `json:"-"`
+}
+```
+
+
+### func \(SessionCancelResponse\) [RawJSON]()
+
+```go
+func (r SessionCancelResponse) RawJSON() string
+```
+
+
+
+
+### func \(\*SessionCancelResponse\) [UnmarshalJSON]()
+
+```go
+func (r *SessionCancelResponse) UnmarshalJSON(data []byte) error
+```
+
+
+
## type [SessionDeleteParams]()
@@ -50518,6 +51129,15 @@ func (r *SessionService) Archive(ctx context.Context, sessionID string, body Ses
Archive Session
+
+### func \(\*SessionService\) [Cancel]()
+
+```go
+func (r *SessionService) Cancel(ctx context.Context, sessionID string, params SessionCancelParams, opts ...option.RequestOption) (res *SessionCancelResponse, err error)
+```
+
+Cancel requests cancellation of the current turn \(202\); an idle session is a safe no\-op \(200\).
+
### func \(\*SessionService\) [Delete]()
diff --git a/forward/api_expansion_live_test.go b/forward/api_expansion_live_test.go
new file mode 100644
index 0000000..8f568ee
--- /dev/null
+++ b/forward/api_expansion_live_test.go
@@ -0,0 +1,93 @@
+//go:build live
+
+package forward_test
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
+)
+
+func TestForwardUsageHourlyLive(t *testing.T) {
+ s := newLiveSuite(t)
+ ctx := s.context(t)
+ end := time.Now().In(time.FixedZone("Asia/Shanghai", 8*60*60)).Truncate(time.Hour)
+ params := forward.UsageListParams{StartAt: end.Add(-24 * time.Hour).Format("2006-01-02T15:00:00"), EndAt: end.Format("2006-01-02T15:00:00"), Limit: forward.Int(2)}
+ identities, err := s.client.Usage.ListIdentities(ctx, params)
+ liveCheck(t, err)
+ checkUsageWindow(t, identities.RawJSON(), "identity_usage.list", params, len(identities.Data))
+ for _, row := range identities.Data {
+ if row.Type != "identity_usage" || row.IdentityID == "" || !row.JSON.ActiveSeconds.Valid() || row.ActiveSeconds < 0 || row.Credits < 0 || row.SessionCount < 0 {
+ t.Fatal("invalid identity usage item")
+ }
+ params.IdentityIDs = append(params.IdentityIDs, row.IdentityID)
+ }
+ if identities.HasMore {
+ next, err := identities.GetNextPage()
+ liveCheck(t, err)
+ if next == nil {
+ t.Fatal("identity usage has_more has no usable cursor")
+ }
+ checkUsageWindow(t, next.RawJSON(), "identity_usage.list", params, len(next.Data))
+ }
+ if len(params.IdentityIDs) > 0 {
+ filtered, err := s.client.Usage.ListIdentities(ctx, params)
+ liveCheck(t, err)
+ for _, row := range filtered.Data {
+ found := false
+ for _, id := range params.IdentityIDs {
+ found = found || row.IdentityID == id
+ }
+ if !found {
+ t.Fatal("identity usage ignored multi-ID filter")
+ }
+ }
+ }
+ params.IdentityIDs = nil
+ templates, err := s.client.Usage.ListTemplates(ctx, params)
+ liveCheck(t, err)
+ checkUsageWindow(t, templates.RawJSON(), "template_usage.list", params, len(templates.Data))
+ for _, row := range templates.Data {
+ if row.Type != "template_usage" || row.TemplateID == "" || !row.JSON.ActiveSeconds.Valid() || row.ActiveSeconds < 0 || row.Credits < 0 || row.SessionCount < 0 || row.ActiveIdentities < 0 {
+ t.Fatal("invalid template usage item")
+ }
+ params.TemplateIDs = append(params.TemplateIDs, row.TemplateID)
+ }
+ if templates.HasMore {
+ next, err := templates.GetNextPage()
+ liveCheck(t, err)
+ if next == nil {
+ t.Fatal("template usage has_more has no usable cursor")
+ }
+ checkUsageWindow(t, next.RawJSON(), "template_usage.list", params, len(next.Data))
+ }
+ if len(params.TemplateIDs) > 0 {
+ filtered, err := s.client.Usage.ListTemplates(ctx, params)
+ liveCheck(t, err)
+ for _, row := range filtered.Data {
+ found := false
+ for _, id := range params.TemplateIDs {
+ found = found || row.TemplateID == id
+ }
+ if !found {
+ t.Fatal("template usage ignored multi-ID filter")
+ }
+ }
+ }
+ t.Logf("usage identity_rows=%d template_rows=%d; empty pages cover the collection only", len(identities.Data), len(templates.Data))
+}
+
+func checkUsageWindow(t *testing.T, raw, kind string, params forward.UsageListParams, count int) {
+ t.Helper()
+ var window struct {
+ Type string `json:"type"`
+ StartAt string `json:"start_at"`
+ EndAt string `json:"end_at"`
+ }
+ liveCheck(t, json.Unmarshal([]byte(raw), &window))
+ if window.Type != kind || window.StartAt != params.StartAt || window.EndAt != params.EndAt || count > 2 {
+ t.Fatal("usage response changed the type, window, or requested limit")
+ }
+}
diff --git a/forward/api_expansion_test.go b/forward/api_expansion_test.go
new file mode 100644
index 0000000..7e7fcd2
--- /dev/null
+++ b/forward/api_expansion_test.go
@@ -0,0 +1,148 @@
+package forward_test
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
+)
+
+func TestForwardExpansionAPIContracts(t *testing.T) {
+ cases := []struct {
+ operation string
+ call func(forward.Client) (any, error)
+ }{
+ {"updateVaultCredential", func(c forward.Client) (any, error) {
+ return c.Vaults.Credentials.Update(context.Background(), pathSegment, pathSegment, contractParams[forward.VaultCredentialUpdateParams](t, "updateVaultCredential"))
+ }},
+ {"listIdentityUsage", func(c forward.Client) (any, error) {
+ return c.Usage.ListIdentities(context.Background(), contractParams[forward.UsageListParams](t, "listIdentityUsage"))
+ }},
+ {"listTemplateUsage", func(c forward.Client) (any, error) {
+ return c.Usage.ListTemplates(context.Background(), contractParams[forward.UsageListParams](t, "listTemplateUsage"))
+ }},
+ }
+ for _, c := range cases {
+ t.Run(c.operation, func(t *testing.T) {
+ for _, status := range []int{200, 400} {
+ client := contractClient(t, c.operation, status)
+ res, err := c.call(client)
+ checkError(t, status, err)
+ if status == 200 {
+ checkDecoded(t, res)
+ }
+ }
+ })
+ }
+}
+
+func TestForwardUsageHourlyPagination(t *testing.T) {
+ for _, backward := range []bool{false, true} {
+ t.Run(map[bool]string{false: "forward", true: "backward"}[backward], func(t *testing.T) {
+ calls := 0
+ client := testClient(func(req *http.Request) (*http.Response, error) {
+ calls++
+ q := req.URL.Query()
+ if q.Get("start_at") != "2026-09-14T09:00:00" || q.Get("end_at") != "2026-09-14T12:00:00" || !reflect.DeepEqual(q["identity_ids"], []string{"idn_one", "idn_two"}) || !reflect.DeepEqual(q["template_ids"], []string{"tmpl_one", "tmpl_two"}) {
+ t.Fatalf("query=%v", q)
+ }
+ if q.Has("start_time") || q.Has("end_time") || q.Has("identity_ids[]") {
+ t.Fatal("unexpected legacy parameters")
+ }
+ if calls == 2 {
+ key, next := "after_id", "last"
+ if backward {
+ key, next = "before_id", "first"
+ }
+ if q.Get(key) != next {
+ t.Fatalf("cursor=%v", q)
+ }
+ }
+ return reply(req, 200, `{"type":"identity_usage.list","start_at":"2026-09-14T09:00:00","end_at":"2026-09-14T12:00:00","first_id":"first","last_id":"last","has_more":true,"data":[{"type":"identity_usage","identity_id":"idn_one","session_count":3,"active_seconds":720.222,"credits":3.1}]}`), nil
+ })
+ params := forward.UsageListParams{StartAt: "2026-09-14T09:00:00", EndAt: "2026-09-14T12:00:00", IdentityIDs: []string{"idn_one", "idn_two"}, TemplateIDs: []string{"tmpl_one", "tmpl_two"}}
+ if backward {
+ params.BeforeID = forward.String("initial")
+ }
+ page, err := client.Usage.ListIdentities(context.Background(), params)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if page.Data[0].ActiveSeconds != 720.222 {
+ t.Fatal("fractional seconds lost")
+ }
+ if page.JSON.ExtraFields["start_at"].Raw() != `"2026-09-14T09:00:00"` {
+ t.Fatal("window metadata lost")
+ }
+ if _, err = page.GetNextPage(); err != nil {
+ t.Fatal(err)
+ }
+ if calls != 2 {
+ t.Fatal(calls)
+ }
+ })
+ }
+}
+
+func TestForwardCredentialUpdateMergePatch(t *testing.T) {
+ for _, clearAll := range []bool{false, true} {
+ t.Run(map[bool]string{false: "remove_key", true: "clear_metadata"}[clearAll], func(t *testing.T) {
+ params := forward.VaultCredentialUpdateParams{IdentityID: forward.String("idn_one"), Auth: forward.VaultCredentialUpdateAuthUnionParam{OfMCPOAuth: &forward.MCPOAuthUpdateParam{Type: "mcp_oauth", ExpiresAt: param.Null[string](), Refresh: forward.MCPOAuthRefreshUpdateParam{Scope: param.Null[string](), RefreshToken: forward.String("test-refresh")}}}, Metadata: map[string]any{"remove": nil}}
+ wantMetadata := any(map[string]any{"remove": nil})
+ if clearAll {
+ params.Metadata = param.NullMap[map[string]any]()
+ wantMetadata = nil
+ }
+ client := testClient(func(req *http.Request) (*http.Response, error) {
+ if req.URL.Query().Get("identity_id") != "idn_one" {
+ t.Fatal(req.URL)
+ }
+ b, err := io.ReadAll(req.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got map[string]any
+ if err = json.Unmarshal(b, &got); err != nil {
+ t.Fatal(err)
+ }
+ want := map[string]any{"auth": map[string]any{"type": "mcp_oauth", "expires_at": nil, "refresh": map[string]any{"scope": nil, "refresh_token": "test-refresh"}}, "metadata": wantMetadata}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("body=%s", b)
+ }
+ return reply(req, 200, string(contract(t, "updateVaultCredential").Response)), nil
+ })
+ if _, err := client.Vaults.Credentials.Update(context.Background(), "vault_one", "cred_one", params); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
+
+func TestForwardCredentialUpdateDoesNotRetry(t *testing.T) {
+ for _, status := range []int{0, 429, 503} {
+ t.Run(statusName(status), func(t *testing.T) {
+ calls := 0
+ client := testClient(func(req *http.Request) (*http.Response, error) {
+ calls++
+ if status == 0 {
+ return nil, errors.New("connection failed")
+ }
+ res := reply(req, status, `{"error":{"type":"api_error","message":"rotation uncertain"}}`)
+ res.Header.Set("Retry-After-Ms", "1")
+ res.Header.Set("X-Should-Retry", "true")
+ return res, nil
+ }, option.WithMaxRetries(3))
+ _, err := client.Vaults.Credentials.Update(context.Background(), "vault_one", "cred_one", forward.VaultCredentialUpdateParams{Auth: forward.VaultCredentialUpdateAuthUnionParam{OfStaticBearer: &forward.StaticBearerUpdateParam{Type: "static_bearer", Token: forward.String("test-secret")}}}, option.WithHeader("Idempotency-Key", "caller-key"), option.WithMaxRetries(3))
+ if err == nil || calls != 1 {
+ t.Fatalf("calls=%d err=%v", calls, err)
+ }
+ })
+ }
+}
diff --git a/forward/client.go b/forward/client.go
index e835d92..c2a05bd 100644
--- a/forward/client.go
+++ b/forward/client.go
@@ -26,6 +26,7 @@ type Client struct {
Skills SkillService
Vaults VaultService
MemoryStores MemoryStoreService
+ Usage UsageService
Models ModelService
}
@@ -57,6 +58,7 @@ func NewClient(opts ...option.RequestOption) Client {
Skills: NewSkillService(opts...),
Vaults: NewVaultService(opts...),
MemoryStores: NewMemoryStoreService(opts...),
+ Usage: NewUsageService(opts...),
Models: NewModelService(opts...),
}
}
diff --git a/forward/client_test.go b/forward/client_test.go
index 5a0c872..8b2d629 100644
--- a/forward/client_test.go
+++ b/forward/client_test.go
@@ -34,8 +34,8 @@ func TestForwardAPIInventory(t *testing.T) {
}
}
walk(root)
- if len(actual) != 110 {
- t.Fatalf("HTTP methods: %d want 110", len(actual))
+ if len(actual) != 113 {
+ t.Fatalf("HTTP methods: %d want 113", len(actual))
}
baseline := map[string]bool{}
for _, c := range operations(t) {
diff --git a/forward/test_helpers_test.go b/forward/test_helpers_test.go
index f5586f4..19d5925 100644
--- a/forward/test_helpers_test.go
+++ b/forward/test_helpers_test.go
@@ -85,8 +85,8 @@ func operations(t *testing.T) []operationCase {
Cases []operationCase `json:"cases"`
}
readJSON(t, "testdata/api-operation-cases.json", &fixture)
- if fixture.Count != 110 || len(fixture.Cases) != 110 {
- t.Fatal("Forward API scope must contain 110 operations")
+ if fixture.Count != 113 || len(fixture.Cases) != 113 {
+ t.Fatal("Forward API scope must contain 113 operations")
}
return fixture.Cases
}
@@ -120,6 +120,9 @@ func contract(t *testing.T, id string) contractCase {
// The original fixture's converter represented identity_ids
// as an empty object, though the published API accepts strings or arrays.
func parameterValue(p parameterCase) any {
+ if p.Name == "template_ids" {
+ return []string{"tmpl_one", "tmpl_two"}
+ }
if p.Name == "identity_ids" {
return []string{"idn_one", "idn_two"}
}
diff --git a/forward/testdata/api-contracts.json b/forward/testdata/api-contracts.json
index d7e7286..83c9b7d 100644
--- a/forward/testdata/api-contracts.json
+++ b/forward/testdata/api-contracts.json
@@ -3154,5 +3154,101 @@
"download": false,
"raw": false,
"empty": false
+ },
+ {
+ "service": "VaultCredentialService",
+ "name": "Update",
+ "response": {
+ "id": "vcred_xxx",
+ "type": "vault_credential",
+ "vault_id": "vault_xxx",
+ "auth": {
+ "type": "static_bearer",
+ "mcp_server_url": "https://mcp.example.com"
+ },
+ "display_name": "",
+ "metadata": {
+ "rotated_by": "console"
+ },
+ "archived_at": null,
+ "created_at": "2026-07-23T10:00:00Z",
+ "updated_at": "2026-08-27T12:00:00Z"
+ },
+ "operation_id": "updateVaultCredential",
+ "entry": "Vaults.Credentials",
+ "stream": false,
+ "download": false,
+ "raw": false,
+ "empty": false
+ },
+ {
+ "service": "UsageService",
+ "name": "ListIdentities",
+ "response": {
+ "type": "identity_usage.list",
+ "start_at": "2026-09-14T09:00:00",
+ "end_at": "2026-09-14T12:00:00",
+ "has_more": false,
+ "first_id": "idn_abc",
+ "last_id": "idn_efg",
+ "data": [
+ {
+ "type": "identity_usage",
+ "identity_id": "idn_abc",
+ "session_count": 3,
+ "active_seconds": 720.222,
+ "credits": 3.1
+ },
+ {
+ "type": "identity_usage",
+ "identity_id": "idn_efg",
+ "session_count": 2,
+ "active_seconds": 480.222,
+ "credits": 1.58
+ }
+ ]
+ },
+ "operation_id": "listIdentityUsage",
+ "entry": "Usage",
+ "stream": false,
+ "download": false,
+ "raw": false,
+ "empty": false
+ },
+ {
+ "service": "UsageService",
+ "name": "ListTemplates",
+ "response": {
+ "type": "template_usage.list",
+ "start_at": "2026-09-14T09:00:00",
+ "end_at": "2026-09-14T12:00:00",
+ "has_more": false,
+ "first_id": "tmpl_123",
+ "last_id": "tmpl_456",
+ "data": [
+ {
+ "type": "template_usage",
+ "template_id": "tmpl_123",
+ "active_identities": 2,
+ "session_count": 3,
+ "active_seconds": 900.321,
+ "credits": 3.45
+ },
+ {
+ "type": "template_usage",
+ "template_id": "tmpl_456",
+ "active_identities": 2,
+ "session_count": 2,
+ "active_seconds": 300.123,
+ "credits": 1.23
+ }
+ ]
+ },
+ "operation_id": "listTemplateUsage",
+ "entry": "Usage",
+ "stream": false,
+ "download": false,
+ "raw": false,
+ "empty": false
}
]
diff --git a/forward/testdata/api-operation-cases.json b/forward/testdata/api-operation-cases.json
index 69cdbd7..9358fd7 100644
--- a/forward/testdata/api-operation-cases.json
+++ b/forward/testdata/api-operation-cases.json
@@ -1,6 +1,6 @@
{
"schema_version": 1,
- "operation_count": 110,
+ "operation_count": 113,
"cases": [
{
"operation_id": "addSessionResource",
@@ -4768,6 +4768,162 @@
],
"request_body": null,
"source": "Forward router POST /environments/{id}/archive; verified against CN production on 2026-09-09"
+ },
+ {
+ "operation_id": "updateVaultCredential",
+ "resource": "Vaults.Credentials",
+ "clients": {
+ "go": "Vaults.Credentials",
+ "typescript": "vaults.credentials"
+ },
+ "methods": {
+ "go": "Update",
+ "typescript": "update"
+ },
+ "http_method": "POST",
+ "path": "/vaults/{id}/credentials/{cred_id}",
+ "parameters": [
+ {
+ "wire_name": "id",
+ "location": "path",
+ "required": true,
+ "value": "path_contract"
+ },
+ {
+ "wire_name": "cred_id",
+ "location": "path",
+ "required": true,
+ "value": "path_contract"
+ },
+ {
+ "wire_name": "identity_id",
+ "location": "query",
+ "required": false,
+ "value": "idn_contract"
+ }
+ ],
+ "request_body": {
+ "kind": "json",
+ "value": {
+ "auth": {
+ "type": "static_bearer",
+ "token": "rotated-test-token"
+ },
+ "metadata": {
+ "remove": null,
+ "rotated_by": "sdk"
+ }
+ }
+ },
+ "source": "cloud-agent/cloud-agents-api-doc origin/master 1b3f3d023b8631395c3f65577a507050d3893d12 (2026-10-09)"
+ },
+ {
+ "operation_id": "listIdentityUsage",
+ "resource": "Usage",
+ "clients": {
+ "go": "Usage",
+ "typescript": "usage"
+ },
+ "methods": {
+ "go": "ListIdentities",
+ "typescript": "listIdentities"
+ },
+ "http_method": "GET",
+ "path": "/usage/identities",
+ "parameters": [
+ {
+ "wire_name": "start_at",
+ "location": "query",
+ "required": true,
+ "value": "2026-09-14T09:00:00"
+ },
+ {
+ "wire_name": "end_at",
+ "location": "query",
+ "required": true,
+ "value": "2026-09-14T12:00:00"
+ },
+ {
+ "wire_name": "identity_ids",
+ "location": "query",
+ "required": false,
+ "value": [
+ "idn_one",
+ "idn_two"
+ ]
+ },
+ {
+ "wire_name": "template_ids",
+ "location": "query",
+ "required": false,
+ "value": [
+ "tmpl_one",
+ "tmpl_two"
+ ]
+ },
+ {
+ "wire_name": "limit",
+ "location": "query",
+ "required": false,
+ "value": 20
+ }
+ ],
+ "request_body": null,
+ "source": "cloud-agent/cloud-agents-api-doc origin/master 1b3f3d023b8631395c3f65577a507050d3893d12 (2026-10-09)"
+ },
+ {
+ "operation_id": "listTemplateUsage",
+ "resource": "Usage",
+ "clients": {
+ "go": "Usage",
+ "typescript": "usage"
+ },
+ "methods": {
+ "go": "ListTemplates",
+ "typescript": "listTemplates"
+ },
+ "http_method": "GET",
+ "path": "/usage/templates",
+ "parameters": [
+ {
+ "wire_name": "start_at",
+ "location": "query",
+ "required": true,
+ "value": "2026-09-14T09:00:00"
+ },
+ {
+ "wire_name": "end_at",
+ "location": "query",
+ "required": true,
+ "value": "2026-09-14T12:00:00"
+ },
+ {
+ "wire_name": "identity_ids",
+ "location": "query",
+ "required": false,
+ "value": [
+ "idn_one",
+ "idn_two"
+ ]
+ },
+ {
+ "wire_name": "template_ids",
+ "location": "query",
+ "required": false,
+ "value": [
+ "tmpl_one",
+ "tmpl_two"
+ ]
+ },
+ {
+ "wire_name": "limit",
+ "location": "query",
+ "required": false,
+ "value": 20
+ }
+ ],
+ "request_body": null,
+ "source": "cloud-agent/cloud-agents-api-doc origin/master 1b3f3d023b8631395c3f65577a507050d3893d12 (2026-10-09)"
}
]
}
diff --git a/forward/usage.go b/forward/usage.go
new file mode 100644
index 0000000..1b81854
--- /dev/null
+++ b/forward/usage.go
@@ -0,0 +1,125 @@
+package forward
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "slices"
+
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apijson"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apiquery"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/pagination"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/respjson"
+)
+
+type UsageService struct{ Options []option.RequestOption }
+
+func NewUsageService(opts ...option.RequestOption) UsageService {
+ return UsageService{Options: slices.Clone(opts)}
+}
+
+// ListIdentities aggregates an hourly Asia/Shanghai window. PAT or Admin SAT required.
+func (r *UsageService) ListIdentities(ctx context.Context, params UsageListParams, opts ...option.RequestOption) (res *pagination.Page[IdentityUsage], err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "usage/identities"
+ var raw *http.Response
+ opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...)
+ cfg, err := convention.NewRequestConfig(ctx, http.MethodGet, path, params, &res, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if err = cfg.Execute(); err != nil {
+ return nil, err
+ }
+ res.SetPageConfig(cfg, raw)
+ return res, nil
+}
+func (r *UsageService) ListIdentitiesAutoPaging(ctx context.Context, params UsageListParams, opts ...option.RequestOption) *pagination.PageAutoPager[IdentityUsage] {
+ return pagination.NewPageAutoPager(r.ListIdentities(ctx, params, opts...))
+}
+
+// ListTemplates aggregates an hourly Asia/Shanghai window. PAT or Admin SAT required.
+func (r *UsageService) ListTemplates(ctx context.Context, params UsageListParams, opts ...option.RequestOption) (res *pagination.Page[TemplateUsage], err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "usage/templates"
+ var raw *http.Response
+ opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...)
+ cfg, err := convention.NewRequestConfig(ctx, http.MethodGet, path, params, &res, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if err = cfg.Execute(); err != nil {
+ return nil, err
+ }
+ res.SetPageConfig(cfg, raw)
+ return res, nil
+}
+func (r *UsageService) ListTemplatesAutoPaging(ctx context.Context, params UsageListParams, opts ...option.RequestOption) *pagination.PageAutoPager[TemplateUsage] {
+ return pagination.NewPageAutoPager(r.ListTemplates(ctx, params, opts...))
+}
+
+// UsageListParams accepts only hourly parameters. StartAt is inclusive; EndAt is exclusive, with a maximum span of 744 hours. Both use YYYY-MM-DDTHH:00:00 in Asia/Shanghai for CN and Global.
+type UsageListParams struct {
+ StartAt string `query:"start_at" json:"-" api:"required"`
+ EndAt string `query:"end_at" json:"-" api:"required"`
+ Limit param.Opt[int64] `query:"limit,omitzero" json:"-"`
+ AfterID param.Opt[string] `query:"after_id,omitzero" json:"-"`
+ BeforeID param.Opt[string] `query:"before_id,omitzero" json:"-"`
+ IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"`
+ // Repeated query values; an entry may also contain comma-separated IDs.
+ IdentityIDs []string `query:"identity_ids,omitzero" json:"-"`
+ TemplateID param.Opt[string] `query:"template_id,omitzero" json:"-"`
+ // Repeated query values; an entry may also contain comma-separated IDs.
+ TemplateIDs []string `query:"template_ids,omitzero" json:"-"`
+ paramObj
+}
+
+func (r *UsageListParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) }
+func (r UsageListParams) URLQuery() (url.Values, error) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets})
+}
+
+type IdentityUsage struct {
+ Type string `json:"type"`
+ IdentityID string `json:"identity_id"`
+ SessionCount int64 `json:"session_count"`
+ ActiveSeconds float64 `json:"active_seconds"`
+ Credits float64 `json:"credits"`
+ JSON struct {
+ Type respjson.Field
+ IdentityID respjson.Field
+ SessionCount respjson.Field
+ ActiveSeconds respjson.Field
+ Credits respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+func (r IdentityUsage) RawJSON() string { return r.JSON.raw }
+func (r *IdentityUsage) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) }
+
+type TemplateUsage struct {
+ Type string `json:"type"`
+ TemplateID string `json:"template_id"`
+ ActiveIdentities int64 `json:"active_identities"`
+ SessionCount int64 `json:"session_count"`
+ ActiveSeconds float64 `json:"active_seconds"`
+ Credits float64 `json:"credits"`
+ JSON struct {
+ Type respjson.Field
+ TemplateID respjson.Field
+ ActiveIdentities respjson.Field
+ SessionCount respjson.Field
+ ActiveSeconds respjson.Field
+ Credits respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+func (r TemplateUsage) RawJSON() string { return r.JSON.raw }
+func (r *TemplateUsage) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) }
diff --git a/forward/vault_live_test.go b/forward/vault_live_test.go
index ac01dcf..46fbe8d 100644
--- a/forward/vault_live_test.go
+++ b/forward/vault_live_test.go
@@ -4,6 +4,7 @@ package forward_test
import (
"context"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param"
"strings"
"testing"
@@ -22,7 +23,8 @@ func TestVaultAndCredentialLifecycleLive(t *testing.T) {
t.Fatal("vault did not round trip")
}
secret := "sdk-live-placeholder-secret"
- credential, err := s.client.Vaults.Credentials.New(ctx, vault.ID, forward.VaultCredentialNewParams{Auth: map[string]any{"type": "static_bearer", "mcp_server_url": "https://example.com/" + liveName("mcp"), "token": secret}})
+ mcpURL := "https://example.com/" + liveName("mcp")
+ credential, err := s.client.Vaults.Credentials.New(ctx, vault.ID, forward.VaultCredentialNewParams{Auth: map[string]any{"type": "static_bearer", "mcp_server_url": mcpURL, "token": secret}})
liveCheck(t, err)
s.cleanup(t, "credential", func(ctx context.Context) error {
return s.client.Vaults.Credentials.Delete(ctx, vault.ID, credential.ID)
@@ -34,4 +36,27 @@ func TestVaultAndCredentialLifecycleLive(t *testing.T) {
}
_, err = s.client.Vaults.Credentials.List(ctx, vault.ID, forward.VaultCredentialListParams{})
liveCheck(t, err)
+ _, err = s.client.Vaults.Credentials.Update(ctx, vault.ID, credential.ID, forward.VaultCredentialUpdateParams{Metadata: map[string]any{"keep": "original", "remove": "old"}})
+ liveCheck(t, err)
+ rotated := liveName("rotated-secret")
+ updated, err := s.client.Vaults.Credentials.Update(ctx, vault.ID, credential.ID, forward.VaultCredentialUpdateParams{
+ Auth: forward.VaultCredentialUpdateAuthUnionParam{OfStaticBearer: &forward.StaticBearerUpdateParam{Type: "static_bearer", Token: forward.String(rotated)}},
+ Metadata: map[string]any{"remove": nil, "added": "new"},
+ })
+ liveCheck(t, err)
+ saved, err := s.client.Vaults.Credentials.Get(ctx, vault.ID, credential.ID)
+ liveCheck(t, err)
+ for _, value := range []*forward.VaultCredential{updated, saved} {
+ _, removed := value.Metadata["remove"]
+ if value.ID != credential.ID || value.Auth.Type != "static_bearer" || value.Auth.MCPServerURL != mcpURL || value.Metadata["keep"] != "original" || value.Metadata["added"] != "new" || removed || strings.Contains(value.RawJSON(), secret) || strings.Contains(value.RawJSON(), rotated) {
+ t.Fatal("credential patch failed preservation or secret-redaction check")
+ }
+ }
+ cleared, err := s.client.Vaults.Credentials.Update(ctx, vault.ID, credential.ID, forward.VaultCredentialUpdateParams{Metadata: param.NullMap[map[string]any]()})
+ liveCheck(t, err)
+ saved, err = s.client.Vaults.Credentials.Get(ctx, vault.ID, credential.ID)
+ liveCheck(t, err)
+ if len(cleared.Metadata) != 0 || len(saved.Metadata) != 0 || strings.Contains(cleared.RawJSON()+saved.RawJSON(), rotated) || strings.Contains(cleared.RawJSON()+saved.RawJSON(), secret) {
+ t.Fatal("null metadata did not clear metadata or disclosed the secret")
+ }
}
diff --git a/forward/vaultcredential.go b/forward/vaultcredential.go
index 1f8bad7..c9c453f 100644
--- a/forward/vaultcredential.go
+++ b/forward/vaultcredential.go
@@ -150,6 +150,8 @@ type VaultCredential struct {
DisplayName string `json:"display_name"`
// Credential metadata.
Metadata map[string]any `json:"metadata"`
+ // Archive time, null while active.
+ ArchivedAt time.Time `json:"archived_at" api:"nullable" format:"date-time"`
// Creation time in RFC 3339 format.
CreatedAt time.Time `json:"created_at" format:"date-time"`
// Last update time in RFC 3339 format.
@@ -161,6 +163,7 @@ type VaultCredential struct {
Auth respjson.Field
DisplayName respjson.Field
Metadata respjson.Field
+ ArchivedAt respjson.Field
CreatedAt respjson.Field
UpdatedAt respjson.Field
ExtraFields map[string]respjson.Field
@@ -174,9 +177,11 @@ func (r *VaultCredential) UnmarshalJSON(data []byte) error { return apijson.Unma
type VaultCredentialAuth struct {
Type string `json:"type"`
MCPServerURL string `json:"mcp_server_url"`
+ SecretName string `json:"secret_name"`
JSON struct {
Type respjson.Field
MCPServerURL respjson.Field
+ SecretName respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
@@ -184,3 +189,107 @@ type VaultCredentialAuth struct {
func (r VaultCredentialAuth) RawJSON() string { return r.JSON.raw }
func (r *VaultCredentialAuth) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) }
+
+// Update merges auth or metadata. Write-only secret rotation is never automatically retried.
+func (r *VaultCredentialService) Update(ctx context.Context, id string, credID string, params VaultCredentialUpdateParams, opts ...option.RequestOption) (res *VaultCredential, err error) {
+ if id == "" {
+ return nil, fmt.Errorf("missing required id parameter")
+ }
+ if credID == "" {
+ return nil, fmt.Errorf("missing required credID parameter")
+ }
+ opts = slices.Concat(r.Options, opts)
+ opts = append(opts, option.WithMaxRetries(0))
+ path := fmt.Sprintf("vaults/%s/credentials/%s", url.PathEscape(id), url.PathEscape(credID))
+ err = convention.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...)
+ return res, err
+}
+
+type VaultCredentialUpdateParams struct {
+ Auth VaultCredentialUpdateAuthUnionParam `json:"auth,omitzero"`
+ Metadata map[string]any `json:"metadata,omitzero"`
+ IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"`
+ paramObj
+}
+
+func (r VaultCredentialUpdateParams) MarshalJSON() ([]byte, error) {
+ type shadow VaultCredentialUpdateParams
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *VaultCredentialUpdateParams) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+func (r VaultCredentialUpdateParams) URLQuery() (url.Values, error) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets})
+}
+
+type VaultCredentialUpdateAuthUnionParam struct {
+ OfStaticBearer *StaticBearerUpdateParam `json:",omitzero,inline"`
+ OfMCPOAuth *MCPOAuthUpdateParam `json:",omitzero,inline"`
+ OfEnvironmentVariable *EnvironmentVariableUpdateParam `json:",omitzero,inline"`
+ paramUnion
+}
+
+func (r VaultCredentialUpdateAuthUnionParam) MarshalJSON() ([]byte, error) {
+ return param.MarshalUnion(r, r.OfStaticBearer, r.OfMCPOAuth, r.OfEnvironmentVariable)
+}
+func (r *VaultCredentialUpdateAuthUnionParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type StaticBearerUpdateParam struct {
+ Type string `json:"type" api:"required"`
+ Token param.Opt[string] `json:"token,omitzero"`
+ paramObj
+}
+
+func (r StaticBearerUpdateParam) MarshalJSON() ([]byte, error) {
+ type shadow StaticBearerUpdateParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *StaticBearerUpdateParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type EnvironmentVariableUpdateParam struct {
+ Type string `json:"type" api:"required"`
+ SecretValue param.Opt[string] `json:"secret_value,omitzero"`
+ paramObj
+}
+
+func (r EnvironmentVariableUpdateParam) MarshalJSON() ([]byte, error) {
+ type shadow EnvironmentVariableUpdateParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *EnvironmentVariableUpdateParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type MCPOAuthUpdateParam struct {
+ Type string `json:"type" api:"required"`
+ AccessToken param.Opt[string] `json:"access_token,omitzero"`
+ ExpiresAt param.Opt[string] `json:"expires_at,omitzero"`
+ Refresh MCPOAuthRefreshUpdateParam `json:"refresh,omitzero"`
+ paramObj
+}
+
+func (r MCPOAuthUpdateParam) MarshalJSON() ([]byte, error) {
+ type shadow MCPOAuthUpdateParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *MCPOAuthUpdateParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) }
+
+type MCPOAuthRefreshUpdateParam struct {
+ RefreshToken param.Opt[string] `json:"refresh_token,omitzero"`
+ Scope param.Opt[string] `json:"scope,omitzero"`
+ TokenEndpointAuth map[string]any `json:"token_endpoint_auth,omitzero"`
+ paramObj
+}
+
+func (r MCPOAuthRefreshUpdateParam) MarshalJSON() ([]byte, error) {
+ type shadow MCPOAuthRefreshUpdateParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *MCPOAuthRefreshUpdateParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
diff --git a/managed/api_expansion_test.go b/managed/api_expansion_test.go
new file mode 100644
index 0000000..a11c866
--- /dev/null
+++ b/managed/api_expansion_test.go
@@ -0,0 +1,77 @@
+package managed_test
+
+import (
+ "context"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/managed"
+ "net/http"
+ "reflect"
+ "testing"
+)
+
+func TestManagedExpansionAPIContracts(t *testing.T) {
+ t.Run("cancel", func(t *testing.T) {
+ c := contractClient(t, "SessionService", "Cancel")
+ res, err := c.Sessions.Cancel(context.Background(), "segment /?%#", managed.SessionCancelParams{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkFields(t, reflect.ValueOf(res), "Session.Cancel")
+ })
+ t.Run("list_runs", func(t *testing.T) {
+ c := contractClient(t, "DeploymentScopedRunService", "List")
+ res, err := c.Deployments.Runs.List(context.Background(), "segment /?%#", managed.DeploymentScopedRunListParams{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkFields(t, reflect.ValueOf(res), "Deployment.Runs.List")
+ })
+ t.Run("get_run", func(t *testing.T) {
+ c := contractClient(t, "DeploymentScopedRunService", "Get")
+ res, err := c.Deployments.Runs.Get(context.Background(), "segment /?%#", "segment /?%#", managed.DeploymentScopedRunGetParams{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkFields(t, reflect.ValueOf(res), "Deployment.Runs.Get")
+ })
+}
+
+func TestManagedSessionCancelAcknowledgement(t *testing.T) {
+ for _, status := range []int{200, 202} {
+ t.Run(http.StatusText(status), func(t *testing.T) {
+ c := testClient(func(req *http.Request) (*http.Response, error) {
+ if req.Method != "POST" || req.URL.Path != "/api/v1/cloud/sessions/sess_one/cancel" {
+ t.Fatal(req.URL)
+ }
+ return reply(req, status, `{"id":"sess_one","type":"session","status":"canceling"}`), nil
+ })
+ ack, err := c.Sessions.Cancel(context.Background(), "sess_one", managed.SessionCancelParams{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if ack.ID != "sess_one" || ack.Type != "session" || ack.Status != "canceling" {
+ t.Fatalf("ack=%+v", ack)
+ }
+ })
+ }
+}
+
+func TestManagedDeploymentScopedRunPagination(t *testing.T) {
+ calls := 0
+ c := testClient(func(req *http.Request) (*http.Response, error) {
+ calls++
+ if req.URL.Path != "/api/v1/cloud/deployments/dep_one/runs" || req.URL.Query().Get("triggered_after") != "2026-06-01T00:00:00Z" || req.Header.Get("Qoder-Workspace-Id") != "workspace_one" {
+ t.Fatalf("request=%v %v", req.URL, req.Header)
+ }
+ if calls == 2 && req.URL.Query().Get("page") != "opaque +/=?" {
+ t.Fatal(req.URL)
+ }
+ if calls == 1 {
+ return reply(req, 200, `{"data":[],"has_more":true,"next_page":"opaque +/=?"}`), nil
+ }
+ return reply(req, 200, `{"data":[{"id":"drun_one","type":"deployment_run","deployment_id":"dep_one"}],"has_more":false,"next_page":null}`), nil
+ })
+ pager := c.Deployments.Runs.ListAutoPaging(context.Background(), "dep_one", managed.DeploymentScopedRunListParams{TriggeredAfter: managed.String("2026-06-01T00:00:00Z"), WorkspaceID: managed.String("workspace_one")})
+ if !pager.Next() || pager.Current().ID != "drun_one" || pager.Next() || pager.Err() != nil || calls != 2 {
+ t.Fatalf("calls=%d err=%v", calls, pager.Err())
+ }
+}
diff --git a/managed/client_test.go b/managed/client_test.go
index 28034f2..0d1ae14 100644
--- a/managed/client_test.go
+++ b/managed/client_test.go
@@ -14,7 +14,7 @@ import (
func TestManagedAPIInventory(t *testing.T) {
fixtures := contracts(t)
- if len(fixtures) != 95 {
+ if len(fixtures) != 98 {
t.Fatalf("got %d API contracts", len(fixtures))
}
expected := map[string]bool{}
diff --git a/managed/deployment.go b/managed/deployment.go
index f2cb514..cc2c345 100644
--- a/managed/deployment.go
+++ b/managed/deployment.go
@@ -28,6 +28,7 @@ import (
// the [NewDeploymentService] method instead.
type DeploymentService struct {
Options []option.RequestOption
+ Runs DeploymentScopedRunService
}
// NewDeploymentService generates a new service that applies the given options
@@ -36,6 +37,7 @@ type DeploymentService struct {
func NewDeploymentService(opts ...option.RequestOption) (r DeploymentService) {
r = DeploymentService{}
r.Options = opts
+ r.Runs = NewDeploymentScopedRunService(opts...)
return
}
diff --git a/managed/deployment_execution_live_test.go b/managed/deployment_execution_live_test.go
index a4ec900..232967a 100644
--- a/managed/deployment_execution_live_test.go
+++ b/managed/deployment_execution_live_test.go
@@ -35,4 +35,19 @@ func TestManagedDeploymentE2ELive(t *testing.T) {
t.Fatal("run session changed")
}
s.waitTurn(t, run.SessionID, "", []string{marker}, false, false)
+ scoped := liveResult(s.client.Deployments.Runs.Get(ctx, deployment.ID, run.ID, managed.DeploymentScopedRunGetParams{})).require(t)
+ if scoped.ID != run.ID || scoped.DeploymentID != deployment.ID || scoped.SessionID != run.SessionID {
+ t.Fatal("scoped run changed its identity, deployment, or session")
+ }
+ runs := liveResult(s.client.Deployments.Runs.List(ctx, deployment.ID, managed.DeploymentScopedRunListParams{Limit: managed.Int(10)})).require(t)
+ found := false
+ for _, item := range runs.Data {
+ if item.DeploymentID != deployment.ID {
+ t.Fatal("scoped list returned another deployment")
+ }
+ found = found || item.ID == run.ID
+ }
+ if !found {
+ t.Fatal("scoped list did not include the created Run")
+ }
}
diff --git a/managed/deployment_live_test.go b/managed/deployment_live_test.go
index 79378fd..a4a0017 100644
--- a/managed/deployment_live_test.go
+++ b/managed/deployment_live_test.go
@@ -33,5 +33,11 @@ func TestDeploymentLifecycleLive(t *testing.T) {
liveResult(s.client.Deployments.Pause(ctx, created.ID, managed.DeploymentPauseParams{})).require(t)
liveResult(s.client.Deployments.Unpause(ctx, created.ID, managed.DeploymentUnpauseParams{})).require(t)
liveResult(s.client.DeploymentRuns.List(ctx, managed.DeploymentRunListParams{})).require(t)
+ runs := liveResult(s.client.Deployments.Runs.List(ctx, created.ID, managed.DeploymentScopedRunListParams{Limit: managed.Int(2)})).require(t)
+ for _, run := range runs.Data {
+ if run.DeploymentID != created.ID {
+ t.Fatal("scoped run belongs to another deployment")
+ }
+ }
}
diff --git a/managed/deploymentrun_live_test.go b/managed/deploymentrun_live_test.go
index 4155062..b051695 100644
--- a/managed/deploymentrun_live_test.go
+++ b/managed/deploymentrun_live_test.go
@@ -3,6 +3,7 @@
package managed_test
import (
+ "context"
"github.com/QoderAI/qoder-cloud-agents-sdk-go/managed"
"testing"
)
@@ -13,3 +14,37 @@ func TestDeploymentRunListLive(t *testing.T) {
defer cancel()
liveResult(s.client.DeploymentRuns.List(ctx, managed.DeploymentRunListParams{})).require(t)
}
+
+func TestManagedDeploymentScopedRunsLive(t *testing.T) {
+ s := newManagedScenarioSuite(t)
+ s.requireExecution(t)
+ ctx, cancel := s.context()
+ defer cancel()
+ environment, agent := s.createEnvironment(t), s.createAgent(t)
+ params := liveJSON[managed.DeploymentNewParams](t, map[string]any{"name": managedUnique("scoped-runs"), "agent": agent.ID, "environment_id": environment.ID, "initial_events": []any{map[string]any{"type": "user.message", "content": []any{map[string]any{"type": "text", "text": "Reply with SDK-LIVE."}}}}})
+ deployment := liveResult(s.client.Deployments.New(ctx, params)).require(t)
+ s.cleanup(t, "deployment", func(ctx context.Context) error {
+ _, err := s.client.Deployments.Archive(ctx, deployment.ID, managed.DeploymentArchiveParams{})
+ return err
+ })
+ run := liveResult(s.client.Deployments.Run(ctx, deployment.ID, managed.DeploymentRunParams{})).require(t)
+ if run.SessionID == "" {
+ t.Fatal("deployment run returned no session for cleanup")
+ }
+ s.cleanupSession(t, run.SessionID)
+ scoped := liveResult(s.client.Deployments.Runs.Get(ctx, deployment.ID, run.ID, managed.DeploymentScopedRunGetParams{})).require(t)
+ if scoped.ID != run.ID || scoped.DeploymentID != deployment.ID || scoped.SessionID != run.SessionID {
+ t.Fatal("scoped run changed identity, deployment, or session")
+ }
+ runs := liveResult(s.client.Deployments.Runs.List(ctx, deployment.ID, managed.DeploymentScopedRunListParams{Limit: managed.Int(10)})).require(t)
+ found := false
+ for _, item := range runs.Data {
+ if item.DeploymentID != deployment.ID {
+ t.Fatal("scoped list returned another deployment")
+ }
+ found = found || item.ID == run.ID
+ }
+ if !found {
+ t.Fatal("scoped list omitted the created Run")
+ }
+}
diff --git a/managed/deploymentscopedrun.go b/managed/deploymentscopedrun.go
new file mode 100644
index 0000000..a2c2f55
--- /dev/null
+++ b/managed/deploymentscopedrun.go
@@ -0,0 +1,100 @@
+package managed
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "slices"
+
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apijson"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apiquery"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/pagination"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param"
+)
+
+type DeploymentScopedRunService struct{ Options []option.RequestOption }
+
+func NewDeploymentScopedRunService(opts ...option.RequestOption) DeploymentScopedRunService {
+ return DeploymentScopedRunService{Options: slices.Clone(opts)}
+}
+
+// List DeploymentScopedRun.
+func (r *DeploymentScopedRunService) List(ctx context.Context, deploymentID string, params DeploymentScopedRunListParams, opts ...option.RequestOption) (res *pagination.PageCursor[ManagedAgentsDeploymentRun], err error) {
+ if deploymentID == "" {
+ return nil, fmt.Errorf("missing required deploymentID parameter")
+ }
+ if params.WorkspaceID.Valid() {
+ opts = append([]option.RequestOption{option.WithHeader("qoder-workspace-id", params.WorkspaceID.Value)}, opts...)
+ }
+ for _, beta := range params.Betas {
+ opts = append(opts, option.WithHeaderAdd("x-qoder-beta", string(beta)))
+ }
+ opts = slices.Concat(r.Options, opts)
+ path := fmt.Sprintf("deployments/%s/runs", url.PathEscape(deploymentID))
+ var raw *http.Response
+ opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...)
+ cfg, err := convention.NewRequestConfig(ctx, http.MethodGet, path, params, &res, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if err = cfg.Execute(); err != nil {
+ return nil, err
+ }
+ res.SetPageConfig(cfg, raw)
+ return res, nil
+}
+func (r *DeploymentScopedRunService) ListAutoPaging(ctx context.Context, deploymentID string, params DeploymentScopedRunListParams, opts ...option.RequestOption) *pagination.PageCursorAutoPager[ManagedAgentsDeploymentRun] {
+ return pagination.NewPageCursorAutoPager(r.List(ctx, deploymentID, params, opts...))
+}
+
+// Get DeploymentScopedRun.
+func (r *DeploymentScopedRunService) Get(ctx context.Context, deploymentID string, runID string, params DeploymentScopedRunGetParams, opts ...option.RequestOption) (res *ManagedAgentsDeploymentRun, err error) {
+ if deploymentID == "" {
+ return nil, fmt.Errorf("missing required deploymentID parameter")
+ }
+ if runID == "" {
+ return nil, fmt.Errorf("missing required runID parameter")
+ }
+ if params.WorkspaceID.Valid() {
+ opts = append([]option.RequestOption{option.WithHeader("qoder-workspace-id", params.WorkspaceID.Value)}, opts...)
+ }
+ for _, beta := range params.Betas {
+ opts = append(opts, option.WithHeaderAdd("x-qoder-beta", string(beta)))
+ }
+ opts = slices.Concat(r.Options, opts)
+ path := fmt.Sprintf("deployments/%s/runs/%s", url.PathEscape(deploymentID), url.PathEscape(runID))
+ err = convention.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return res, err
+}
+
+type DeploymentScopedRunListParams struct {
+ Limit param.Opt[int64] `query:"limit,omitzero" json:"-"`
+ Page param.Opt[string] `query:"page,omitzero" json:"-"`
+ AfterID param.Opt[string] `query:"after_id,omitzero" json:"-"`
+ BeforeID param.Opt[string] `query:"before_id,omitzero" json:"-"`
+ TriggeredAfter param.Opt[string] `query:"triggered_after,omitzero" json:"-"`
+ TriggeredBefore param.Opt[string] `query:"triggered_before,omitzero" json:"-"`
+ WorkspaceID param.Opt[string] `header:"qoder-workspace-id,omitzero" json:"-"`
+ Betas []QoderBeta `header:"x-qoder-beta,omitzero" json:"-"`
+ paramObj
+}
+
+func (r *DeploymentScopedRunListParams) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+func (r DeploymentScopedRunListParams) URLQuery() (url.Values, error) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets})
+}
+
+type DeploymentScopedRunGetParams struct {
+ WorkspaceID param.Opt[string] `header:"qoder-workspace-id,omitzero" json:"-"`
+ Betas []QoderBeta `header:"x-qoder-beta,omitzero" json:"-"`
+ paramObj
+}
+
+func (r *DeploymentScopedRunGetParams) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
diff --git a/managed/session.go b/managed/session.go
index 47dfb5f..29d7604 100644
--- a/managed/session.go
+++ b/managed/session.go
@@ -2890,3 +2890,46 @@ type SessionArchiveParams struct {
Betas []QoderBeta `header:"x-qoder-beta,omitzero" json:"-"`
paramObj
}
+
+// Cancel requests cancellation of the current turn (202); an idle session is a safe no-op (200).
+func (r *SessionService) Cancel(ctx context.Context, sessionID string, params SessionCancelParams, opts ...option.RequestOption) (res *SessionCancelResponse, err error) {
+ if sessionID == "" {
+ return nil, fmt.Errorf("missing required sessionID parameter")
+ }
+ if params.WorkspaceID.Valid() {
+ opts = append([]option.RequestOption{option.WithHeader("qoder-workspace-id", params.WorkspaceID.Value)}, opts...)
+ }
+ for _, beta := range params.Betas {
+ opts = append(opts, option.WithHeaderAdd("x-qoder-beta", string(beta)))
+ }
+ opts = slices.Concat(r.Options, opts)
+ path := fmt.Sprintf("sessions/%s/cancel", url.PathEscape(sessionID))
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return res, err
+}
+
+type SessionCancelParams struct {
+ WorkspaceID param.Opt[string] `header:"qoder-workspace-id,omitzero" json:"-"`
+ Betas []QoderBeta `header:"x-qoder-beta,omitzero" json:"-"`
+ paramObj
+}
+
+func (r *SessionCancelParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) }
+
+type SessionCancelResponse struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Status string `json:"status"`
+ JSON struct {
+ ID respjson.Field
+ Type respjson.Field
+ Status respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+func (r SessionCancelResponse) RawJSON() string { return r.JSON.raw }
+func (r *SessionCancelResponse) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
diff --git a/managed/session_cancel_live_test.go b/managed/session_cancel_live_test.go
new file mode 100644
index 0000000..19ef7f3
--- /dev/null
+++ b/managed/session_cancel_live_test.go
@@ -0,0 +1,50 @@
+//go:build live
+
+package managed_test
+
+import (
+ "context"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/managed"
+)
+
+func TestManagedSessionCancelActiveLive(t *testing.T) {
+ s := newManagedScenarioSuite(t)
+ s.requireExecution(t)
+ ctx, cancel := s.context()
+ defer cancel()
+ environment := s.createEnvironment(t)
+ agentParams := liveJSON[managed.AgentNewParams](t, map[string]any{"name": managedUnique("cancel-agent"), "model": map[string]any{"id": s.model(t)}, "tools": []any{map[string]any{"type": "agent_toolset_20260401"}}})
+ agent := liveResult(s.client.Agents.New(ctx, agentParams)).require(t)
+ s.cleanup(t, "agent", func(ctx context.Context) error {
+ _, err := s.client.Agents.Archive(ctx, agent.ID, managed.AgentArchiveParams{})
+ return err
+ })
+ params := liveJSON[managed.SessionNewParams](t, map[string]any{"agent": agent.ID, "environment_id": environment.ID})
+ session := liveResult(s.client.Sessions.New(ctx, params)).require(t)
+ s.cleanupSession(t, session.ID)
+ events := liveJSON[managed.SessionEventSendParams](t, map[string]any{"events": []any{map[string]any{"type": "user.message", "content": []any{map[string]any{"type": "text", "text": "Use a shell command to sleep 30 seconds, then reply with SDK-LIVE."}}}}})
+ liveResult(s.client.Sessions.Events.Send(ctx, session.ID, events)).require(t)
+ var raw *http.Response
+ ack := liveResult(s.client.Sessions.Cancel(ctx, session.ID, managed.SessionCancelParams{}, option.WithResponseInto(&raw))).require(t)
+ // The turn can finish between send and cancel; 200 is the documented idle no-op.
+ if (raw.StatusCode != 200 && raw.StatusCode != 202) || ack.ID != session.ID || ack.Type != "session" || ack.Status != "canceling" {
+ t.Fatal("cancellation did not return the lightweight acknowledgement")
+ }
+ t.Logf("session=%s cancellation_http_status=%d", session.ID, raw.StatusCode)
+ for {
+ current := liveResult(s.client.Sessions.Get(ctx, session.ID, managed.SessionGetParams{})).require(t)
+ if current.Status == "idle" || current.Status == "terminated" {
+ break
+ }
+ select {
+ case <-ctx.Done():
+ t.Fatal("session did not become idle after cancellation")
+ case <-time.After(time.Second):
+ }
+ }
+}
diff --git a/managed/session_live_test.go b/managed/session_live_test.go
index cd4e63b..3d9d15e 100644
--- a/managed/session_live_test.go
+++ b/managed/session_live_test.go
@@ -4,7 +4,9 @@ package managed_test
import (
"context"
+ "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option"
"github.com/QoderAI/qoder-cloud-agents-sdk-go/managed"
+ "net/http"
"testing"
)
@@ -33,5 +35,10 @@ func TestSessionLifecycleLive(t *testing.T) {
liveResult(s.client.Sessions.Events.List(ctx, session.ID, managed.SessionEventListParams{})).require(t)
liveResult(s.client.Sessions.Resources.List(ctx, session.ID, managed.SessionResourceListParams{})).require(t)
liveResult(s.client.Sessions.Threads.List(ctx, session.ID, managed.SessionThreadListParams{})).require(t)
+ var raw *http.Response
+ ack := liveResult(s.client.Sessions.Cancel(ctx, session.ID, managed.SessionCancelParams{}, option.WithResponseInto(&raw))).require(t)
+ if raw.StatusCode != 200 || ack.ID != session.ID || ack.Type != "session" || ack.Status != "canceling" {
+ t.Fatal("idle cancellation did not return the lightweight acknowledgement")
+ }
}
diff --git a/managed/test_helpers_test.go b/managed/test_helpers_test.go
index 417449d..5c32386 100644
--- a/managed/test_helpers_test.go
+++ b/managed/test_helpers_test.go
@@ -51,7 +51,7 @@ func services(v reflect.Value, into map[string]reflect.Value) {
}
// Contract fixtures come from official documentation, independently of the adapted services.
-// The inventory also detects extra HTTP methods outside the authorized 95 endpoints.
+// The inventory also detects extra HTTP methods outside the authorized 98 endpoints.
func checkFields(t *testing.T, v reflect.Value, path string) {
t.Helper()
diff --git a/managed/testdata/api-contracts.json b/managed/testdata/api-contracts.json
index 12cb015..6133edc 100644
--- a/managed/testdata/api-contracts.json
+++ b/managed/testdata/api-contracts.json
@@ -2724,5 +2724,77 @@
"environment_variables": {},
"vault_ids": []
}
+ },
+ {
+ "service": "SessionService",
+ "name": "Cancel",
+ "response": {
+ "id": "sess_019ef30a4f0275678965d496ab542ef0",
+ "type": "session",
+ "status": "canceling"
+ },
+ "method": "POST",
+ "path": "/sessions/{}/cancel",
+ "route": "/sessions/{session_id}/cancel",
+ "doc": "sessions__cancel.md",
+ "source": "cloud-agent/cloud-agents-api-doc origin/master 1b3f3d023b8631395c3f65577a507050d3893d12 (2026-10-09)"
+ },
+ {
+ "service": "DeploymentScopedRunService",
+ "name": "List",
+ "response": {
+ "data": [
+ {
+ "agent": {
+ "id": "agent_019ebb21ef8e7df6a559052c94875160",
+ "type": "agent",
+ "version": 1
+ },
+ "created_at": "2026-06-14T08:58:17Z",
+ "deployment_id": "dep_019ec55a2b687b3f94eee77dd77e4b2a",
+ "error": null,
+ "id": "drun_019ec55a68af73028afa5b87931cb2f3",
+ "session_id": "sess_019ec55a68b37e1e8d660691af161ab4",
+ "trigger_context": {
+ "type": "manual"
+ },
+ "type": "deployment_run"
+ }
+ ],
+ "first_id": "drun_019ec55a68af73028afa5b87931cb2f3",
+ "has_more": false,
+ "last_id": "drun_019ec55a68af73028afa5b87931cb2f3",
+ "next_page": null
+ },
+ "method": "GET",
+ "path": "/deployments/{}/runs",
+ "route": "/deployments/{deployment_id}/runs",
+ "doc": "deployments__list-runs.md",
+ "source": "cloud-agent/cloud-agents-api-doc origin/master 1b3f3d023b8631395c3f65577a507050d3893d12 (2026-10-09)"
+ },
+ {
+ "service": "DeploymentScopedRunService",
+ "name": "Get",
+ "response": {
+ "agent": {
+ "id": "agent_019ebb21ef8e7df6a559052c94875160",
+ "type": "agent",
+ "version": 1
+ },
+ "created_at": "2026-06-14T08:58:17Z",
+ "deployment_id": "dep_019ec55a2b687b3f94eee77dd77e4b2a",
+ "error": null,
+ "id": "drun_019ec55a68af73028afa5b87931cb2f3",
+ "session_id": "sess_019ec55a68b37e1e8d660691af161ab4",
+ "trigger_context": {
+ "type": "manual"
+ },
+ "type": "deployment_run"
+ },
+ "method": "GET",
+ "path": "/deployments/{}/runs/{}",
+ "route": "/deployments/{deployment_id}/runs/{run_id}",
+ "doc": "deployments__get-run.md",
+ "source": "cloud-agent/cloud-agents-api-doc origin/master 1b3f3d023b8631395c3f65577a507050d3893d12 (2026-10-09)"
}
]