diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d2ac0b..10f3091 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "0.2.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 57e5ba5..b2181d4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 64 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/calvinfo-o4h6u5/cerca-a3610b6045d1d701859276e09dff35829bdf50509b762116860d8005831b0297.yml -openapi_spec_hash: 2a240632b5ef4814881e36247ec3c8dc -config_hash: 5d687451efd1d2898e81fa3ddb25f8ed +configured_endpoints: 66 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/calvinfo-o4h6u5/cerca-b1933e28ba1d2a81cae6514fd41ec2ac5289660666174bfa4466d456e1740fb1.yml +openapi_spec_hash: d1e63b49a56f6c27dc3dac475c34d612 +config_hash: 411e4c3ec8f57219c56018ea49c09614 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9743757..196e94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.2.0 (2026-05-06) + +Full Changelog: [v0.1.0...v0.2.0](https://github.com/matrices/cerca-go/compare/v0.1.0...v0.2.0) + +### Features + +* **api:** api update ([d64f019](https://github.com/matrices/cerca-go/commit/d64f019fa5da77a390e42a75244c1fc96f762865)) + ## 0.1.0 (2026-05-01) Full Changelog: [v0.0.1...v0.1.0](https://github.com/matrices/cerca-go/compare/v0.0.1...v0.1.0) diff --git a/README.md b/README.md index c7bf64a..6ed1e74 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Or to pin the version: ```sh -go get -u 'github.com/matrices/cerca-go@v0.1.0' +go get -u 'github.com/matrices/cerca-go@v0.2.0' ``` diff --git a/api.md b/api.md index d429f6d..3a8c6c0 100644 --- a/api.md +++ b/api.md @@ -196,14 +196,18 @@ Params Types: Response Types: +- cercago.ActivityDetail +- cercago.ActivitySummary - cercago.CompiledContext - cercago.ContentBlock - cercago.Message +- cercago.MessagePage - cercago.Status - cercago.SteerResult - cercago.SubThreadSummary - cercago.Thread - cercago.ThreadSummary +- cercago.ThreadTurnSummary - cercago.TokenUsage - cercago.Turn @@ -212,9 +216,11 @@ Methods: - client.Threads.New(ctx context.Context, agentID string, body cercago.ThreadNewParams) (\*cercago.Thread, error) - client.Threads.Get(ctx context.Context, agentID string, threadID string, query cercago.ThreadGetParams) (\*cercago.Thread, error) - client.Threads.List(ctx context.Context, agentID string, query cercago.ThreadListParams) (\*pagination.ThreadsCursorPage[cercago.ThreadSummary], error) +- client.Threads.Activity(ctx context.Context, agentID string, threadID string, query cercago.ThreadActivityParams) (\*cercago.ActivityDetail, error) - client.Threads.Cancel(ctx context.Context, agentID string, threadID string) (\*cercago.Thread, error) - client.Threads.Close(ctx context.Context, agentID string, threadID string) (\*cercago.Thread, error) - client.Threads.Compact(ctx context.Context, agentID string, threadID string) (\*cercago.Thread, error) +- client.Threads.ListMessages(ctx context.Context, agentID string, threadID string, query cercago.ThreadListMessagesParams) (\*pagination.ThreadMessagesCursorPage[cercago.Message], error) - client.Threads.StartTurn(ctx context.Context, agentID string, threadID string, body cercago.ThreadStartTurnParams) (\*cercago.Turn, error) - client.Threads.Steer(ctx context.Context, agentID string, threadID string, body cercago.ThreadSteerParams) (\*cercago.SteerResult, error) diff --git a/internal/version.go b/internal/version.go index 02eac73..774c6c4 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.1.0" // x-release-please-version +const PackageVersion = "0.2.0" // x-release-please-version diff --git a/packages/pagination/pagination.go b/packages/pagination/pagination.go index e8bc7ef..dd0c27c 100644 --- a/packages/pagination/pagination.go +++ b/packages/pagination/pagination.go @@ -1219,3 +1219,113 @@ func (r *ThreadsCursorPageAutoPager[T]) Err() error { func (r *ThreadsCursorPageAutoPager[T]) Index() int { return r.run } + +type ThreadMessagesCursorPage[T any] struct { + Messages []T `json:"messages"` + Cursor string `json:"cursor" api:"nullable"` + HasMore bool `json:"hasMore"` + JSON threadMessagesCursorPageJSON `json:"-"` + cfg *requestconfig.RequestConfig + res *http.Response +} + +// threadMessagesCursorPageJSON contains the JSON metadata for the struct +// [ThreadMessagesCursorPage[T]] +type threadMessagesCursorPageJSON struct { + Messages apijson.Field + Cursor apijson.Field + HasMore apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ThreadMessagesCursorPage[T]) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r threadMessagesCursorPageJSON) RawJSON() string { + return r.raw +} + +// GetNextPage returns the next page as defined by this pagination style. When +// there is no next page, this function will return a 'nil' for the page value, but +// will not return an error +func (r *ThreadMessagesCursorPage[T]) GetNextPage() (res *ThreadMessagesCursorPage[T], err error) { + if len(r.Messages) == 0 { + return nil, nil + } + + if !r.JSON.HasMore.IsMissing() && r.HasMore == false { + return nil, nil + } + next := r.Cursor + if len(next) == 0 { + return nil, nil + } + cfg := r.cfg.Clone(r.cfg.Context) + err = cfg.Apply(option.WithQuery("cursor", next)) + if err != nil { + return nil, err + } + var raw *http.Response + cfg.ResponseInto = &raw + cfg.ResponseBodyInto = &res + err = cfg.Execute() + if err != nil { + return nil, err + } + res.SetPageConfig(cfg, raw) + return res, nil +} + +func (r *ThreadMessagesCursorPage[T]) SetPageConfig(cfg *requestconfig.RequestConfig, res *http.Response) { + if r == nil { + r = &ThreadMessagesCursorPage[T]{} + } + r.cfg = cfg + r.res = res +} + +type ThreadMessagesCursorPageAutoPager[T any] struct { + page *ThreadMessagesCursorPage[T] + cur T + idx int + run int + err error +} + +func NewThreadMessagesCursorPageAutoPager[T any](page *ThreadMessagesCursorPage[T], err error) *ThreadMessagesCursorPageAutoPager[T] { + return &ThreadMessagesCursorPageAutoPager[T]{ + page: page, + err: err, + } +} + +func (r *ThreadMessagesCursorPageAutoPager[T]) Next() bool { + if r.page == nil || len(r.page.Messages) == 0 { + return false + } + if r.idx >= len(r.page.Messages) { + r.idx = 0 + r.page, r.err = r.page.GetNextPage() + if r.err != nil || r.page == nil || len(r.page.Messages) == 0 { + return false + } + } + r.cur = r.page.Messages[r.idx] + r.run += 1 + r.idx += 1 + return true +} + +func (r *ThreadMessagesCursorPageAutoPager[T]) Current() T { + return r.cur +} + +func (r *ThreadMessagesCursorPageAutoPager[T]) Err() error { + return r.err +} + +func (r *ThreadMessagesCursorPageAutoPager[T]) Index() int { + return r.run +} diff --git a/thread.go b/thread.go index 892e5a7..6bc03f3 100644 --- a/thread.go +++ b/thread.go @@ -95,6 +95,23 @@ func (r *ThreadService) ListAutoPaging(ctx context.Context, agentID string, quer return pagination.NewThreadsCursorPageAutoPager(r.List(ctx, agentID, query, opts...)) } +// Fetch compact current and recent activity for a thread without returning +// transcript content or runtime debug state. +func (r *ThreadService) Activity(ctx context.Context, agentID string, threadID string, query ThreadActivityParams, opts ...option.RequestOption) (res *ActivityDetail, err error) { + opts = slices.Concat(r.Options, opts) + if agentID == "" { + err = errors.New("missing required agentId parameter") + return nil, err + } + if threadID == "" { + err = errors.New("missing required threadId parameter") + return nil, err + } + path := fmt.Sprintf("agents/%s/threads/%s/activity", agentID, threadID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return res, err +} + // Cancel a running or awaiting thread. The underlying runtime treats repeat // cancellation as an idempotent lifecycle operation when possible. func (r *ThreadService) Cancel(ctx context.Context, agentID string, threadID string, opts ...option.RequestOption) (res *Thread, err error) { @@ -146,6 +163,39 @@ func (r *ThreadService) Compact(ctx context.Context, agentID string, threadID st return res, err } +// List a bounded page of transcript messages for a thread, newest first. Use the +// returned `cursor` to page older messages. +func (r *ThreadService) ListMessages(ctx context.Context, agentID string, threadID string, query ThreadListMessagesParams, opts ...option.RequestOption) (res *pagination.ThreadMessagesCursorPage[Message], err error) { + var raw *http.Response + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...) + if agentID == "" { + err = errors.New("missing required agentId parameter") + return nil, err + } + if threadID == "" { + err = errors.New("missing required threadId parameter") + return nil, err + } + path := fmt.Sprintf("agents/%s/threads/%s/messages", agentID, threadID) + cfg, err := requestconfig.NewRequestConfig(ctx, http.MethodGet, path, query, &res, opts...) + if err != nil { + return nil, err + } + err = cfg.Execute() + if err != nil { + return nil, err + } + res.SetPageConfig(cfg, raw) + return res, nil +} + +// List a bounded page of transcript messages for a thread, newest first. Use the +// returned `cursor` to page older messages. +func (r *ThreadService) ListMessagesAutoPaging(ctx context.Context, agentID string, threadID string, query ThreadListMessagesParams, opts ...option.RequestOption) *pagination.ThreadMessagesCursorPageAutoPager[Message] { + return pagination.NewThreadMessagesCursorPageAutoPager(r.ListMessages(ctx, agentID, threadID, query, opts...)) +} + // Create turn func (r *ThreadService) StartTurn(ctx context.Context, agentID string, threadID string, body ThreadStartTurnParams, opts ...option.RequestOption) (res *Turn, err error) { opts = slices.Concat(r.Options, opts) @@ -179,6 +229,78 @@ func (r *ThreadService) Steer(ctx context.Context, agentID string, threadID stri return res, err } +type ActivityDetail struct { + Error string `json:"error" api:"required,nullable"` + RecentTurns []ThreadTurnSummary `json:"recentTurns" api:"required"` + JSON activityDetailJSON `json:"-"` + ActivitySummary +} + +// activityDetailJSON contains the JSON metadata for the struct [ActivityDetail] +type activityDetailJSON struct { + Error apijson.Field + RecentTurns apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ActivityDetail) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r activityDetailJSON) RawJSON() string { + return r.raw +} + +type ActivitySummary struct { + ID string `json:"id" api:"required"` + CompletedAt string `json:"completedAt" api:"required,nullable"` + CreatedAt string `json:"createdAt" api:"required"` + Goal string `json:"goal" api:"required"` + LatestActivity string `json:"latestActivity" api:"required,nullable"` + MessageCount float64 `json:"messageCount" api:"required"` + Model string `json:"model" api:"required"` + NextStep string `json:"nextStep" api:"required,nullable"` + ParentThreadID string `json:"parentThreadId" api:"required,nullable"` + Result string `json:"result" api:"required,nullable"` + ScheduleID string `json:"scheduleId" api:"required,nullable"` + // `idle` threads can accept a new turn or be closed. `running` threads have an + // active turn. `awaiting` threads are paused on external input such as approvals. + // `closed` threads are terminal. + Status Status `json:"status" api:"required"` + StepCount float64 `json:"stepCount" api:"required"` + UpdatedAt string `json:"updatedAt" api:"required"` + JSON activitySummaryJSON `json:"-"` +} + +// activitySummaryJSON contains the JSON metadata for the struct [ActivitySummary] +type activitySummaryJSON struct { + ID apijson.Field + CompletedAt apijson.Field + CreatedAt apijson.Field + Goal apijson.Field + LatestActivity apijson.Field + MessageCount apijson.Field + Model apijson.Field + NextStep apijson.Field + ParentThreadID apijson.Field + Result apijson.Field + ScheduleID apijson.Field + Status apijson.Field + StepCount apijson.Field + UpdatedAt apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ActivitySummary) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r activitySummaryJSON) RawJSON() string { + return r.raw +} + type CompiledContext struct { EnabledTools []shared.ToolName `json:"enabledTools" api:"required,nullable"` SystemPrompt string `json:"systemPrompt" api:"required,nullable"` @@ -588,6 +710,30 @@ func (r MessageRole) IsKnown() bool { return false } +type MessagePage struct { + Cursor string `json:"cursor" api:"required,nullable"` + HasMore bool `json:"hasMore" api:"required"` + Messages []Message `json:"messages" api:"required"` + JSON messagePageJSON `json:"-"` +} + +// messagePageJSON contains the JSON metadata for the struct [MessagePage] +type messagePageJSON struct { + Cursor apijson.Field + HasMore apijson.Field + Messages apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessagePage) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messagePageJSON) RawJSON() string { + return r.raw +} + // `idle` threads can accept a new turn or be closed. `running` threads have an // active turn. `awaiting` threads are paused on external input such as approvals. // `closed` threads are terminal. @@ -723,9 +869,11 @@ type Thread struct { CreatedAt string `json:"createdAt" api:"required"` Depth float64 `json:"depth" api:"required"` Error string `json:"error" api:"required,nullable"` + HasMoreMessages bool `json:"hasMoreMessages" api:"required"` Instructions string `json:"instructions" api:"required,nullable"` LastTurnStatus ThreadLastTurnStatus `json:"lastTurnStatus" api:"required,nullable"` Message string `json:"message" api:"required"` + MessageCursor float64 `json:"messageCursor" api:"required,nullable"` Messages []Message `json:"messages" api:"required"` Model string `json:"model" api:"required"` ParentThreadID string `json:"parentThreadId" api:"required,nullable"` @@ -753,9 +901,11 @@ type threadJSON struct { CreatedAt apijson.Field Depth apijson.Field Error apijson.Field + HasMoreMessages apijson.Field Instructions apijson.Field LastTurnStatus apijson.Field Message apijson.Field + MessageCursor apijson.Field Messages apijson.Field Model apijson.Field ParentThreadID apijson.Field @@ -879,6 +1029,52 @@ func (r threadSummaryJSON) RawJSON() string { return r.raw } +type ThreadTurnSummary struct { + Activity string `json:"activity" api:"required,nullable"` + CompletedAt string `json:"completedAt" api:"required"` + MessageCount float64 `json:"messageCount" api:"required"` + NextStep string `json:"nextStep" api:"required,nullable"` + Status ThreadTurnSummaryStatus `json:"status" api:"required"` + TurnSeq float64 `json:"turnSeq" api:"required"` + JSON threadTurnSummaryJSON `json:"-"` +} + +// threadTurnSummaryJSON contains the JSON metadata for the struct +// [ThreadTurnSummary] +type threadTurnSummaryJSON struct { + Activity apijson.Field + CompletedAt apijson.Field + MessageCount apijson.Field + NextStep apijson.Field + Status apijson.Field + TurnSeq apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ThreadTurnSummary) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r threadTurnSummaryJSON) RawJSON() string { + return r.raw +} + +type ThreadTurnSummaryStatus string + +const ( + ThreadTurnSummaryStatusCompleted ThreadTurnSummaryStatus = "completed" + ThreadTurnSummaryStatusFailed ThreadTurnSummaryStatus = "failed" +) + +func (r ThreadTurnSummaryStatus) IsKnown() bool { + switch r { + case ThreadTurnSummaryStatusCompleted, ThreadTurnSummaryStatusFailed: + return true + } + return false +} + type TokenUsage struct { InputTokens float64 `json:"inputTokens" api:"required"` OutputTokens float64 `json:"outputTokens" api:"required"` @@ -978,7 +1174,9 @@ func (r ThreadNewParams) MarshalJSON() (data []byte, err error) { type ThreadGetParams struct { // When true, includes debug-only compiled context fields. Debug param.Field[ThreadGetParamsDebug] `query:"debug"` - // When true, includes message content in the thread detail. + // Deprecated compatibility flag. Thread detail includes a bounded recent message + // page by default; pass `false` only to opt out when no message pagination params + // are present. IncludeMessages param.Field[ThreadGetParamsIncludeMessages] `query:"includeMessages"` } @@ -1006,7 +1204,9 @@ func (r ThreadGetParamsDebug) IsKnown() bool { return false } -// When true, includes message content in the thread detail. +// Deprecated compatibility flag. Thread detail includes a bounded recent message +// page by default; pass `false` only to opt out when no message pagination params +// are present. type ThreadGetParamsIncludeMessages string const ( @@ -1042,6 +1242,37 @@ func (r ThreadListParams) URLQuery() (v url.Values) { }) } +type ThreadActivityParams struct { + // Optional fleet id for index-backed authorization. + FleetID param.Field[string] `query:"fleetId"` +} + +// URLQuery serializes [ThreadActivityParams]'s query parameters as `url.Values`. +func (r ThreadActivityParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type ThreadListMessagesParams struct { + // Cursor returned by a previous thread messages response. + Cursor param.Field[string] `query:"cursor"` + // Optional fleet id for index-backed authorization. + FleetID param.Field[string] `query:"fleetId"` + // Maximum number of messages to include, capped at 500. + Limit param.Field[string] `query:"limit"` +} + +// URLQuery serializes [ThreadListMessagesParams]'s query parameters as +// `url.Values`. +func (r ThreadListMessagesParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type ThreadStartTurnParams struct { Message param.Field[string] `json:"message" api:"required"` Model param.Field[string] `json:"model"` diff --git a/thread_test.go b/thread_test.go index 3317281..a985a58 100644 --- a/thread_test.go +++ b/thread_test.go @@ -107,6 +107,35 @@ func TestThreadListWithOptionalParams(t *testing.T) { } } +func TestThreadActivityWithOptionalParams(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := cercago.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Threads.Activity( + context.TODO(), + "agent_abc123", + "thread_abc123", + cercago.ThreadActivityParams{ + FleetID: cercago.F("fleetId"), + }, + ) + if err != nil { + var apierr *cercago.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestThreadCancel(t *testing.T) { baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -185,6 +214,37 @@ func TestThreadCompact(t *testing.T) { } } +func TestThreadListMessagesWithOptionalParams(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := cercago.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Threads.ListMessages( + context.TODO(), + "agent_abc123", + "thread_abc123", + cercago.ThreadListMessagesParams{ + Cursor: cercago.F("42"), + FleetID: cercago.F("fleetId"), + Limit: cercago.F("100"), + }, + ) + if err != nil { + var apierr *cercago.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestThreadStartTurnWithOptionalParams(t *testing.T) { baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {