diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 6fb7c6a52..85bbd0005 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -368,7 +368,7 @@ definitions: $ref: '#/definitions/api.ExecutorConnection' data: items: - $ref: '#/definitions/store.ExecutorCredential' + $ref: '#/definitions/sessions.ExecutorCredential' type: array required: - connection @@ -1335,33 +1335,7 @@ definitions: source_commit: type: string type: object - store.AddressBindings: - properties: - hosted_sandboxes: - type: integer - nodes: - type: integer - nodes_on_other_address: - type: integer - self_hosted_executors: - type: integer - type: object - store.AdminAssetCounts: - properties: - agents: - type: integer - credentials: - type: integer - environment_templates: - type: integer - files: - type: integer - skills: - type: integer - vaults: - type: integer - type: object - store.ExecutorCredential: + sessions.ExecutorCredential: properties: created_at: type: string @@ -1372,7 +1346,7 @@ definitions: type: string x-nullable: true type: object - store.IssuedExecutorCredential: + sessions.IssuedExecutorCredential: properties: environment_id: type: string @@ -1381,7 +1355,7 @@ definitions: key_id: type: string type: object - store.ManagedSessionArchive: + sessions.ManagedArchive: properties: environment_id: type: string @@ -1390,6 +1364,32 @@ definitions: state: type: string type: object + store.AddressBindings: + properties: + hosted_sandboxes: + type: integer + nodes: + type: integer + nodes_on_other_address: + type: integer + self_hosted_executors: + type: integer + type: object + store.AdminAssetCounts: + properties: + agents: + type: integer + credentials: + type: integer + environment_templates: + type: integer + files: + type: integer + skills: + type: integer + vaults: + type: integer + type: object store.RuntimeNodeAllocation: properties: compute_phase: @@ -4162,7 +4162,7 @@ paths: "201": description: Created schema: - $ref: '#/definitions/store.IssuedExecutorCredential' + $ref: '#/definitions/sessions.IssuedExecutorCredential' "400": description: Bad Request schema: @@ -4771,7 +4771,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/store.ManagedSessionArchive' + $ref: '#/definitions/sessions.ManagedArchive' "400": description: Bad Request schema: @@ -4824,7 +4824,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/store.ManagedSessionArchive' + $ref: '#/definitions/sessions.ManagedArchive' "400": description: Bad Request schema: diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index fcc0cf90a..1a1463257 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -16,7 +16,7 @@ Two errors are shared across domains, each with one `api` helper: `textvalue.Err Shared vocabulary has one owner each, and domains use it rather than copy it. `internal/environmentconfig` owns Environment setup, Skills, Plugins and initial files with their validation and public metadata; `Setup.Validate` checks requested configuration, where a Skill may be an unresolved reference, and `Setup.ValidateInstalled` checks frozen, installable configuration. `internal/skills` owns `ParseVersion`, the canonical positive decimal Skill version. `internal/metadata` owns the metadata rules: `Validate` for the pair, key and value limits and U+0000, `ValidateStorable` for U+0000 alone, and `Encode` with its 64 KiB bound. `internal/jsonobject` owns `Normalize`, the stable encoding of stored JSON objects that snapshots and retry identities compare. These packages import no persistence. -`internal/sessions` owns the Session change vocabulary and its decisions: the public changes that report Turn and Session transitions, what a Turn that ends settles, measured Turn usage and the Session activity a change reports. `internal/items` owns public Items: it projects observations, merges them into stored Items and builds the ordered events that report each Item change. Neither imports persistence. `internal/persistence/postgres/sessionpg` loads the facts those decisions read and applies them inside the caller's Session transaction, under the Session lock: it allocates event sequence positions, event IDs, Item positions and output indexes, writes the journal, Items, Turn usage and Artifact settlement, and prunes the journal. It decides nothing. +`internal/sessions` owns the Session vocabulary: Sessions, Turns, inputs, Environments and their provisioning failures, function calls, Item and Artifact reads, executor credentials, and the errors Session operations return, which `api` maps in `writeSessionsError`. It also owns the Session change vocabulary and its decisions: the public changes that report Turn and Session transitions, what a Turn that ends settles, measured Turn usage and the Session activity a change reports. `internal/items` owns public Items: it projects observations, merges them into stored Items and builds the ordered events that report each Item change. Neither imports persistence. `internal/persistence/postgres/sessionpg` loads the facts those decisions read and applies them inside the caller's Session transaction, under the Session lock: it allocates event sequence positions, event IDs, Item positions and output indexes, writes the journal, Items, Turn usage and Artifact settlement, and prunes the journal. It decides nothing. `store` is transitional. `store.New` builds a pooled Store, and `store.NewExecution(s, lease)` builds the execution writer on a lease it borrows. An execution-only operation on a pooled Store fails with `store.ErrExecutionAuthority`. New adapters do not copy that check: their execution repositories require a `*pgunit.Lease` at construction, their public repositories expose no execution operation, and the check goes away with `store`. diff --git a/services/core/cmd/environment-key/main.go b/services/core/cmd/environment-key/main.go index 6554795ef..86b89fbfa 100644 --- a/services/core/cmd/environment-key/main.go +++ b/services/core/cmd/environment-key/main.go @@ -13,6 +13,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/databaseurl" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -111,7 +112,7 @@ func run() error { if options.revoke { return credentialOperationError(s.RevokeExecutorCredential(ctx, options.principal, options.keyID)) } - var credential store.IssuedExecutorCredential + var credential sessions.IssuedExecutorCredential if options.rotate { credential, err = s.RotateExecutorCredential(ctx, options.principal, options.keyID) } else { @@ -131,11 +132,11 @@ func credentialOperationError(err error) error { switch { case err == nil: return nil - case errors.Is(err, store.ErrExecutorCredentialExists): - return store.ErrExecutorCredentialExists - case errors.Is(err, store.ErrNotFound): + case errors.Is(err, sessions.ErrExecutorCredentialExists): + return sessions.ErrExecutorCredentialExists + case errors.Is(err, sessions.ErrNotFound): return errors.New("executor principal project mapping or authorized credential target not found") - case errors.Is(err, store.ErrInvalidInput): + case errors.Is(err, sessions.ErrInvalidInput): return errors.New("invalid executor credential identity or target") default: return errors.New("executor credential database operation failed") diff --git a/services/core/cmd/environment-key/main_test.go b/services/core/cmd/environment-key/main_test.go index 97c12ca4d..54cae3f0b 100644 --- a/services/core/cmd/environment-key/main_test.go +++ b/services/core/cmd/environment-key/main_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) const ( @@ -141,9 +141,9 @@ func TestCredentialOperationErrorsDoNotExposeDatabaseValues(t *testing.T) { const secret = "postgres://operator:private-password@database/execution" for _, err := range []error{ errors.New(secret), - fmt.Errorf("%w: %s", store.ErrInvalidInput, secret), - fmt.Errorf("%w: %s", store.ErrNotFound, secret), - fmt.Errorf("%w: %s", store.ErrExecutorCredentialExists, secret), + fmt.Errorf("%w: %s", sessions.ErrInvalidInput, secret), + fmt.Errorf("%w: %s", sessions.ErrNotFound, secret), + fmt.Errorf("%w: %s", sessions.ErrExecutorCredentialExists, secret), } { redacted := credentialOperationError(err) if redacted == nil || strings.Contains(redacted.Error(), secret) { @@ -153,7 +153,7 @@ func TestCredentialOperationErrorsDoNotExposeDatabaseValues(t *testing.T) { if err := credentialOperationError(nil); err != nil { t.Fatalf("successful revocation returned an error: %v", err) } - if !errors.Is(credentialOperationError(store.ErrExecutorCredentialExists), store.ErrExecutorCredentialExists) { + if !errors.Is(credentialOperationError(sessions.ErrExecutorCredentialExists), sessions.ErrExecutorCredentialExists) { t.Fatal("duplicate key guidance was lost") } } diff --git a/services/core/cmd/server/http_routes_test.go b/services/core/cmd/server/http_routes_test.go index aecd31cfd..c3c40e46a 100644 --- a/services/core/cmd/server/http_routes_test.go +++ b/services/core/cmd/server/http_routes_test.go @@ -93,14 +93,25 @@ func daemonComposition(t testing.TB) http.Handler { Vaults: struct{ api.Vaults }{}, VaultsReader: struct{ api.VaultsReader }{}, Files: struct{ api.Files }{}, FilesReader: struct{ api.FilesReader }{}, Skills: struct{ api.Skills }{}, SkillsReader: struct{ api.SkillsReader }{}, - Agents: struct{ api.Agents }{}, AgentsReader: struct{ api.AgentsReader }{}, Sessions: struct{ api.Sessions }{}, SessionEvents: struct{ api.SessionEvents }{}, + Agents: struct{ api.Agents }{}, AgentsReader: struct{ api.AgentsReader }{}, EnvironmentTemplates: struct{ api.EnvironmentTemplates }{}, EnvironmentTemplatesReader: struct{ api.EnvironmentTemplatesReader }{}, - SessionHistory: struct{ api.SessionHistory }{}, Subagents: struct{ api.Subagents }{}, Artifacts: struct{ api.Artifacts }{}, - SessionAdmin: struct{ api.SessionAdmin }{}, Environments: struct{ api.Environments }{}, ExecutorConnections: struct{ api.ExecutorConnections }{}, + Sessions: struct{ api.Sessions }{}, + SessionCreation: struct{ api.SessionCreation }{}, + SessionEvents: struct{ api.SessionEvents }{}, + Turns: struct{ api.Turns }{}, + Items: struct{ api.Items }{}, + Subagents: struct{ api.Subagents }{}, + Artifacts: struct{ api.Artifacts }{}, + SessionAdmin: struct{ api.SessionAdmin }{}, Environments: struct{ api.Environments }{}, ExecutorConnections: struct{ api.ExecutorConnections }{}, Admin: struct{ api.Admin }{}, AdminAudit: struct{ api.AdminAudit }{}, WriteAudit: struct{ api.WriteAudit }{}, Metrics: struct{ api.Metrics }{}, RuntimeObservations: struct{ api.RuntimeObservations }{}, RuntimeHistory: struct{ api.RuntimeHistory }{}, - Execution: &api.Execution{ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws", Admission: struct{ api.Admission }{}, - SessionArchive: struct{ api.SessionArchive }{}, Workspaces: struct{ api.EnvironmentWorkspaces }{}}, + Execution: &api.Execution{ + ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws", + SessionAdmission: struct{ api.SessionAdmission }{}, + InputAdmission: struct{ api.InputAdmission }{}, + SessionArchive: struct{ api.SessionArchive }{}, + Workspaces: struct{ api.EnvironmentWorkspaces }{}, + }, Sandboxes: &api.Sandboxes{Deployment: struct{ api.Deployment }{}, NodeAllocations: struct{ api.NodeAllocations }{}, DeploymentChanges: struct{ api.DeploymentChanges }{}, DeploymentReset: struct{ api.DeploymentReset }{}, ConfigurationDiscovery: struct{ api.ConfigurationDiscovery }{}}, }) diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go index 7e74fbfc4..7964810a1 100644 --- a/services/core/cmd/server/main.go +++ b/services/core/cmd/server/main.go @@ -369,14 +369,27 @@ func run() error { EnvironmentTemplates: environmentTemplates, EnvironmentTemplatesReader: templateStore, Files: fileService, FilesReader: fileStore, Agents: agentService, AgentsReader: agentStore, - Sessions: executionStore, SessionEvents: executionStore, SessionHistory: executionStore, - Subagents: executionStore, Artifacts: executionStore, SessionAdmin: executionStore, - Environments: executionStore, ExecutorConnections: executorConnections{store: executionStore, registry: registry}, + Sessions: executionStore, + SessionCreation: executionStore, + SessionEvents: executionStore, + Turns: executionStore, + Items: executionStore, + Subagents: executionStore, + Artifacts: executionStore, + SessionAdmin: executionStore, + Environments: executionStore, ExecutorConnections: executorConnections{store: executionStore, registry: registry}, Admin: executionStore, AdminAudit: auditStore, WriteAudit: auditStore, Metrics: metrics, RuntimeObservations: observationService, RuntimeHistory: historyService, } if worker != nil { - deps.Execution = &api.Execution{ExecutorURL: executorURL, Admission: worker, SessionArchive: worker, Workspaces: worker, NativeInstaller: nativeInstaller} + deps.Execution = &api.Execution{ + ExecutorURL: executorURL, + SessionAdmission: worker, + InputAdmission: worker, + SessionArchive: worker, + Workspaces: worker, + NativeInstaller: nativeInstaller, + } } if managedNodes != nil { deps.Sandboxes = &api.Sandboxes{ diff --git a/services/core/internal/api/admin_resources.go b/services/core/internal/api/admin_resources.go index f7a9b877c..fbddfcf92 100644 --- a/services/core/internal/api/admin_resources.go +++ b/services/core/internal/api/admin_resources.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" ) @@ -15,7 +16,7 @@ type adminTenantContextKey struct{} // Admin reads the administrator's cross-Project views: the asset summary and // the Sessions whose Runtime is observed. type Admin interface { - ReadAdminSummary(context.Context, string, store.AdminSummaryFilter, func(store.Session, *string) error) (store.AdminAssetCounts, error) + ReadAdminSummary(context.Context, string, store.AdminSummaryFilter, func(sessions.Session, *string) error) (store.AdminAssetCounts, error) ListAdminRuntimeTargets(context.Context, []string, string, int, bool) (store.AdminRuntimeTargetPage, error) } diff --git a/services/core/internal/api/admin_resources_test.go b/services/core/internal/api/admin_resources_test.go index dee33e1d6..2a64b778a 100644 --- a/services/core/internal/api/admin_resources_test.go +++ b/services/core/internal/api/admin_resources_test.go @@ -120,10 +120,10 @@ type summaryFixture struct { filter store.AdminSummaryFilter } -func (s *summaryFixture) ReadAdminSummary(_ context.Context, tenant string, filter store.AdminSummaryFilter, visit func(store.Session, *string) error) (store.AdminAssetCounts, error) { +func (s *summaryFixture) ReadAdminSummary(_ context.Context, tenant string, filter store.AdminSummaryFilter, visit func(sessions.Session, *string) error) (store.AdminAssetCounts, error) { s.tenant, s.filter = tenant, filter for i, usage := range []json.RawMessage{nil, json.RawMessage(`{"input_tokens":3,"output_tokens":5,"total_tokens":8,"input_tokens_details":{"cached_tokens":2},"output_tokens_details":{"reasoning_tokens":1}}`)} { - session := store.Session{ID: "session", TenantID: tenant, Configuration: json.RawMessage(`{"agent":{"id":"agent","model":"model","tools":[]},"environment":{"type":"none"}}`), CreatedAt: time.Unix(100+int64(i), 0), Usage: usage} + session := sessions.Session{ID: "session", TenantID: tenant, Configuration: json.RawMessage(`{"agent":{"id":"agent","model":"model","tools":[]},"environment":{"type":"none"}}`), CreatedAt: time.Unix(100+int64(i), 0), Usage: usage} if i == 0 { session.LastTurn = &sessions.Turn{Status: sessions.TurnInProgress, CreatedAt: time.Unix(110, 0)} } diff --git a/services/core/internal/api/admin_session_archive.go b/services/core/internal/api/admin_session_archive.go index f450318ff..a17268514 100644 --- a/services/core/internal/api/admin_session_archive.go +++ b/services/core/internal/api/admin_session_archive.go @@ -4,14 +4,14 @@ import ( "context" "net/http" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" ) // SessionArchive archives a managed Session through the execution owner; // SessionAdmin reads its archive state. type SessionArchive interface { - ArchiveManagedSession(context.Context, string, string, uint64) (store.ManagedSessionArchive, error) + ArchiveManagedSession(context.Context, string, string, uint64) (sessions.ManagedArchive, error) } type AdminSessionArchiveRequest struct { @@ -27,7 +27,7 @@ type AdminSessionArchiveRequest struct { // @Param project_id path string true "Project ID" // @Param session_id path string true "Session ID" // @Param body body api.AdminSessionArchiveRequest true "Current deployment generation" -// @Success 200 {object} store.ManagedSessionArchive +// @Success 200 {object} sessions.ManagedArchive // @Failure 400,401,404,409,413,500,503 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/sessions/{session_id}/archive [post] func (h *Handler) adminArchiveSession(w http.ResponseWriter, r *http.Request) { @@ -37,11 +37,11 @@ func (h *Handler) adminArchiveSession(w http.ResponseWriter, r *http.Request) { } var input AdminSessionArchiveRequest if decodeInputObject(raw, &input, "expected_generation") != nil || input.ExpectedGeneration == 0 { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } if h.Execution == nil { - writeStoreError(w, r, store.ErrEnvironmentUnavailable) + writeStoreError(w, r, sessions.ErrEnvironmentUnavailable) return } result, err := h.Execution.SessionArchive.ArchiveManagedSession(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), input.ExpectedGeneration) @@ -59,7 +59,7 @@ func (h *Handler) adminArchiveSession(w http.ResponseWriter, r *http.Request) { // @Security DeploymentAdminAuth // @Param project_id path string true "Project ID" // @Param session_id path string true "Session ID" -// @Success 200 {object} store.ManagedSessionArchive +// @Success 200 {object} sessions.ManagedArchive // @Failure 400,401,404,500,503 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/sessions/{session_id}/archive [get] func (h *Handler) adminGetSessionArchive(w http.ResponseWriter, r *http.Request) { diff --git a/services/core/internal/api/admin_session_archive_test.go b/services/core/internal/api/admin_session_archive_test.go index eb7f2c05b..6ef7550ff 100644 --- a/services/core/internal/api/admin_session_archive_test.go +++ b/services/core/internal/api/admin_session_archive_test.go @@ -8,7 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type archiveManagementFixture struct { @@ -19,19 +19,19 @@ type archiveManagementFixture struct { err error } -func (s *archiveManagementFixture) ArchiveManagedSession(ctx context.Context, tenant, session string, generation uint64) (store.ManagedSessionArchive, error) { +func (s *archiveManagementFixture) ArchiveManagedSession(ctx context.Context, tenant, session string, generation uint64) (sessions.ManagedArchive, error) { s.calls++ s.tenant, s.session, s.generation = tenant, session, generation source, ok := adminaudit.FromContext(ctx) s.audited = ok && source.ProjectID == managementProjectID && source.CredentialID != "" && source.RequestID != "" s.impersonated = ctx.Value(principalContextKey{}) != nil - return store.ManagedSessionArchive{SessionID: session, EnvironmentID: "environment", State: "cleanup_pending"}, s.err + return sessions.ManagedArchive{SessionID: session, EnvironmentID: "environment", State: "cleanup_pending"}, s.err } -func (s *archiveManagementFixture) GetManagedSessionArchive(_ context.Context, tenant, session string) (store.ManagedSessionArchive, error) { +func (s *archiveManagementFixture) GetManagedSessionArchive(_ context.Context, tenant, session string) (sessions.ManagedArchive, error) { s.calls++ s.tenant, s.session = tenant, session - return store.ManagedSessionArchive{SessionID: session, EnvironmentID: "environment", State: "released"}, s.err + return sessions.ManagedArchive{SessionID: session, EnvironmentID: "environment", State: "released"}, s.err } func TestAdminSessionArchiveAuthorityAndValidation(t *testing.T) { @@ -59,7 +59,7 @@ func TestAdminSessionArchiveAuthorityAndValidation(t *testing.T) { t.Fatal("invalid request reached store") } w := projectKeyHTTP(h, http.MethodPost, path, "admin", `{"expected_generation":2}`) - var result store.ManagedSessionArchive + var result sessions.ManagedArchive if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &result) != nil || result.State != "cleanup_pending" { t.Fatalf("archive: %d %s", w.Code, w.Body) } @@ -73,7 +73,7 @@ func TestAdminSessionArchiveAuthorityAndValidation(t *testing.T) { for _, failure := range []struct { err error status int - }{{deployment.ErrConflict, 409}, {store.ErrNotFound, 404}, {store.ErrInvalidInput, 400}} { + }{{deployment.ErrConflict, 409}, {sessions.ErrNotFound, 404}, {sessions.ErrInvalidInput, 400}} { fixture.err = failure.err if w := projectKeyHTTP(h, http.MethodPost, path, "admin", `{"expected_generation":2}`); w.Code != failure.status { t.Fatalf("archive error: %d %s", w.Code, w.Body) diff --git a/services/core/internal/api/admin_summary.go b/services/core/internal/api/admin_summary.go index fcd1f60bd..9805e7517 100644 --- a/services/core/internal/api/admin_summary.go +++ b/services/core/internal/api/admin_summary.go @@ -8,6 +8,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -45,11 +46,11 @@ func adminSummaryTime(r *http.Request, name string) (*time.Time, error) { return nil, nil } if len(values) != 1 || values[0] == "" { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } value, err := time.Parse(time.RFC3339Nano, values[0]) if err != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } return &value, nil } @@ -79,7 +80,7 @@ func (h *Handler) adminSummary(w http.ResponseWriter, r *http.Request) { group = "project" } if group != "project" && group != "agent" && group != "key" { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } after, err := adminSummaryTime(r, "created_after") @@ -93,19 +94,19 @@ func (h *Handler) adminSummary(w http.ResponseWriter, r *http.Request) { return } if after != nil && before != nil && !after.Before(*before) { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() if group == "agent" && r.URL.Query().Get("project_id") == "" { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } var page projects.Page if projectID := r.URL.Query().Get("project_id"); projectID != "" { if options.after != "" { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } var binding projects.Binding @@ -124,7 +125,7 @@ func (h *Handler) adminSummary(w http.ResponseWriter, r *http.Request) { if group == "project" { groups[""] = &AdminSummaryRow{ProjectID: project.ID} } - counts, err := h.Admin.ReadAdminSummary(ctx, project.TenantID, store.AdminSummaryFilter{CreatedAfter: after, CreatedBefore: before}, func(session store.Session, creationKeyID *string) error { + counts, err := h.Admin.ReadAdminSummary(ctx, project.TenantID, store.AdminSummaryFilter{CreatedAfter: after, CreatedBefore: before}, func(session sessions.Session, creationKeyID *string) error { projected, err := sessionResponse(session, h.executorURL()) if err != nil { return err diff --git a/services/core/internal/api/capability_archive.go b/services/core/internal/api/capability_archive.go index 451528ff9..fd4e87967 100644 --- a/services/core/internal/api/capability_archive.go +++ b/services/core/internal/api/capability_archive.go @@ -5,7 +5,7 @@ import ( "encoding/json" "github.com/MiniMax-AI/OpenAgentCore/internal/agentbundle" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // Skills and Plugins share the pinned inline capability source shape. @@ -16,11 +16,11 @@ func decodeCapabilityArchive(raw json.RawMessage) ([]byte, error) { Data string `json:"data"` } if decodeInputObject(raw, &source, "type", "media_type", "data") != nil || source.Type != "base64" || source.MediaType != "application/zip" || len(source.Data) > base64.StdEncoding.EncodedLen(agentbundle.MaxArchiveBytes) { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } body, err := base64.StdEncoding.Strict().DecodeString(source.Data) if err != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } return body, nil } diff --git a/services/core/internal/api/claude_admission_test.go b/services/core/internal/api/claude_admission_test.go index c1142d5e6..dc948d39e 100644 --- a/services/core/internal/api/claude_admission_test.go +++ b/services/core/internal/api/claude_admission_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestClaudeSessionConfigurationAdmission(t *testing.T) { @@ -47,9 +47,9 @@ func TestClaudeSessionConfigurationAdmission(t *testing.T) { d.Engine = "claude_sdk" admitSessions(d, f) // The Worker's stream admission reports that it cannot execute. - f.admission.createSessionStream = func(_ context.Context, _ string, input store.CreateSessionInput) (store.SessionCreation, error) { + f.sessionAdmission.createSessionStream = func(_ context.Context, _ string, input sessions.CreateSession) (sessions.Creation, error) { streamed = input.Engine - return store.SessionCreation{}, execution.ErrExecutionUnavailable + return sessions.Creation{}, execution.ErrExecutionUnavailable } f.metrics.recordUnavailable = func() {} }) diff --git a/services/core/internal/api/core_validation_errors.go b/services/core/internal/api/core_validation_errors.go index 44be8bacc..ccdf15d60 100644 --- a/services/core/internal/api/core_validation_errors.go +++ b/services/core/internal/api/core_validation_errors.go @@ -7,7 +7,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) const invalidNameMessage = "The name exceeds its length limit or contains invalid characters." @@ -18,7 +17,6 @@ func writeCoreValidationError(w http.ResponseWriter, err error) bool { if !isCoreErrorWriter(w) { return false } - var field *store.AdminValidationError var node *deployment.NodeValidationError var configuration *deployment.ConfigurationError switch { @@ -31,9 +29,6 @@ func writeCoreValidationError(w http.ResponseWriter, err error) bool { } writeCoreError(w, http.StatusBadRequest, node.Code, message, details, node.Param) return true - case errors.As(err, &field): - writeCoreError(w, http.StatusBadRequest, field.Code, invalidNameMessage, CoreErrorDetails{"max_length": CoreErrorNumber(float64(field.MaxLength))}, field.Param) - return true case errors.As(err, &configuration) && configuration.Validation != nil: field := configuration.Validation details := CoreErrorDetails{} diff --git a/services/core/internal/api/dependencies.go b/services/core/internal/api/dependencies.go index 705be6b12..cfc368596 100644 --- a/services/core/internal/api/dependencies.go +++ b/services/core/internal/api/dependencies.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // Dependencies is everything the handler uses. Each application area is one @@ -43,8 +43,10 @@ type Dependencies struct { Agents Agents AgentsReader AgentsReader Sessions Sessions + SessionCreation SessionCreation SessionEvents SessionEvents - SessionHistory SessionHistory + Turns Turns + Items Items Subagents Subagents Artifacts Artifacts SessionAdmin SessionAdmin @@ -74,10 +76,11 @@ type Dependencies struct { type Execution struct { // ExecutorURL is the validated daemon WebSocket URL that self-hosted // Sessions report and executors connect to. - ExecutorURL string - Admission Admission - SessionArchive SessionArchive - Workspaces EnvironmentWorkspaces + ExecutorURL string + SessionAdmission SessionAdmission + InputAdmission InputAdmission + SessionArchive SessionArchive + Workspaces EnvironmentWorkspaces // NativeInstaller is nil for a build without a source revision: the native // installation routes are then absent and Sessions carry no installation. NativeInstaller *NativeInstaller @@ -112,7 +115,7 @@ func NewHandler(deps Dependencies) (http.Handler, error) { } func (d Dependencies) validate() error { - if !store.ValidEngine(d.Engine) { + if !sessions.ValidEngine(d.Engine) { return errors.New("api: a valid default Harness is required") } if d.CoreKeys == nil { @@ -128,7 +131,11 @@ func (d Dependencies) validate() error { field{"Skills", d.Skills}, field{"SkillsReader", d.SkillsReader}, field{"Agents", d.Agents}, field{"AgentsReader", d.AgentsReader}, field{"Sessions", d.Sessions}, - field{"SessionEvents", d.SessionEvents}, field{"SessionHistory", d.SessionHistory}, field{"Subagents", d.Subagents}, + field{"SessionCreation", d.SessionCreation}, + field{"SessionEvents", d.SessionEvents}, + field{"Turns", d.Turns}, + field{"Items", d.Items}, + field{"Subagents", d.Subagents}, field{"Artifacts", d.Artifacts}, field{"SessionAdmin", d.SessionAdmin}, field{"Environments", d.Environments}, field{"ExecutorConnections", d.ExecutorConnections}, field{"Admin", d.Admin}, field{"AdminAudit", d.AdminAudit}, field{"WriteAudit", d.WriteAudit}, field{"Metrics", d.Metrics}, field{"RuntimeObservations", d.RuntimeObservations}, field{"RuntimeHistory", d.RuntimeHistory}, @@ -142,7 +149,12 @@ func (d Dependencies) validate() error { if e.NativeInstaller != nil && e.NativeInstaller.Version == "" { return errors.New("api: Execution.NativeInstaller.Version is required") } - if err := required(field{"Execution.Admission", e.Admission}, field{"Execution.SessionArchive", e.SessionArchive}, field{"Execution.Workspaces", e.Workspaces}); err != nil { + if err := required( + field{"Execution.SessionAdmission", e.SessionAdmission}, + field{"Execution.InputAdmission", e.InputAdmission}, + field{"Execution.SessionArchive", e.SessionArchive}, + field{"Execution.Workspaces", e.Workspaces}, + ); err != nil { return err } } diff --git a/services/core/internal/api/dependencies_test.go b/services/core/internal/api/dependencies_test.go index ea515c06f..e8c3d00b1 100644 --- a/services/core/internal/api/dependencies_test.go +++ b/services/core/internal/api/dependencies_test.go @@ -29,8 +29,10 @@ type testFakes struct { agents *fakeAgents agentsReader *fakeAgentsReader sessions *fakeSessions + sessionCreation *fakeSessionCreation sessionEvents *fakeSessionEvents - sessionHistory *fakeSessionHistory + turns *fakeTurns + items *fakeItems subagents *fakeSubagents artifacts *fakeArtifacts sessionAdmin *fakeSessionAdmin @@ -43,7 +45,8 @@ type testFakes struct { runtimeObservations *fakeRuntimeObservations runtimeHistory *fakeRuntimeHistory installationBindings *fakeInstallationBindings - admission *fakeAdmission + sessionAdmission *fakeSessionAdmission + inputAdmission *fakeInputAdmission sessionArchive *fakeSessionArchive workspaces *fakeEnvironmentWorkspaces deployment *fakeDeployment @@ -70,12 +73,18 @@ func testDependencies(t testing.TB) (Dependencies, *testFakes) { files: &fakeFiles{t: t}, filesReader: &fakeFilesReader{t: t}, skills: &fakeSkills{t: t}, skillsReader: &fakeSkillsReader{t: t}, agents: &fakeAgents{t: t}, agentsReader: &fakeAgentsReader{t: t}, - sessions: &fakeSessions{t: t}, sessionEvents: &fakeSessionEvents{t: t}, - sessionHistory: &fakeSessionHistory{t: t}, subagents: &fakeSubagents{t: t}, artifacts: &fakeArtifacts{t: t}, + sessions: &fakeSessions{t: t}, + sessionCreation: &fakeSessionCreation{t: t}, + sessionEvents: &fakeSessionEvents{t: t}, + turns: &fakeTurns{t: t}, + items: &fakeItems{t: t}, + subagents: &fakeSubagents{t: t}, artifacts: &fakeArtifacts{t: t}, sessionAdmin: &fakeSessionAdmin{t: t}, environments: &fakeEnvironments{t: t}, executorConnections: &fakeExecutorConnections{t: t}, admin: &fakeAdmin{t: t}, adminAudit: &fakeAdminAudit{t: t}, writeAudit: &fakeWriteAudit{t: t}, metrics: &fakeMetrics{t: t}, runtimeObservations: &fakeRuntimeObservations{t: t}, runtimeHistory: &fakeRuntimeHistory{t: t}, installationBindings: &fakeInstallationBindings{t: t}, - admission: &fakeAdmission{t: t}, sessionArchive: &fakeSessionArchive{t: t}, workspaces: &fakeEnvironmentWorkspaces{t: t}, + sessionAdmission: &fakeSessionAdmission{t: t}, + inputAdmission: &fakeInputAdmission{t: t}, + sessionArchive: &fakeSessionArchive{t: t}, workspaces: &fakeEnvironmentWorkspaces{t: t}, deployment: &fakeDeployment{t: t}, nodeAllocations: &fakeNodeAllocations{t: t}, deploymentChanges: &fakeDeploymentChanges{t: t}, deploymentReset: &fakeDeploymentReset{t: t}, configurationDiscovery: &fakeConfigurationDiscovery{t: t}, @@ -89,8 +98,12 @@ func testDependencies(t testing.TB) (Dependencies, *testFakes) { EnvironmentTemplates: f.environmentTemplates, EnvironmentTemplatesReader: f.environmentTemplatesReader, Skills: f.skills, SkillsReader: f.skillsReader, Agents: f.agents, AgentsReader: f.agentsReader, - Sessions: f.sessions, SessionEvents: f.sessionEvents, - SessionHistory: f.sessionHistory, Subagents: f.subagents, Artifacts: f.artifacts, SessionAdmin: f.sessionAdmin, + Sessions: f.sessions, + SessionCreation: f.sessionCreation, + SessionEvents: f.sessionEvents, + Turns: f.turns, + Items: f.items, + Subagents: f.subagents, Artifacts: f.artifacts, SessionAdmin: f.sessionAdmin, Environments: f.environments, ExecutorConnections: f.executorConnections, Admin: f.admin, AdminAudit: f.adminAudit, WriteAudit: f.writeAudit, Metrics: f.metrics, RuntimeObservations: f.runtimeObservations, RuntimeHistory: f.runtimeHistory, }, f @@ -99,7 +112,13 @@ func testDependencies(t testing.TB) (Dependencies, *testFakes) { // execution is an Execution group backed by f's strict fakes, reporting // testExecutorURL and without a native installer. func (f *testFakes) execution() *Execution { - return &Execution{ExecutorURL: testExecutorURL, Admission: f.admission, SessionArchive: f.sessionArchive, Workspaces: f.workspaces} + return &Execution{ + ExecutorURL: testExecutorURL, + SessionAdmission: f.sessionAdmission, + InputAdmission: f.inputAdmission, + SessionArchive: f.sessionArchive, + Workspaces: f.workspaces, + } } // sandboxes is a Sandboxes group backed by f's strict fakes. It requires @@ -167,14 +186,21 @@ func TestNewHandlerRejectsIncompleteDependencies(t *testing.T) { {"InstallationBindings", func(d *Dependencies, _ *testFakes) { d.InstallationBindings = nil }}, {"Projects", func(d *Dependencies, _ *testFakes) { d.Projects = nil }}, {"Sessions", func(d *Dependencies, _ *testFakes) { d.Sessions = nil }}, + {"SessionCreation", func(d *Dependencies, _ *testFakes) { d.SessionCreation = nil }}, + {"Turns", func(d *Dependencies, _ *testFakes) { d.Turns = nil }}, + {"Items", func(d *Dependencies, _ *testFakes) { d.Items = nil }}, {"RuntimeHistory", func(d *Dependencies, _ *testFakes) { d.RuntimeHistory = nil }}, {"Execution.ExecutorURL", func(d *Dependencies, f *testFakes) { d.Execution = f.execution() d.Execution.ExecutorURL = "" }}, - {"Execution.Admission", func(d *Dependencies, f *testFakes) { + {"Execution.SessionAdmission", func(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + d.Execution.SessionAdmission = nil + }}, + {"Execution.InputAdmission", func(d *Dependencies, f *testFakes) { d.Execution = f.execution() - d.Execution.Admission = nil + d.Execution.InputAdmission = nil }}, {"Execution.NativeInstaller.Version", func(d *Dependencies, f *testFakes) { d.Execution = f.execution() diff --git a/services/core/internal/api/environment_creation_test.go b/services/core/internal/api/environment_creation_test.go index 3a00b9ce1..b66a2e9c1 100644 --- a/services/core/internal/api/environment_creation_test.go +++ b/services/core/internal/api/environment_creation_test.go @@ -14,39 +14,39 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) type environmentCreationFixture struct { streamFixture - input store.CreateSessionInput + input sessions.CreateSession } -func (f *environmentCreationFixture) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) { - return store.SessionCreation{}, store.ErrNotFound +func (f *environmentCreationFixture) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (sessions.Creation, error) { + return sessions.Creation{}, sessions.ErrNotFound } -func (f *environmentCreationFixture) CreateSession(_ context.Context, tenant string, input store.CreateSessionInput) (store.Session, error) { +func (f *environmentCreationFixture) CreateSession(_ context.Context, tenant string, input sessions.CreateSession) (sessions.Session, error) { f.mu.Lock() defer f.mu.Unlock() f.input = input - f.session = store.Session{ID: uuid.NewString(), TenantID: tenant, Configuration: input.Configuration, Metadata: input.Metadata, CreatedAt: time.Unix(1700000000, 0)} + f.session = sessions.Session{ID: uuid.NewString(), TenantID: tenant, Configuration: input.Configuration, Metadata: input.Metadata, CreatedAt: time.Unix(1700000000, 0)} var snapshot struct { Environment json.RawMessage `json:"environment"` } if err := json.Unmarshal(input.Configuration, &snapshot); err != nil { - return store.Session{}, err + return sessions.Session{}, err } - f.session.Environment = &store.Environment{ + f.session.Environment = &sessions.Environment{ ID: uuid.NewString(), SessionID: f.session.ID, TenantID: tenant, Status: "pending", Configuration: snapshot.Environment, } return f.session, nil } -func (f *environmentCreationFixture) CreateSessionStream(ctx context.Context, tenant string, input store.CreateSessionInput) (store.SessionCreation, error) { +func (f *environmentCreationFixture) CreateSessionStream(ctx context.Context, tenant string, input sessions.CreateSession) (sessions.Creation, error) { session, err := f.CreateSession(ctx, tenant, input) - return store.SessionCreation{Session: session, Created: true}, err + return sessions.Creation{Session: session, Created: true}, err } // environmentCreationHandler serves Session creation and reads from a fresh @@ -63,7 +63,7 @@ func environmentCreationHandler(t *testing.T, engine string, configure ...func(* TokenSHA256: runtimedevice.HashCredential("key"), TenantID: uuid.NewString(), }).ResolveAPIKey fixture.serve(fakes) - fakes.sessions.findSessionCreation, fakes.sessions.createSession, fakes.sessions.createSessionStream = fixture.FindSessionCreation, fixture.CreateSession, fixture.CreateSessionStream + fakes.sessionCreation.findSessionCreation, fakes.sessionCreation.createSession, fakes.sessionCreation.createSessionStream = fixture.FindSessionCreation, fixture.CreateSession, fixture.CreateSessionStream fakes.modelProviders.resolve = fixtureDeploymentProvider for _, c := range configure { c(&deps, fakes) diff --git a/services/core/internal/api/environment_executor_management.go b/services/core/internal/api/environment_executor_management.go index acfda8f83..2b03da88a 100644 --- a/services/core/internal/api/environment_executor_management.go +++ b/services/core/internal/api/environment_executor_management.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) @@ -28,8 +28,8 @@ type EnvironmentExecutorCredentialRequest struct { // ExecutorCredentialList holds credential metadata only, never a secret. type ExecutorCredentialList struct { - Data []store.ExecutorCredential `json:"data" binding:"required"` - Connection ExecutorConnection `json:"connection" binding:"required"` + Data []sessions.ExecutorCredential `json:"data" binding:"required"` + Connection ExecutorConnection `json:"connection" binding:"required"` } // ExecutorConnection reports binding history and current Core-observed connectivity. @@ -77,7 +77,7 @@ func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request connection = ExecutorConnection{Status: "disconnected", BoundKeyID: observed.BoundKeyID, EnrolledAt: observed.EnrolledAt, LastSeenAt: observed.LastSeenAt} if observed.EnvironmentStatus == "connected" && observed.CredentialHash != "" { connected, err := h.ExecutorConnections.ExecutorConnected(r.Context(), state.EnvironmentID, observed.CredentialHash) - if err != nil && !errors.Is(err, store.ErrNotFound) && !errors.Is(err, store.ErrDeviceBindingConflict) { + if err != nil && !errors.Is(err, sessions.ErrNotFound) && !errors.Is(err, sessions.ErrDeviceBindingConflict) { writeStoreError(w, r, err) return } @@ -98,7 +98,7 @@ func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request // @Param project_id path string true "Project UUID" // @Param environment_id path string true "Environment UUID" // @Param body body api.EnvironmentExecutorCredentialRequest true "Request" -// @Success 201 {object} store.IssuedExecutorCredential +// @Success 201 {object} sessions.IssuedExecutorCredential // @Failure 400,401,404,409,500 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/environments/{environment_id}/executor-credentials [post] func (h *Handler) issueExecutorCredential(w http.ResponseWriter, r *http.Request) { @@ -111,7 +111,7 @@ func (h *Handler) issueExecutorCredential(w http.ResponseWriter, r *http.Request var input EnvironmentExecutorCredentialRequest var fields map[string]json.RawMessage if decodeInputObject(raw, &input, "key_id", "rotate") != nil || json.Unmarshal(raw, &fields) != nil || bytes.Equal(bytes.TrimSpace(fields["rotate"]), []byte("null")) || !executorManagementID(input.KeyID) { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } binding, ok := h.adminProjectScope(w, r) @@ -143,7 +143,7 @@ func (h *Handler) revokeExecutorCredential(w http.ResponseWriter, r *http.Reques } keyID := chi.URLParam(r, "key_id") if !executorManagementID(keyID) { - writeStoreError(w, r, store.ErrNotFound) + writeStoreError(w, r, sessions.ErrNotFound) return } if err := h.Environments.RevokeProjectExecutorCredential(r.Context(), binding.Principal, chi.URLParam(r, "environment_id"), keyID); err != nil { diff --git a/services/core/internal/api/environment_executor_management_test.go b/services/core/internal/api/environment_executor_management_test.go index 306fb3eac..25895cbc0 100644 --- a/services/core/internal/api/environment_executor_management_test.go +++ b/services/core/internal/api/environment_executor_management_test.go @@ -12,7 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -22,7 +22,7 @@ type executorManagementFixture struct { rotate, audited bool calls int err error - connection store.ExecutorConnectionState + connection sessions.ExecutorConnectionState resolvedEnvironment string } @@ -31,18 +31,18 @@ func (f *executorManagementFixture) record(ctx context.Context, principal identi _, f.audited = adminaudit.FromContext(ctx) f.calls++ } -func (f *executorManagementFixture) ProjectExecutorCredentialState(ctx context.Context, principal identity.Principal, environment string) (store.ExecutorCredentialState, error) { +func (f *executorManagementFixture) ProjectExecutorCredentialState(ctx context.Context, principal identity.Principal, environment string) (sessions.ExecutorCredentialState, error) { f.record(ctx, principal, environment, "") resolved := f.resolvedEnvironment if resolved == "" { resolved = environment } - return store.ExecutorCredentialState{EnvironmentID: resolved, Credentials: []store.ExecutorCredential{{KeyID: "listed", CreatedAt: time.Unix(1, 0).UTC()}}, Connection: f.connection}, f.err + return sessions.ExecutorCredentialState{EnvironmentID: resolved, Credentials: []sessions.ExecutorCredential{{KeyID: "listed", CreatedAt: time.Unix(1, 0).UTC()}}, Connection: f.connection}, f.err } -func (f *executorManagementFixture) IssueProjectExecutorCredential(ctx context.Context, principal identity.Principal, environment, key string, rotate bool) (store.IssuedExecutorCredential, error) { +func (f *executorManagementFixture) IssueProjectExecutorCredential(ctx context.Context, principal identity.Principal, environment, key string, rotate bool) (sessions.IssuedExecutorCredential, error) { f.record(ctx, principal, environment, key) f.rotate = rotate - return store.IssuedExecutorCredential{KeyID: key, EnvironmentID: environment, Token: "synthetic-connect-only"}, f.err + return sessions.IssuedExecutorCredential{KeyID: key, EnvironmentID: environment, Token: "synthetic-connect-only"}, f.err } func (f *executorManagementFixture) RevokeProjectExecutorCredential(ctx context.Context, principal identity.Principal, environment, key string) error { f.record(ctx, principal, environment, key) @@ -116,7 +116,7 @@ func TestProjectExecutorCredentialsHTTP(t *testing.T) { t.Fatal("invalid request accepted", w.Code, body) } } - f.err = store.ErrExecutorCredentialExists + f.err = sessions.ErrExecutorCredentialExists if w := projectKeyHTTP(h, "POST", path, "admin", body); w.Code != 409 || !strings.Contains(w.Body.String(), `"code":"executor_credential_exists"`) || strings.Contains(w.Body.String(), "synthetic-connect-only") { t.Fatal("uncertain retry", w.Code, w.Body) } @@ -124,7 +124,7 @@ func TestProjectExecutorCredentialsHTTP(t *testing.T) { if w := projectKeyHTTP(h, "POST", path, "admin", body); w.Code != 409 || !strings.Contains(w.Body.String(), `"code":"project_archived"`) { t.Fatal("archived Project", w.Code, w.Body) } - f.err = store.ErrNotFound + f.err = sessions.ErrNotFound // Rotating a key_id that was never issued is not found. if w := projectKeyHTTP(h, "POST", path, "admin", `{"key_id":"`+uuid.NewString()+`","rotate":true}`); w.Code != 404 || !f.rotate { t.Fatal("unknown key rotation", w.Code) @@ -149,14 +149,14 @@ func TestExecutorConnectionListObservation(t *testing.T) { status int }{ {"live", true, nil, "connected", 200}, {"closed", false, nil, "disconnected", 200}, - {"rotated", false, store.ErrDeviceBindingConflict, "disconnected", 200}, - {"revoked", false, store.ErrNotFound, "disconnected", 200}, {"database failure", false, errors.New("private-database"), "", 500}, + {"rotated", false, sessions.ErrDeviceBindingConflict, "disconnected", 200}, + {"revoked", false, sessions.ErrNotFound, "disconnected", 200}, {"database failure", false, errors.New("private-database"), "", 500}, } { t.Run(tc.name, func(t *testing.T) { key := callerBinding() at := time.Unix(1, 0).UTC() bound := "bound-key" - f := &executorManagementFixture{connection: store.ExecutorConnectionState{DeviceID: "device", BoundKeyID: &bound, EnrolledAt: &at, CredentialHash: "private-digest", EnvironmentStatus: "connected"}} + f := &executorManagementFixture{connection: sessions.ExecutorConnectionState{DeviceID: "device", BoundKeyID: &bound, EnrolledAt: &at, CredentialHash: "private-digest", EnvironmentStatus: "connected"}} h := executorManagementHandler(t, key, f, func(_ context.Context, environment, digest string) (bool, error) { if environment != "environment" || digest != "private-digest" { t.Fatal("wrong binding") @@ -185,13 +185,13 @@ func TestExecutorConnectionListUsesResolvedEnvironment(t *testing.T) { key := callerBinding() f := &executorManagementFixture{ resolvedEnvironment: canonical, - connection: store.ExecutorConnectionState{DeviceID: "device", CredentialHash: "private-digest", EnvironmentStatus: "connected"}, + connection: sessions.ExecutorConnectionState{DeviceID: "device", CredentialHash: "private-digest", EnvironmentStatus: "connected"}, } observations := 0 h := executorManagementHandler(t, key, f, func(_ context.Context, environment, digest string) (bool, error) { observations++ if environment != canonical || digest != "private-digest" { - return false, store.ErrDeviceBindingConflict + return false, sessions.ErrDeviceBindingConflict } return true, nil }) diff --git a/services/core/internal/api/environment_files.go b/services/core/internal/api/environment_files.go index e57d3e0f8..fbab274bf 100644 --- a/services/core/internal/api/environment_files.go +++ b/services/core/internal/api/environment_files.go @@ -12,15 +12,15 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" ) // EnvironmentWorkspaces reads and writes a connected Environment's live // workspace through its Runtime. type EnvironmentWorkspaces interface { - ReadEnvironmentDirectory(context.Context, store.Environment, string) (proto.WorkspaceDirectoryResult, error) - WriteEnvironmentFile(context.Context, store.Environment, string, []byte) (int64, error) + ReadEnvironmentDirectory(context.Context, sessions.Environment, string) (proto.WorkspaceDirectoryResult, error) + WriteEnvironmentFile(context.Context, sessions.Environment, string, []byte) (int64, error) } // @Summary List live Environment files @@ -97,7 +97,7 @@ var errHostedEnvironmentProvisioning = &fieldError{message: "the hosted environm // environmentFilesAccessible rejects Files operations on an openai_hosted // Environment whose first connection has not been observed (HE-18). Callers // run it after the tenant-scoped lookup, so foreign Environments stay missing. -func environmentFilesAccessible(w http.ResponseWriter, environment store.Environment) bool { +func environmentFilesAccessible(w http.ResponseWriter, environment sessions.Environment) bool { var configuration struct { Type string `json:"type"` } diff --git a/services/core/internal/api/environment_files_create.go b/services/core/internal/api/environment_files_create.go index 1f39433b7..0bc6aaedd 100644 --- a/services/core/internal/api/environment_files_create.go +++ b/services/core/internal/api/environment_files_create.go @@ -18,7 +18,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/echotext" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" ) @@ -56,28 +56,28 @@ func (h *Handler) createEnvironmentFile(w http.ResponseWriter, r *http.Request) return } if err := decodeInputObject(raw, &request, fields...); err != nil || request.Path == nil { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } switch request.Type { case "inline": fields = []string{"type", "path", "data"} if request.Data == nil { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } case "file_id": fields = []string{"type", "path", "file_id"} if request.FileID == nil || *request.FileID == "" { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } default: - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } if decodeInputObject(raw, &request, fields...) != nil { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } if err := environmentFileCreatePathError(*request.Path); err != nil { @@ -88,7 +88,7 @@ func (h *Handler) createEnvironmentFile(w http.ResponseWriter, r *http.Request) if request.Type == "inline" { data, err = base64.StdEncoding.Strict().DecodeString(*request.Data) if err != nil { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } if len(data) > maxInlineEnvironmentFileBytes { diff --git a/services/core/internal/api/environment_files_create_test.go b/services/core/internal/api/environment_files_create_test.go index d7f14b6c3..5129de312 100644 --- a/services/core/internal/api/environment_files_create_test.go +++ b/services/core/internal/api/environment_files_create_test.go @@ -13,7 +13,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -26,7 +26,7 @@ type environmentFileCreateFixture struct { wrongSize bool } -func (f *environmentFileCreateFixture) WriteEnvironmentFile(_ context.Context, environment store.Environment, path string, data []byte) (int64, error) { +func (f *environmentFileCreateFixture) WriteEnvironmentFile(_ context.Context, environment sessions.Environment, path string, data []byte) (int64, error) { f.writes++ f.readEnvironment, f.path, f.data = environment, path, append([]byte(nil), data...) if f.wrongSize { diff --git a/services/core/internal/api/environment_files_query.go b/services/core/internal/api/environment_files_query.go index 4237e23e1..503b96dfd 100644 --- a/services/core/internal/api/environment_files_query.go +++ b/services/core/internal/api/environment_files_query.go @@ -7,7 +7,7 @@ import ( "strings" "unicode/utf8" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type environmentFileOptions struct { @@ -31,12 +31,12 @@ var ( // ignored and a repeated supported key uses the Beta duplicate-field error. // Unlike the shared lists, which drop malformed pairs, it rejects malformed // query encoding. -func readEnvironmentFileQuery(w http.ResponseWriter, r *http.Request, environment store.Environment) (environmentFileOptions, bool) { +func readEnvironmentFileQuery(w http.ResponseWriter, r *http.Request, environment sessions.Environment) (environmentFileOptions, bool) { var options environmentFileOptions // Malformed query encoding remains a local rejection; no official sample exists. q, err := url.ParseQuery(r.URL.RawQuery) if err != nil { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return options, false } for _, key := range environmentFileQueryKeys { diff --git a/services/core/internal/api/environment_files_test.go b/services/core/internal/api/environment_files_test.go index f54e27c90..4ad1bffa0 100644 --- a/services/core/internal/api/environment_files_test.go +++ b/services/core/internal/api/environment_files_test.go @@ -16,32 +16,32 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) type environmentFilesFixture struct { - environment store.Environment + environment sessions.Environment result proto.WorkspaceDirectoryResult storeError, readError error lookups, reads int directory string - readEnvironment store.Environment + readEnvironment sessions.Environment readDelay time.Duration } -func (f *environmentFilesFixture) GetEnvironment(_ context.Context, tenant, id string) (store.Environment, error) { +func (f *environmentFilesFixture) GetEnvironment(_ context.Context, tenant, id string) (sessions.Environment, error) { f.lookups++ if f.storeError != nil { - return store.Environment{}, f.storeError + return sessions.Environment{}, f.storeError } if tenant != f.environment.TenantID || id != f.environment.ID { - return store.Environment{}, store.ErrNotFound + return sessions.Environment{}, sessions.ErrNotFound } return f.environment, nil } -func (f *environmentFilesFixture) ReadEnvironmentDirectory(ctx context.Context, environment store.Environment, directory string) (proto.WorkspaceDirectoryResult, error) { +func (f *environmentFilesFixture) ReadEnvironmentDirectory(ctx context.Context, environment sessions.Environment, directory string) (proto.WorkspaceDirectoryResult, error) { f.reads++ f.directory, f.readEnvironment = directory, environment if f.readDelay > 0 { @@ -56,7 +56,7 @@ func (f *environmentFilesFixture) ReadEnvironmentDirectory(ctx context.Context, func newEnvironmentFilesFixture() *environmentFilesFixture { return &environmentFilesFixture{ - environment: store.Environment{ID: uuid.NewString(), TenantID: uuid.NewString(), SessionID: uuid.NewString(), Status: "connected", + environment: sessions.Environment{ID: uuid.NewString(), TenantID: uuid.NewString(), SessionID: uuid.NewString(), Status: "connected", Configuration: json.RawMessage(`{"type":"self_hosted","workspace_directory":"/workspace"}`)}, result: proto.WorkspaceDirectoryResult{Entries: []proto.WorkspaceDirectoryEntry{}}, } @@ -232,7 +232,7 @@ func TestEnvironmentFilesSafeStoreAndReaderFailures(t *testing.T) { err error status int }{ - {store.ErrNotFound, 404}, {store.ErrInvalidInput, 400}, {execution.ErrExecutionUnavailable, 503}, {errors.New("private-native-secret"), 500}, + {sessions.ErrNotFound, 404}, {sessions.ErrInvalidInput, 400}, {execution.ErrExecutionUnavailable, 503}, {errors.New("private-native-secret"), 500}, } { unavailable := 0 h, f := environmentFilesHandler(t, true, countEnvironmentFilesUnavailable(&unavailable)) diff --git a/services/core/internal/api/environment_files_write_test.go b/services/core/internal/api/environment_files_write_test.go index 29beb0b02..38233d92c 100644 --- a/services/core/internal/api/environment_files_write_test.go +++ b/services/core/internal/api/environment_files_write_test.go @@ -9,7 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -69,7 +69,7 @@ func TestEnvironmentFileCreateDestinationConflicts(t *testing.T) { {execution.ErrEnvironmentFileDirectory, "invalid_request_error", conflict}, {execution.ErrEnvironmentFileUnsafe, "invalid_request_error", unsafe}, // A generic installer rejection keeps the local code. - {store.ErrInvalidInput, "invalid_request", "Invalid resource identifier or request limits."}, + {sessions.ErrInvalidInput, "invalid_request", "Invalid resource identifier or request limits."}, } { f.err = tc.err assertListQueryError(t, requestCreateEnvironmentFile(h, f.environment.ID, body, "files-key"), tc.code, nil, tc.message) diff --git a/services/core/internal/api/environment_input_test.go b/services/core/internal/api/environment_input_test.go index a8db592b2..a0803f2b3 100644 --- a/services/core/internal/api/environment_input_test.go +++ b/services/core/internal/api/environment_input_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestPublicEnvironmentInputFailureMappings(t *testing.T) { @@ -20,7 +20,7 @@ func TestPublicEnvironmentInputFailureMappings(t *testing.T) { status int code string }{ - {store.ErrEnvironmentUnavailable, http.StatusConflict, "environment_unavailable"}, + {sessions.ErrEnvironmentUnavailable, http.StatusConflict, "environment_unavailable"}, {execution.ErrEnvironmentInputExpired, http.StatusConflict, "environment_input_expired"}, {execution.ErrEnvironmentInputCancelled, http.StatusConflict, "environment_input_cancelled"}, {execution.ErrExecutionUnavailable, http.StatusServiceUnavailable, "execution_unavailable"}, @@ -52,7 +52,7 @@ type waitingEnvironmentInput struct { release chan struct{} } -func (s *waitingEnvironmentInput) SubmitInputs(ctx context.Context, _, _, _ string, _ []store.Input) ([]store.InputReceipt, error) { +func (s *waitingEnvironmentInput) SubmitInputs(ctx context.Context, _, _, _ string, _ []sessions.Input) ([]sessions.InputReceipt, error) { close(s.entered) select { case <-s.release: @@ -73,7 +73,7 @@ func TestPreparedEnvironmentInputWaitExtendsOnlyItsResponseDeadline(t *testing.T // The Worker admits the initial input into the fixture and waits on // the next input. handler, fixture := environmentCreationHandler(t, "codex", selfHostedExecution, func(_ *Dependencies, f *testFakes) { - f.admission.createSession, f.admission.submitInputs = f.sessions.createSession, waiting.SubmitInputs + f.sessionAdmission.createSession, f.inputAdmission.submitInputs = f.sessionCreation.createSession, waiting.SubmitInputs }) create := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"MiniMax-M3"},"environment":`+environmentJSON+`,"input":"Prepare the response deadline fixture."}`)) create.Header.Set("Authorization", "Bearer key") diff --git a/services/core/internal/api/environment_installation.go b/services/core/internal/api/environment_installation.go index 30c102f82..121e23f6f 100644 --- a/services/core/internal/api/environment_installation.go +++ b/services/core/internal/api/environment_installation.go @@ -9,7 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/nativeinstaller" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" ) @@ -82,16 +82,16 @@ func (h *Handler) registerNativeInstallationRoutes(r chi.Router) { // installationAuthorization validates a grant route's bearer grant. The routes // are registered only when this Core serves a native installer. -func (h *Handler) installationAuthorization(w http.ResponseWriter, r *http.Request) (store.InstallationAuthorization, string, bool) { +func (h *Handler) installationAuthorization(w http.ResponseWriter, r *http.Request) (sessions.InstallationAuthorization, string, bool) { w.Header().Set("Cache-Control", "no-store") if h.Execution.NativeInstaller.Catalog == nil { writeError(w, 503, "installation_unavailable", "Matching native installation artifacts are unavailable.") - return store.InstallationAuthorization{}, "", false + return sessions.InstallationAuthorization{}, "", false } parts := strings.Fields(r.Header.Get("Authorization")) if len(r.Header.Values("Authorization")) != 1 || len(parts) != 2 || parts[0] != "Bearer" { - writeStoreError(w, r, store.ErrInstallationAuthorization) - return store.InstallationAuthorization{}, "", false + writeStoreError(w, r, sessions.ErrInstallationAuthorization) + return sessions.InstallationAuthorization{}, "", false } claim, err := h.Environments.ValidateEnvironmentInstallation(r.Context(), parts[1], h.Execution.NativeInstaller.Version) if err != nil { @@ -154,7 +154,7 @@ func (h *Handler) claimNativeInstallation(w http.ResponseWriter, r *http.Request } var input NativeInstallationClaim if decodeInputObject(raw, &input, "executor_token") != nil { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } if err := h.Environments.ClaimEnvironmentInstallation(r.Context(), token, h.Execution.NativeInstaller.Version, input.ExecutorToken); err != nil { diff --git a/services/core/internal/api/environment_installation_test.go b/services/core/internal/api/environment_installation_test.go index d94e18c08..828342045 100644 --- a/services/core/internal/api/environment_installation_test.go +++ b/services/core/internal/api/environment_installation_test.go @@ -12,7 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/nativeinstaller" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -23,20 +23,20 @@ type installationFixture struct { func (f *installationFixture) AuthorizeEnvironmentInstallation(_ context.Context, p identity.Principal, environment, version string) (string, int64, error) { if p.TenantID != f.session.TenantID || environment != f.session.Environment.ID || version != "build" { - return "", 0, store.ErrNotFound + return "", 0, sessions.ErrNotFound } f.authorizedEnvironment = environment return "short-lived-install-grant", 2000000000, nil } -func (f *installationFixture) ValidateEnvironmentInstallation(context.Context, string, string) (store.InstallationAuthorization, error) { - return store.InstallationAuthorization{}, store.ErrInstallationAuthorization +func (f *installationFixture) ValidateEnvironmentInstallation(context.Context, string, string) (sessions.InstallationAuthorization, error) { + return sessions.InstallationAuthorization{}, sessions.ErrInstallationAuthorization } func TestSelfHostedCreationReturnsInstallationWithoutWebCredential(t *testing.T) { f := &installationFixture{} deps, fakes := testDependencies(t) fakes.projectsReader.resolveAPIKey = projectKeys(t, APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("project-key"), TenantID: uuid.NewString()}).ResolveAPIKey - fakes.sessions.findSessionCreation, fakes.sessions.createSession = f.FindSessionCreation, f.CreateSession + fakes.sessionCreation.findSessionCreation, fakes.sessionCreation.createSession = f.FindSessionCreation, f.CreateSession fakes.modelProviders.resolve = fixtureDeploymentProvider fakes.environments.authorizeEnvironmentInstallation, fakes.environments.validateEnvironmentInstallation = f.AuthorizeEnvironmentInstallation, f.ValidateEnvironmentInstallation deps.Execution = fakes.execution() diff --git a/services/core/internal/api/environment_network_test.go b/services/core/internal/api/environment_network_test.go index 09da6b014..f3b387582 100644 --- a/services/core/internal/api/environment_network_test.go +++ b/services/core/internal/api/environment_network_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestRestrictedNetworkPublicMetadataPreservesInput(t *testing.T) { @@ -25,7 +25,7 @@ func TestRestrictedNetworkPublicMetadataPreservesInput(t *testing.T) { if err != nil || !reflect.DeepEqual(env.Network.AllowedDomains, domains) { t.Fatal("inline input changed", env, err) } - session, err := hostedSessionEnvironment(store.Environment{ID: "environment", Configuration: raw}) + session, err := hostedSessionEnvironment(sessions.Environment{ID: "environment", Configuration: raw}) if err != nil || !reflect.DeepEqual(session.Network.AllowedDomains, domains) { t.Fatal("frozen Session metadata changed", session, err) } diff --git a/services/core/internal/api/environment_plugins.go b/services/core/internal/api/environment_plugins.go index 63cdb9d43..134668071 100644 --- a/services/core/internal/api/environment_plugins.go +++ b/services/core/internal/api/environment_plugins.go @@ -5,7 +5,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func decodeEnvironmentPlugins(raw json.RawMessage) ([]environmentconfig.Plugin, error) { @@ -14,7 +14,7 @@ func decodeEnvironmentPlugins(raw json.RawMessage) ([]environmentconfig.Plugin, } var entries []json.RawMessage if json.Unmarshal(raw, &entries) != nil || len(entries) > 50 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } result := make([]environmentconfig.Plugin, 0, len(entries)) for _, entry := range entries { @@ -25,7 +25,7 @@ func decodeEnvironmentPlugins(raw json.RawMessage) ([]environmentconfig.Plugin, Source json.RawMessage `json:"source"` } if decodeInputObject(entry, &input, "type", "name", "description", "source") != nil || input.Type != "inline" { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } body, err := decodeCapabilityArchive(input.Source) if err != nil { @@ -51,13 +51,13 @@ func storedPlugins(raw json.RawMessage) ([]json.RawMessage, error) { } var entries []json.RawMessage if json.Unmarshal(raw, &entries) != nil || len(entries) > 50 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } seen := map[string]bool{} for _, entry := range entries { var metadata agentplugin.Metadata if decodeInputObject(entry, &metadata, "type", "name", "description") != nil || metadata.Type != "inline" || metadata.Name == "" || metadata.Description == "" || seen[metadata.Name] { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } seen[metadata.Name] = true } diff --git a/services/core/internal/api/environment_plugins_test.go b/services/core/internal/api/environment_plugins_test.go index a1cb17722..d4ba2365e 100644 --- a/services/core/internal/api/environment_plugins_test.go +++ b/services/core/internal/api/environment_plugins_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func pluginInput(t *testing.T) json.RawMessage { @@ -72,7 +72,7 @@ func TestPluginsSharedParsingConfidentialMetadataAndOverrides(t *testing.T) { if err != nil || len(stored.Plugins) != 1 || len(stored.CapabilityDirectories) != 1 { t.Fatal("metadata", err) } - env := store.Environment{ID: "environment", Status: "connected", Configuration: snapshot.Environment} + env := sessions.Environment{ID: "environment", Status: "connected", Configuration: snapshot.Environment} if response, err := environmentResponse(env); err != nil || len(response.Plugins) != 1 { t.Fatal("environment response", err) } diff --git a/services/core/internal/api/environment_preparation_input.go b/services/core/internal/api/environment_preparation_input.go index e20320abd..d0d43e4e7 100644 --- a/services/core/internal/api/environment_preparation_input.go +++ b/services/core/internal/api/environment_preparation_input.go @@ -5,7 +5,7 @@ import ( "encoding/json" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // The official placement union and the Core extension converge before resource @@ -20,21 +20,21 @@ func preparationEnvironmentInput(raw json.RawMessage, extension *v1.SessionExecu } var fields, setup map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil || json.Unmarshal(extension.Environment, &setup) != nil || setup == nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } var kind string if json.Unmarshal(fields["type"], &kind) != nil || (kind != "openai_hosted" && kind != "self_hosted") { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } for name, value := range setup { switch name { case "environment_template_id", "files", "env", "packages", "setup_commands", "skills", "plugins", "capability_directories": default: - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } // Duplicate sources are ambiguous, including an explicit null. if _, exists := fields[name]; exists { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } fields[name] = value } diff --git a/services/core/internal/api/environment_request.go b/services/core/internal/api/environment_request.go index e142e43a0..b3c045621 100644 --- a/services/core/internal/api/environment_request.go +++ b/services/core/internal/api/environment_request.go @@ -5,17 +5,17 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func decodeSessionEnvironment(raw json.RawMessage) (*v1.Environment, error) { var environment v1.Environment if json.Unmarshal(raw, &environment) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } var input map[string]json.RawMessage if json.Unmarshal(raw, &input) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } if err := rejectSystemPackages(input["packages"]); err != nil { return nil, err @@ -28,10 +28,10 @@ func decodeSessionEnvironment(raw json.RawMessage) (*v1.Environment, error) { case "self_hosted": fields = append(fields, "workspace_directory", "capability_directories") if agentcapabilities.ValidateSourceDirectories([]string{environment.WorkspaceDirectory}) != nil || agentcapabilities.ValidateSourceDirectories(environment.CapabilityDirectories) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } default: - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } if err := decodeInputObject(raw, &environment, fields...); err != nil { return nil, err diff --git a/services/core/internal/api/environment_setup.go b/services/core/internal/api/environment_setup.go index ccd1b6ab6..1ec72d077 100644 --- a/services/core/internal/api/environment_setup.go +++ b/services/core/internal/api/environment_setup.go @@ -6,7 +6,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) var errSystemPackages = &fieldError{ @@ -32,12 +32,12 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (environmentconfi if value, ok := fields["env"]; ok { var entries map[string]*string if json.Unmarshal(value, &entries) != nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } result.Env = make(map[string]string, len(entries)) for name, entry := range entries { if entry == nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } result.Env[name] = *entry } @@ -45,7 +45,7 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (environmentconfi if value, ok := fields["setup_commands"]; ok { var commands []json.RawMessage if json.Unmarshal(value, &commands) != nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } for _, command := range commands { var input struct { @@ -53,12 +53,12 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (environmentconfi CWD *string `json:"cwd"` } if decodeInputObject(command, &input, "command", "cwd") != nil || input.Command == nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } step := environmentconfig.SetupCommand{Command: *input.Command} if input.CWD != nil { if *input.CWD == "" { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } step.CWD = *input.CWD } @@ -71,7 +71,7 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (environmentconfi Python []*string `json:"python"` } if decodeInputObject(value, &input, "npm", "python") != nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } for name, entries := range map[string][]*string{"npm": input.NPM, "python": input.Python} { var target *[]string @@ -83,7 +83,7 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (environmentconfi } for _, entry := range entries { if entry == nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } *target = append(*target, *entry) } @@ -101,11 +101,11 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (environmentconfi if raw, supplied := fields["capability_directories"]; supplied { var entries []*string if json.Unmarshal(raw, &entries) != nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } for _, entry := range entries { if entry == nil { - return result, store.ErrInvalidInput + return result, sessions.ErrInvalidInput } result.CapabilityDirectories = append(result.CapabilityDirectories, *entry) } diff --git a/services/core/internal/api/environment_skill_selectors_test.go b/services/core/internal/api/environment_skill_selectors_test.go index 0db09da74..84fd160cc 100644 --- a/services/core/internal/api/environment_skill_selectors_test.go +++ b/services/core/internal/api/environment_skill_selectors_test.go @@ -7,7 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestSkillReferenceNullableSelectorAdmissionAndTemplateProjection(t *testing.T) { @@ -75,7 +75,7 @@ func TestInstalledSkillReferenceRequiresConcreteVersion(t *testing.T) { } for _, selector := range []string{`"2"`, `null`, `"latest"`} { raw := json.RawMessage(`{"type":"openai_hosted","skills":[{"type":"skill_reference","skill_id":"skill-owned","version":` + selector + `,"name":"proof","description":"A proof."}]}`) - result, err := environmentResponse(store.Environment{ID: "environment-owned", Status: "pending", Configuration: raw}) + result, err := environmentResponse(sessions.Environment{ID: "environment-owned", Status: "pending", Configuration: raw}) if selector != `"2"` { if err == nil { t.Fatalf("unresolved installed selector accepted: %s", selector) diff --git a/services/core/internal/api/environment_skills.go b/services/core/internal/api/environment_skills.go index 102a2056e..9238b9d3d 100644 --- a/services/core/internal/api/environment_skills.go +++ b/services/core/internal/api/environment_skills.go @@ -5,7 +5,7 @@ import ( "encoding/json" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func decodeEnvironmentSkills(raw json.RawMessage) ([]environmentconfig.Skill, error) { @@ -14,7 +14,7 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]environmentconfig.Skill, er } var entries []json.RawMessage if json.Unmarshal(raw, &entries) != nil || len(entries) > 50 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } result := make([]environmentconfig.Skill, 0, len(entries)) for _, entry := range entries { @@ -22,7 +22,7 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]environmentconfig.Skill, er Type string `json:"type"` } if json.Unmarshal(entry, &discriminator) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } if discriminator.Type == "skill_reference" { var reference struct { @@ -31,12 +31,12 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]environmentconfig.Skill, er Version json.RawMessage `json:"version"` } if decodeInputObject(entry, &reference, "type", "skill_id", "version") != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } metadata := environmentconfig.SkillMetadata{Type: reference.Type, SkillID: reference.SkillID} if len(reference.Version) > 0 && !bytes.Equal(bytes.TrimSpace(reference.Version), []byte("null")) { if json.Unmarshal(reference.Version, &metadata.Version) != nil || metadata.Version == "" { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } } result = append(result, environmentconfig.Skill{Metadata: metadata}) @@ -49,7 +49,7 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]environmentconfig.Skill, er Source json.RawMessage `json:"source"` } if decodeInputObject(entry, &input, "type", "name", "description", "source") != nil || input.Type != "inline" { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } body, err := decodeCapabilityArchive(input.Source) if err != nil { @@ -82,13 +82,13 @@ func storedSkills(raw json.RawMessage) ([]json.RawMessage, error) { } var entries []json.RawMessage if json.Unmarshal(raw, &entries) != nil || len(entries) > 50 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } seen := map[string]bool{} for _, entry := range entries { var metadata environmentconfig.SkillMetadata if decodeInputObject(entry, &metadata, "type", "name", "description", "skill_id", "version") != nil || metadata.ValidateInstalled() != nil || seen[metadata.Name] { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } seen[metadata.Name] = true } diff --git a/services/core/internal/api/environments.go b/services/core/internal/api/environments.go index e75649e57..a44b7be0b 100644 --- a/services/core/internal/api/environments.go +++ b/services/core/internal/api/environments.go @@ -8,7 +8,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" ) @@ -17,12 +17,12 @@ import ( // Project's principal is an executor credential's execution principal; the // Core key that authorizes the request is not. type Environments interface { - GetEnvironment(context.Context, string, string) (store.Environment, error) + GetEnvironment(context.Context, string, string) (sessions.Environment, error) AuthorizeEnvironmentInstallation(context.Context, identity.Principal, string, string) (string, int64, error) - ValidateEnvironmentInstallation(context.Context, string, string) (store.InstallationAuthorization, error) + ValidateEnvironmentInstallation(context.Context, string, string) (sessions.InstallationAuthorization, error) ClaimEnvironmentInstallation(context.Context, string, string, string) error - ProjectExecutorCredentialState(context.Context, identity.Principal, string) (store.ExecutorCredentialState, error) - IssueProjectExecutorCredential(context.Context, identity.Principal, string, string, bool) (store.IssuedExecutorCredential, error) + ProjectExecutorCredentialState(context.Context, identity.Principal, string) (sessions.ExecutorCredentialState, error) + IssueProjectExecutorCredential(context.Context, identity.Principal, string, string, bool) (sessions.IssuedExecutorCredential, error) RevokeProjectExecutorCredential(context.Context, identity.Principal, string, string) error } @@ -50,7 +50,7 @@ func (h *Handler) getEnvironment(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, response) } -func environmentResponse(environment store.Environment) (v1.EnvironmentInfo, error) { +func environmentResponse(environment sessions.Environment) (v1.EnvironmentInfo, error) { configuration, err := storedEnvironment(environment.Configuration) if err != nil || (configuration.Type != "self_hosted" && configuration.Type != "openai_hosted") || environment.ID == "" { return v1.EnvironmentInfo{}, errors.New("unsupported stored environment metadata configuration") diff --git a/services/core/internal/api/environments_test.go b/services/core/internal/api/environments_test.go index 7ac92e3ee..3104677d5 100644 --- a/services/core/internal/api/environments_test.go +++ b/services/core/internal/api/environments_test.go @@ -11,18 +11,18 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) type environmentResourceFixture struct { - environment store.Environment + environment sessions.Environment err error tenant, id string calls int } -func (f *environmentResourceFixture) GetEnvironment(_ context.Context, tenant, id string) (store.Environment, error) { +func (f *environmentResourceFixture) GetEnvironment(_ context.Context, tenant, id string) (sessions.Environment, error) { f.tenant, f.id = tenant, id f.calls++ return f.environment, f.err @@ -30,7 +30,7 @@ func (f *environmentResourceFixture) GetEnvironment(_ context.Context, tenant, i func environmentResourceHandler(t *testing.T) (http.Handler, *environmentResourceFixture) { t.Helper() - f := &environmentResourceFixture{environment: store.Environment{ + f := &environmentResourceFixture{environment: sessions.Environment{ ID: uuid.NewString(), TenantID: uuid.NewString(), SessionID: uuid.NewString(), Status: "pending", Configuration: json.RawMessage(`{"type":"self_hosted","workspace_directory":"/private/workspace"}`), }} @@ -126,10 +126,10 @@ func TestEnvironmentResourceRequestAndStoreErrors(t *testing.T) { {"wrong beta", "GET", "", "Bearer resource-key", "agents=v2", nil, 400, 0}, // Unknown keys, including include, are not pinned retrieval parameters. {"ignored query", "GET", "?tenant_id=foreign&include=files", "Bearer resource-key", "agents=v1", nil, 200, 1}, - {"not found with query", "GET", "?include=files", "Bearer resource-key", "agents=v1", store.ErrNotFound, 404, 1}, + {"not found with query", "GET", "?include=files", "Bearer resource-key", "agents=v1", sessions.ErrNotFound, 404, 1}, {"method", "POST", "", "Bearer resource-key", "agents=v1", nil, 405, 0}, - {"not found", "GET", "", "Bearer resource-key", "agents=v1", store.ErrNotFound, 404, 1}, - {"invalid id", "GET", "", "Bearer resource-key", "agents=v1", store.ErrInvalidInput, 400, 1}, + {"not found", "GET", "", "Bearer resource-key", "agents=v1", sessions.ErrNotFound, 404, 1}, + {"invalid id", "GET", "", "Bearer resource-key", "agents=v1", sessions.ErrInvalidInput, 400, 1}, {"backend", "GET", "", "Bearer resource-key", "agents=v1", errors.New("private-backend-canary"), 500, 1}, } { t.Run(test.name, func(t *testing.T) { diff --git a/services/core/internal/api/errors.go b/services/core/internal/api/errors.go index 9e6170a16..15e2ac270 100644 --- a/services/core/internal/api/errors.go +++ b/services/core/internal/api/errors.go @@ -9,9 +9,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/textvalue" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" @@ -57,20 +54,6 @@ func writeAPIError(w http.ResponseWriter, status int, code, message string, deta writeJSON(w, status, v1.ErrorResponse{Error: v1.APIError{Message: message, Type: kind, Code: errorCode, Param: errorParam}}) } -// writeInputError reports Session input admission failures. Input that the -// Session cannot accept in its current state is the official conflict_error; -// Idempotency-Key reuse keeps Core's local idempotency_conflict code. -func writeInputError(w http.ResponseWriter, r *http.Request, err error) { - switch { - case errors.Is(err, store.ErrSessionInputPending): - writeError(w, http.StatusConflict, "conflict_error", "Earlier input to this Session is still pending.") - case errors.Is(err, store.ErrTurnConflict): - writeError(w, http.StatusConflict, "conflict_error", "The Turn cannot accept this input in its current state.") - default: - writeStoreError(w, r, err) - } -} - // writeContentTooLarge reports uploaded or copied content beyond the // operation's limit. func writeContentTooLarge(w http.ResponseWriter) { @@ -102,7 +85,9 @@ func writeFieldError(w http.ResponseWriter, err error) bool { return true } -func writeStoreError(w http.ResponseWriter, r *http.Request, err error, notFoundParam ...string) { +// writeStoreError reports a failure of an operation that can meet a sandbox +// reset or a deployment's sandbox resources, then the Session errors. +func writeStoreError(w http.ResponseWriter, r *http.Request, err error) { // A reset's resource check unwraps to deployment.ErrConflict and admission // paused by a reset is Session admission's, so both precede the deployment errors. var inUse *store.SandboxInUseError @@ -117,57 +102,7 @@ func writeStoreError(w http.ResponseWriter, r *http.Request, err error, notFound if writeSandboxError(w, err) { return } - var cursor *store.InvalidCursorError - switch { - case errors.Is(err, projects.ErrArchived): - // Executor credential management checks the Project in its own - // transaction. - writeProjectsError(w, r, err) - case errors.Is(err, store.ErrInstallationAuthorization): - writeError(w, http.StatusUnauthorized, "installation_authorization_invalid", store.ErrInstallationAuthorization.Error()) - case errors.Is(err, store.ErrExecutorCredentialExists): - writeError(w, http.StatusConflict, "executor_credential_exists", "This executor key ID already exists. Explicitly rotate it to replace the secret.") - case errors.Is(err, execution.ErrModelProviderRequired): - writeError(w, http.StatusBadRequest, "model_provider_required", "This Session was created without a model provider and cannot run. Create a new Session with x_agents_core.model_provider or an Agent that has one saved.") - case errors.Is(err, store.ErrHostedEnvironmentFailed): - // Observed official status, type, code, null param and message. - writeError(w, http.StatusConflict, "conflict_error", "the hosted environment failed to provision") - case errors.Is(err, store.ErrEnvironmentUnavailable): - writeError(w, http.StatusConflict, "environment_unavailable", "The environment is no longer available for new input.") - case errors.Is(err, execution.ErrEnvironmentInputExpired): - writeError(w, http.StatusConflict, "environment_input_expired", "The environment input deadline elapsed before admission.") - case errors.Is(err, execution.ErrEnvironmentInputCancelled): - writeError(w, http.StatusConflict, "environment_input_cancelled", "The environment input was cancelled before admission.") - case errors.Is(err, execution.ErrWhitespaceOnlyText): - writeError(w, http.StatusBadRequest, "unsupported_or_invalid_configuration", "This Session's harness does not accept a message whose text is only whitespace. Include non-whitespace text or an image, or use a harness that supports whitespace-only text.") - case errors.Is(err, execution.ErrExecutionUnavailable): - writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution is not available on this service.") - case errors.As(err, &cursor): - // Observed official fields for an unresolved Beta list cursor, with a null param. - writeError(w, http.StatusBadRequest, "invalid_request_error", cursor.Message) - case errors.Is(err, store.ErrNotFound): - writeError(w, http.StatusNotFound, "not_found_error", "Resource not found.", notFoundParam...) - case errors.Is(err, store.ErrSessionNotIdle): - // Observed official status, type, code, null param and message. - writeError(w, http.StatusConflict, "conflict_error", "session must be durably idle or failed without required actions before deletion") - case errors.Is(err, store.ErrUnknownFunctionCall): - writeError(w, http.StatusBadRequest, "invalid_request_error", "Unknown pending tool call.") - case errors.Is(err, store.ErrFunctionCallTurnMismatch): - writeError(w, http.StatusBadRequest, "invalid_request_error", "The tool call belongs to a different Turn.") - case errors.Is(err, store.ErrFunctionResultConflict): - writeError(w, http.StatusConflict, "conflict_error", "The tool call already has a different result.") - case errors.Is(err, store.ErrTurnConflict): - writeError(w, http.StatusConflict, "turn_conflict", "The Turn cannot accept this input in its current state.") - case errors.Is(err, store.ErrIdempotencyConflict): - writeError(w, http.StatusConflict, "idempotency_conflict", "This idempotency key was used with different input.") - case errors.Is(err, store.ErrInvalidInput), errors.Is(err, environmentconfig.ErrInvalid): - writeError(w, http.StatusBadRequest, "invalid_request", invalidInputMessage) - default: - if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) { - return - } - writeInternalError(w, r) - } + writeSessionsError(w, r, err) } // invalidInputMessage accompanies the 400 invalid_request for invalid diff --git a/services/core/internal/api/errors_sessions.go b/services/core/internal/api/errors_sessions.go new file mode 100644 index 000000000..4c45919ad --- /dev/null +++ b/services/core/internal/api/errors_sessions.go @@ -0,0 +1,82 @@ +package api + +import ( + "errors" + "net/http" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" +) + +// writeInputError reports Session input admission failures. Input that the +// Session cannot accept in its current state is the official conflict_error; +// Idempotency-Key reuse keeps Core's local idempotency_conflict code. +func writeInputError(w http.ResponseWriter, r *http.Request, err error) { + switch { + case errors.Is(err, sessions.ErrInputPending): + writeError(w, http.StatusConflict, "conflict_error", "Earlier input to this Session is still pending.") + case errors.Is(err, sessions.ErrTurnConflict): + writeError(w, http.StatusConflict, "conflict_error", "The Turn cannot accept this input in its current state.") + default: + writeStoreError(w, r, err) + } +} + +// writeSessionsError maps the errors of Session operations: the sessions +// domain's errors, the execution admission errors they meet, and the Project +// and Environment configuration checks they perform. +func writeSessionsError(w http.ResponseWriter, r *http.Request, err error) { + var cursor *sessions.CursorError + switch { + case errors.Is(err, projects.ErrArchived): + // Executor credential management checks the Project in its own + // transaction. + writeProjectsError(w, r, err) + case errors.Is(err, sessions.ErrInstallationAuthorization): + writeError(w, http.StatusUnauthorized, "installation_authorization_invalid", sessions.ErrInstallationAuthorization.Error()) + case errors.Is(err, sessions.ErrExecutorCredentialExists): + writeError(w, http.StatusConflict, "executor_credential_exists", "This executor key ID already exists. Explicitly rotate it to replace the secret.") + case errors.Is(err, execution.ErrModelProviderRequired): + writeError(w, http.StatusBadRequest, "model_provider_required", "This Session was created without a model provider and cannot run. Create a new Session with x_agents_core.model_provider or an Agent that has one saved.") + case errors.Is(err, sessions.ErrHostedEnvironmentFailed): + // Observed official status, type, code, null param and message. + writeError(w, http.StatusConflict, "conflict_error", "the hosted environment failed to provision") + case errors.Is(err, sessions.ErrEnvironmentUnavailable): + writeError(w, http.StatusConflict, "environment_unavailable", "The environment is no longer available for new input.") + case errors.Is(err, execution.ErrEnvironmentInputExpired): + writeError(w, http.StatusConflict, "environment_input_expired", "The environment input deadline elapsed before admission.") + case errors.Is(err, execution.ErrEnvironmentInputCancelled): + writeError(w, http.StatusConflict, "environment_input_cancelled", "The environment input was cancelled before admission.") + case errors.Is(err, execution.ErrWhitespaceOnlyText): + writeError(w, http.StatusBadRequest, "unsupported_or_invalid_configuration", "This Session's harness does not accept a message whose text is only whitespace. Include non-whitespace text or an image, or use a harness that supports whitespace-only text.") + case errors.Is(err, execution.ErrExecutionUnavailable): + writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution is not available on this service.") + case errors.As(err, &cursor): + // Observed official fields for an unresolved Beta list cursor, with a null param. + writeError(w, http.StatusBadRequest, "invalid_request_error", cursor.Message) + case errors.Is(err, sessions.ErrNotFound): + writeError(w, http.StatusNotFound, "not_found_error", "Resource not found.") + case errors.Is(err, sessions.ErrNotIdle): + // Observed official status, type, code, null param and message. + writeError(w, http.StatusConflict, "conflict_error", "session must be durably idle or failed without required actions before deletion") + case errors.Is(err, sessions.ErrUnknownFunctionCall): + writeError(w, http.StatusBadRequest, "invalid_request_error", "Unknown pending tool call.") + case errors.Is(err, sessions.ErrFunctionCallTurnMismatch): + writeError(w, http.StatusBadRequest, "invalid_request_error", "The tool call belongs to a different Turn.") + case errors.Is(err, sessions.ErrFunctionResultConflict): + writeError(w, http.StatusConflict, "conflict_error", "The tool call already has a different result.") + case errors.Is(err, sessions.ErrTurnConflict): + writeError(w, http.StatusConflict, "turn_conflict", "The Turn cannot accept this input in its current state.") + case errors.Is(err, sessions.ErrIdempotencyConflict): + writeError(w, http.StatusConflict, "idempotency_conflict", "This idempotency key was used with different input.") + case errors.Is(err, sessions.ErrInvalidInput), errors.Is(err, environmentconfig.ErrInvalid): + writeError(w, http.StatusBadRequest, "invalid_request", invalidInputMessage) + default: + if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) { + return + } + writeInternalError(w, r) + } +} diff --git a/services/core/internal/api/errors_test.go b/services/core/internal/api/errors_test.go index 08a68ba2a..6d88ecab7 100644 --- a/services/core/internal/api/errors_test.go +++ b/services/core/internal/api/errors_test.go @@ -15,7 +15,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/textvalue" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" ) @@ -32,7 +32,7 @@ func TestResourceNotFoundErrorSurfaces(t *testing.T) { if strings.HasPrefix(path, "/v1/files") { writeFilesError(response, request, fmt.Errorf("lookup: %w", files.ErrNotFound)) } else { - writeStoreError(response, request, fmt.Errorf("lookup: %w", store.ErrNotFound)) + writeSessionsError(response, request, fmt.Errorf("lookup: %w", sessions.ErrNotFound)) } var body v1.ErrorResponse if response.Code != http.StatusNotFound || json.Unmarshal(response.Body.Bytes(), &body) != nil { @@ -53,6 +53,9 @@ func TestResourceNotFoundErrorSurfaces(t *testing.T) { if _, present := envelope["code"]; !present { t.Fatal("nullable error code must remain present") } + if param, present := envelope["param"]; !present || string(param) != "null" { + t.Fatalf("param = %s", param) + } }) } } @@ -67,7 +70,7 @@ func TestInvalidCursorErrorFields(t *testing.T) { } { response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, path, nil) - writeStoreError(response, request, fmt.Errorf("list: %w", &store.InvalidCursorError{Message: "Invalid session item ID in `after`"})) + writeSessionsError(response, request, fmt.Errorf("list: %w", &sessions.CursorError{Message: "Invalid session item ID in `after`"})) if response.Code != http.StatusBadRequest || response.Body.String() != want+"\n" { t.Errorf("%s: %d %s", path, response.Code, response.Body) } @@ -121,7 +124,7 @@ func equalOptional(got, want *string) bool { func TestSessionDeletionConflictError(t *testing.T) { response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodDelete, "/v1/agents/sessions/session", nil) - writeStoreError(response, request, fmt.Errorf("delete: %w", store.ErrSessionNotIdle)) + writeSessionsError(response, request, fmt.Errorf("delete: %w", sessions.ErrNotIdle)) want := `{"error":{"message":"session must be durably idle or failed without required actions before deletion","type":"conflict_error","code":"conflict_error","param":null}}` + "\n" if response.Code != http.StatusConflict || response.Body.String() != want { t.Fatalf("response = %d %s", response.Code, response.Body) @@ -137,14 +140,14 @@ func TestConflictErrorsUseConflictType(t *testing.T) { deployment.ErrNodeInUse: "runtime_node_in_use", deployment.ErrLocalNodeConfigured: "runtime_local_node_configured", deployment.ErrNodeAddressMismatch: "sandbox_node_address_mismatch", - store.ErrEnvironmentUnavailable: "environment_unavailable", + sessions.ErrEnvironmentUnavailable: "environment_unavailable", execution.ErrEnvironmentInputExpired: "environment_input_expired", execution.ErrEnvironmentInputCancelled: "environment_input_cancelled", - store.ErrSessionNotIdle: "conflict_error", - store.ErrFunctionResultConflict: "conflict_error", - store.ErrIdempotencyConflict: "idempotency_conflict", - store.ErrTurnConflict: "turn_conflict", - store.ErrSessionInputPending: "turn_conflict", + sessions.ErrNotIdle: "conflict_error", + sessions.ErrFunctionResultConflict: "conflict_error", + sessions.ErrIdempotencyConflict: "idempotency_conflict", + sessions.ErrTurnConflict: "turn_conflict", + sessions.ErrInputPending: "turn_conflict", } { t.Run(code, func(t *testing.T) { response := httptest.NewRecorder() @@ -173,7 +176,7 @@ func TestSharedPersistenceErrors(t *testing.T) { write(response, httptest.NewRequest(http.MethodPost, "/v1/agents", nil), err) return response } - invalid := respond(storeError, store.ErrInvalidInput).Body.String() + invalid := respond(storeError, sessions.ErrInvalidInput).Body.String() for _, test := range []struct { write func(http.ResponseWriter, *http.Request, error) err error diff --git a/services/core/internal/api/fakes_test.go b/services/core/internal/api/fakes_test.go index d10ffce34..7379141f5 100644 --- a/services/core/internal/api/fakes_test.go +++ b/services/core/internal/api/fakes_test.go @@ -43,11 +43,11 @@ func unexpectedCall(t testing.TB, method string) { type fakeAdmin struct { t testing.TB - readAdminSummary func(context.Context, string, store.AdminSummaryFilter, func(store.Session, *string) error) (store.AdminAssetCounts, error) + readAdminSummary func(context.Context, string, store.AdminSummaryFilter, func(sessions.Session, *string) error) (store.AdminAssetCounts, error) listAdminRuntimeTargets func(context.Context, []string, string, int, bool) (store.AdminRuntimeTargetPage, error) } -func (f *fakeAdmin) ReadAdminSummary(a0 context.Context, a1 string, a2 store.AdminSummaryFilter, a3 func(store.Session, *string) error) (store.AdminAssetCounts, error) { +func (f *fakeAdmin) ReadAdminSummary(a0 context.Context, a1 string, a2 store.AdminSummaryFilter, a3 func(sessions.Session, *string) error) (store.AdminAssetCounts, error) { if f.readAdminSummary == nil { unexpectedCall(f.t, "ReadAdminSummary") } @@ -73,34 +73,6 @@ func (f *fakeAdminAudit) ListAdminAudit(a0 context.Context, a1 adminaudit.Filter return f.listAdminAudit(a0, a1) } -type fakeAdmission struct { - t testing.TB - createSession func(context.Context, string, store.CreateSessionInput) (store.Session, error) - createSessionStream func(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) - submitInputs func(context.Context, string, string, string, []store.Input) ([]store.InputReceipt, error) -} - -func (f *fakeAdmission) CreateSession(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.Session, error) { - if f.createSession == nil { - unexpectedCall(f.t, "CreateSession") - } - return f.createSession(a0, a1, a2) -} - -func (f *fakeAdmission) CreateSessionStream(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.SessionCreation, error) { - if f.createSessionStream == nil { - unexpectedCall(f.t, "CreateSessionStream") - } - return f.createSessionStream(a0, a1, a2) -} - -func (f *fakeAdmission) SubmitInputs(a0 context.Context, a1 string, a2 string, a3 string, a4 []store.Input) ([]store.InputReceipt, error) { - if f.submitInputs == nil { - unexpectedCall(f.t, "SubmitInputs") - } - return f.submitInputs(a0, a1, a2, a3, a4) -} - type fakeAgents struct { t testing.TB create func(context.Context, agents.CreateCommand) (agents.Agent, error) @@ -159,27 +131,27 @@ func (f *fakeAgentsReader) GetAgentWithModelProvider(a0 context.Context, a1 stri type fakeArtifacts struct { t testing.TB - getSessionArtifact func(context.Context, string, string, string) (store.SessionArtifact, error) - listSessionArtifacts func(context.Context, string, string, string, string, int, bool) (store.ArtifactPage, error) - readSessionArtifact func(context.Context, string, string, string, func(store.SessionArtifact, io.Reader) error) error + getSessionArtifact func(context.Context, string, string, string) (sessions.Artifact, error) + listSessionArtifacts func(context.Context, string, string, string, string, int, bool) (sessions.ArtifactPage, error) + readSessionArtifact func(context.Context, string, string, string, func(sessions.Artifact, io.Reader) error) error deleteSessionArtifact func(context.Context, string, string, string) error } -func (f *fakeArtifacts) GetSessionArtifact(a0 context.Context, a1 string, a2 string, a3 string) (store.SessionArtifact, error) { +func (f *fakeArtifacts) GetSessionArtifact(a0 context.Context, a1 string, a2 string, a3 string) (sessions.Artifact, error) { if f.getSessionArtifact == nil { unexpectedCall(f.t, "GetSessionArtifact") } return f.getSessionArtifact(a0, a1, a2, a3) } -func (f *fakeArtifacts) ListSessionArtifacts(a0 context.Context, a1 string, a2 string, a3 string, a4 string, a5 int, a6 bool) (store.ArtifactPage, error) { +func (f *fakeArtifacts) ListSessionArtifacts(a0 context.Context, a1 string, a2 string, a3 string, a4 string, a5 int, a6 bool) (sessions.ArtifactPage, error) { if f.listSessionArtifacts == nil { unexpectedCall(f.t, "ListSessionArtifacts") } return f.listSessionArtifacts(a0, a1, a2, a3, a4, a5, a6) } -func (f *fakeArtifacts) ReadSessionArtifact(a0 context.Context, a1 string, a2 string, a3 string, a4 func(store.SessionArtifact, io.Reader) error) error { +func (f *fakeArtifacts) ReadSessionArtifact(a0 context.Context, a1 string, a2 string, a3 string, a4 func(sessions.Artifact, io.Reader) error) error { if f.readSessionArtifact == nil { unexpectedCall(f.t, "ReadSessionArtifact") } @@ -399,18 +371,18 @@ func (f *fakeEnvironmentTemplatesReader) Resolve(a0 context.Context, a1 string, type fakeEnvironmentWorkspaces struct { t testing.TB - readEnvironmentDirectory func(context.Context, store.Environment, string) (proto.WorkspaceDirectoryResult, error) - writeEnvironmentFile func(context.Context, store.Environment, string, []byte) (int64, error) + readEnvironmentDirectory func(context.Context, sessions.Environment, string) (proto.WorkspaceDirectoryResult, error) + writeEnvironmentFile func(context.Context, sessions.Environment, string, []byte) (int64, error) } -func (f *fakeEnvironmentWorkspaces) ReadEnvironmentDirectory(a0 context.Context, a1 store.Environment, a2 string) (proto.WorkspaceDirectoryResult, error) { +func (f *fakeEnvironmentWorkspaces) ReadEnvironmentDirectory(a0 context.Context, a1 sessions.Environment, a2 string) (proto.WorkspaceDirectoryResult, error) { if f.readEnvironmentDirectory == nil { unexpectedCall(f.t, "ReadEnvironmentDirectory") } return f.readEnvironmentDirectory(a0, a1, a2) } -func (f *fakeEnvironmentWorkspaces) WriteEnvironmentFile(a0 context.Context, a1 store.Environment, a2 string, a3 []byte) (int64, error) { +func (f *fakeEnvironmentWorkspaces) WriteEnvironmentFile(a0 context.Context, a1 sessions.Environment, a2 string, a3 []byte) (int64, error) { if f.writeEnvironmentFile == nil { unexpectedCall(f.t, "WriteEnvironmentFile") } @@ -419,16 +391,16 @@ func (f *fakeEnvironmentWorkspaces) WriteEnvironmentFile(a0 context.Context, a1 type fakeEnvironments struct { t testing.TB - getEnvironment func(context.Context, string, string) (store.Environment, error) + getEnvironment func(context.Context, string, string) (sessions.Environment, error) authorizeEnvironmentInstallation func(context.Context, identity.Principal, string, string) (string, int64, error) - validateEnvironmentInstallation func(context.Context, string, string) (store.InstallationAuthorization, error) + validateEnvironmentInstallation func(context.Context, string, string) (sessions.InstallationAuthorization, error) claimEnvironmentInstallation func(context.Context, string, string, string) error - projectExecutorCredentialState func(context.Context, identity.Principal, string) (store.ExecutorCredentialState, error) - issueProjectExecutorCredential func(context.Context, identity.Principal, string, string, bool) (store.IssuedExecutorCredential, error) + projectExecutorCredentialState func(context.Context, identity.Principal, string) (sessions.ExecutorCredentialState, error) + issueProjectExecutorCredential func(context.Context, identity.Principal, string, string, bool) (sessions.IssuedExecutorCredential, error) revokeProjectExecutorCredential func(context.Context, identity.Principal, string, string) error } -func (f *fakeEnvironments) GetEnvironment(a0 context.Context, a1 string, a2 string) (store.Environment, error) { +func (f *fakeEnvironments) GetEnvironment(a0 context.Context, a1 string, a2 string) (sessions.Environment, error) { if f.getEnvironment == nil { unexpectedCall(f.t, "GetEnvironment") } @@ -442,7 +414,7 @@ func (f *fakeEnvironments) AuthorizeEnvironmentInstallation(a0 context.Context, return f.authorizeEnvironmentInstallation(a0, a1, a2, a3) } -func (f *fakeEnvironments) ValidateEnvironmentInstallation(a0 context.Context, a1 string, a2 string) (store.InstallationAuthorization, error) { +func (f *fakeEnvironments) ValidateEnvironmentInstallation(a0 context.Context, a1 string, a2 string) (sessions.InstallationAuthorization, error) { if f.validateEnvironmentInstallation == nil { unexpectedCall(f.t, "ValidateEnvironmentInstallation") } @@ -456,14 +428,14 @@ func (f *fakeEnvironments) ClaimEnvironmentInstallation(a0 context.Context, a1 s return f.claimEnvironmentInstallation(a0, a1, a2, a3) } -func (f *fakeEnvironments) ProjectExecutorCredentialState(a0 context.Context, a1 identity.Principal, a2 string) (store.ExecutorCredentialState, error) { +func (f *fakeEnvironments) ProjectExecutorCredentialState(a0 context.Context, a1 identity.Principal, a2 string) (sessions.ExecutorCredentialState, error) { if f.projectExecutorCredentialState == nil { unexpectedCall(f.t, "ProjectExecutorCredentialState") } return f.projectExecutorCredentialState(a0, a1, a2) } -func (f *fakeEnvironments) IssueProjectExecutorCredential(a0 context.Context, a1 identity.Principal, a2 string, a3 string, a4 bool) (store.IssuedExecutorCredential, error) { +func (f *fakeEnvironments) IssueProjectExecutorCredential(a0 context.Context, a1 identity.Principal, a2 string, a3 string, a4 bool) (sessions.IssuedExecutorCredential, error) { if f.issueProjectExecutorCredential == nil { unexpectedCall(f.t, "IssueProjectExecutorCredential") } @@ -537,6 +509,18 @@ func (f *fakeFiles) Delete(a0 context.Context, a1 files.DeleteCommand) error { return f.delete(a0, a1) } +type fakeInputAdmission struct { + t testing.TB + submitInputs func(context.Context, string, string, string, []sessions.Input) ([]sessions.InputReceipt, error) +} + +func (f *fakeInputAdmission) SubmitInputs(a0 context.Context, a1 string, a2 string, a3 string, a4 []sessions.Input) ([]sessions.InputReceipt, error) { + if f.submitInputs == nil { + unexpectedCall(f.t, "SubmitInputs") + } + return f.submitInputs(a0, a1, a2, a3, a4) +} + type fakeInstallationBindings struct { t testing.TB addressBindings func(context.Context) (store.AddressBindings, error) @@ -549,6 +533,18 @@ func (f *fakeInstallationBindings) AddressBindings(a0 context.Context) (store.Ad return f.addressBindings(a0) } +type fakeItems struct { + t testing.TB + listItems func(context.Context, string, string, string, int, bool) (sessions.ItemPage, error) +} + +func (f *fakeItems) ListItems(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (sessions.ItemPage, error) { + if f.listItems == nil { + unexpectedCall(f.t, "ListItems") + } + return f.listItems(a0, a1, a2, a3, a4, a5) +} + type fakeMetrics struct { t testing.TB read func(context.Context, string) (coremetrics.View, error) @@ -731,20 +727,20 @@ func (f *fakeRuntimeObservations) ObserveSessions(a0 context.Context, a1 []runti type fakeSessionAdmin struct { t testing.TB - getSessionDiagnosticsSnapshot func(context.Context, string, string) (store.Session, error) - getTurnDiagnosticsSnapshot func(context.Context, string, string, string) (store.TurnDiagnosticsSnapshot, error) + getSessionDiagnosticsSnapshot func(context.Context, string, string) (sessions.Session, error) + getTurnDiagnosticsSnapshot func(context.Context, string, string, string) (sessions.TurnDiagnosticsSnapshot, error) getSessionExecutionConfiguration func(context.Context, string, string) (v1.SessionExecutionConfiguration, error) - getManagedSessionArchive func(context.Context, string, string) (store.ManagedSessionArchive, error) + getManagedSessionArchive func(context.Context, string, string) (sessions.ManagedArchive, error) } -func (f *fakeSessionAdmin) GetSessionDiagnosticsSnapshot(a0 context.Context, a1 string, a2 string) (store.Session, error) { +func (f *fakeSessionAdmin) GetSessionDiagnosticsSnapshot(a0 context.Context, a1 string, a2 string) (sessions.Session, error) { if f.getSessionDiagnosticsSnapshot == nil { unexpectedCall(f.t, "GetSessionDiagnosticsSnapshot") } return f.getSessionDiagnosticsSnapshot(a0, a1, a2) } -func (f *fakeSessionAdmin) GetTurnDiagnosticsSnapshot(a0 context.Context, a1 string, a2 string, a3 string) (store.TurnDiagnosticsSnapshot, error) { +func (f *fakeSessionAdmin) GetTurnDiagnosticsSnapshot(a0 context.Context, a1 string, a2 string, a3 string) (sessions.TurnDiagnosticsSnapshot, error) { if f.getTurnDiagnosticsSnapshot == nil { unexpectedCall(f.t, "GetTurnDiagnosticsSnapshot") } @@ -758,30 +754,78 @@ func (f *fakeSessionAdmin) GetSessionExecutionConfiguration(a0 context.Context, return f.getSessionExecutionConfiguration(a0, a1, a2) } -func (f *fakeSessionAdmin) GetManagedSessionArchive(a0 context.Context, a1 string, a2 string) (store.ManagedSessionArchive, error) { +func (f *fakeSessionAdmin) GetManagedSessionArchive(a0 context.Context, a1 string, a2 string) (sessions.ManagedArchive, error) { if f.getManagedSessionArchive == nil { unexpectedCall(f.t, "GetManagedSessionArchive") } return f.getManagedSessionArchive(a0, a1, a2) } +type fakeSessionAdmission struct { + t testing.TB + createSession func(context.Context, string, sessions.CreateSession) (sessions.Session, error) + createSessionStream func(context.Context, string, sessions.CreateSession) (sessions.Creation, error) +} + +func (f *fakeSessionAdmission) CreateSession(a0 context.Context, a1 string, a2 sessions.CreateSession) (sessions.Session, error) { + if f.createSession == nil { + unexpectedCall(f.t, "CreateSession") + } + return f.createSession(a0, a1, a2) +} + +func (f *fakeSessionAdmission) CreateSessionStream(a0 context.Context, a1 string, a2 sessions.CreateSession) (sessions.Creation, error) { + if f.createSessionStream == nil { + unexpectedCall(f.t, "CreateSessionStream") + } + return f.createSessionStream(a0, a1, a2) +} + type fakeSessionArchive struct { t testing.TB - archiveManagedSession func(context.Context, string, string, uint64) (store.ManagedSessionArchive, error) + archiveManagedSession func(context.Context, string, string, uint64) (sessions.ManagedArchive, error) } -func (f *fakeSessionArchive) ArchiveManagedSession(a0 context.Context, a1 string, a2 string, a3 uint64) (store.ManagedSessionArchive, error) { +func (f *fakeSessionArchive) ArchiveManagedSession(a0 context.Context, a1 string, a2 string, a3 uint64) (sessions.ManagedArchive, error) { if f.archiveManagedSession == nil { unexpectedCall(f.t, "ArchiveManagedSession") } return f.archiveManagedSession(a0, a1, a2, a3) } +type fakeSessionCreation struct { + t testing.TB + createSession func(context.Context, string, sessions.CreateSession) (sessions.Session, error) + createSessionStream func(context.Context, string, sessions.CreateSession) (sessions.Creation, error) + findSessionCreation func(context.Context, string, string, json.RawMessage, identity.Subject) (sessions.Creation, error) +} + +func (f *fakeSessionCreation) CreateSession(a0 context.Context, a1 string, a2 sessions.CreateSession) (sessions.Session, error) { + if f.createSession == nil { + unexpectedCall(f.t, "CreateSession") + } + return f.createSession(a0, a1, a2) +} + +func (f *fakeSessionCreation) CreateSessionStream(a0 context.Context, a1 string, a2 sessions.CreateSession) (sessions.Creation, error) { + if f.createSessionStream == nil { + unexpectedCall(f.t, "CreateSessionStream") + } + return f.createSessionStream(a0, a1, a2) +} + +func (f *fakeSessionCreation) FindSessionCreation(a0 context.Context, a1 string, a2 string, a3 json.RawMessage, a4 identity.Subject) (sessions.Creation, error) { + if f.findSessionCreation == nil { + unexpectedCall(f.t, "FindSessionCreation") + } + return f.findSessionCreation(a0, a1, a2, a3, a4) +} + type fakeSessionEvents struct { t testing.TB sessionEventCursor func(context.Context, string, string) (int64, error) listSessionEvents func(context.Context, string, string, int64) ([]sessions.SessionChange, error) - sessionStreamSnapshot func(context.Context, string, string) (store.Session, int64, error) + sessionStreamSnapshot func(context.Context, string, string) (sessions.Session, int64, error) } func (f *fakeSessionEvents) SessionEventCursor(a0 context.Context, a1 string, a2 string) (int64, error) { @@ -798,89 +842,37 @@ func (f *fakeSessionEvents) ListSessionEvents(a0 context.Context, a1 string, a2 return f.listSessionEvents(a0, a1, a2, a3) } -func (f *fakeSessionEvents) SessionStreamSnapshot(a0 context.Context, a1 string, a2 string) (store.Session, int64, error) { +func (f *fakeSessionEvents) SessionStreamSnapshot(a0 context.Context, a1 string, a2 string) (sessions.Session, int64, error) { if f.sessionStreamSnapshot == nil { unexpectedCall(f.t, "SessionStreamSnapshot") } return f.sessionStreamSnapshot(a0, a1, a2) } -type fakeSessionHistory struct { - t testing.TB - getTurn func(context.Context, string, string, string) (sessions.Turn, error) - listTurns func(context.Context, string, string, string, int, bool) (store.TurnPage, error) - listItems func(context.Context, string, string, string, int, bool) (store.ItemPage, error) -} - -func (f *fakeSessionHistory) GetTurn(a0 context.Context, a1 string, a2 string, a3 string) (sessions.Turn, error) { - if f.getTurn == nil { - unexpectedCall(f.t, "GetTurn") - } - return f.getTurn(a0, a1, a2, a3) -} - -func (f *fakeSessionHistory) ListTurns(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (store.TurnPage, error) { - if f.listTurns == nil { - unexpectedCall(f.t, "ListTurns") - } - return f.listTurns(a0, a1, a2, a3, a4, a5) -} - -func (f *fakeSessionHistory) ListItems(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (store.ItemPage, error) { - if f.listItems == nil { - unexpectedCall(f.t, "ListItems") - } - return f.listItems(a0, a1, a2, a3, a4, a5) -} - type fakeSessions struct { t testing.TB - createSession func(context.Context, string, store.CreateSessionInput) (store.Session, error) - createSessionStream func(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) - findSessionCreation func(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) - getSession func(context.Context, string, string) (store.Session, error) - listSessions func(context.Context, string, string, int, bool, *string) (store.SessionPage, error) - updateSessionMetadata func(context.Context, string, string, map[string]string) (store.Session, error) + getSession func(context.Context, string, string) (sessions.Session, error) + listSessions func(context.Context, string, string, int, bool, *string) (sessions.Page, error) + updateSessionMetadata func(context.Context, string, string, map[string]string) (sessions.Session, error) deleteSession func(context.Context, string, string) error auditSessionOperation func(context.Context, string, string, string) error } -func (f *fakeSessions) CreateSession(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.Session, error) { - if f.createSession == nil { - unexpectedCall(f.t, "CreateSession") - } - return f.createSession(a0, a1, a2) -} - -func (f *fakeSessions) CreateSessionStream(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.SessionCreation, error) { - if f.createSessionStream == nil { - unexpectedCall(f.t, "CreateSessionStream") - } - return f.createSessionStream(a0, a1, a2) -} - -func (f *fakeSessions) FindSessionCreation(a0 context.Context, a1 string, a2 string, a3 json.RawMessage, a4 identity.Subject) (store.SessionCreation, error) { - if f.findSessionCreation == nil { - unexpectedCall(f.t, "FindSessionCreation") - } - return f.findSessionCreation(a0, a1, a2, a3, a4) -} - -func (f *fakeSessions) GetSession(a0 context.Context, a1 string, a2 string) (store.Session, error) { +func (f *fakeSessions) GetSession(a0 context.Context, a1 string, a2 string) (sessions.Session, error) { if f.getSession == nil { unexpectedCall(f.t, "GetSession") } return f.getSession(a0, a1, a2) } -func (f *fakeSessions) ListSessions(a0 context.Context, a1 string, a2 string, a3 int, a4 bool, a5 *string) (store.SessionPage, error) { +func (f *fakeSessions) ListSessions(a0 context.Context, a1 string, a2 string, a3 int, a4 bool, a5 *string) (sessions.Page, error) { if f.listSessions == nil { unexpectedCall(f.t, "ListSessions") } return f.listSessions(a0, a1, a2, a3, a4, a5) } -func (f *fakeSessions) UpdateSessionMetadata(a0 context.Context, a1 string, a2 string, a3 map[string]string) (store.Session, error) { +func (f *fakeSessions) UpdateSessionMetadata(a0 context.Context, a1 string, a2 string, a3 map[string]string) (sessions.Session, error) { if f.updateSessionMetadata == nil { unexpectedCall(f.t, "UpdateSessionMetadata") } @@ -1049,6 +1041,26 @@ func (f *fakeSubagents) ListSubagentTurnItems(a0 context.Context, a1 string, a2 return f.listSubagentTurnItems(a0, a1, a2, a3, a4, a5, a6, a7) } +type fakeTurns struct { + t testing.TB + getTurn func(context.Context, string, string, string) (sessions.Turn, error) + listTurns func(context.Context, string, string, string, int, bool) (sessions.TurnPage, error) +} + +func (f *fakeTurns) GetTurn(a0 context.Context, a1 string, a2 string, a3 string) (sessions.Turn, error) { + if f.getTurn == nil { + unexpectedCall(f.t, "GetTurn") + } + return f.getTurn(a0, a1, a2, a3) +} + +func (f *fakeTurns) ListTurns(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (sessions.TurnPage, error) { + if f.listTurns == nil { + unexpectedCall(f.t, "ListTurns") + } + return f.listTurns(a0, a1, a2, a3, a4, a5) +} + type fakeVaults struct { t testing.TB createVault func(context.Context, vaults.CreateVault) (vaults.Vault, error) diff --git a/services/core/internal/api/function_inputs.go b/services/core/internal/api/function_inputs.go index aabe1ee9d..ab62b47e2 100644 --- a/services/core/internal/api/function_inputs.go +++ b/services/core/internal/api/function_inputs.go @@ -7,7 +7,7 @@ import ( "slices" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type decodedInputEvent struct { @@ -18,7 +18,7 @@ type decodedInputEvent struct { func decodeInputEvent(raw json.RawMessage) (decodedInputEvent, error) { var event decodedInputEvent if err := json.Unmarshal(raw, &event); err != nil { - return event, store.ErrInvalidInput + return event, sessions.ErrInvalidInput } fields := []string{"type"} switch event.Type { @@ -31,13 +31,13 @@ func decodeInputEvent(raw json.RawMessage) (decodedInputEvent, error) { } `json:"input"` } if json.Unmarshal(raw, &messages) != nil { - return event, store.ErrInvalidInput + return event, sessions.ErrInvalidInput } for _, message := range messages.Input { if len(message.Type) > 0 { var kind string if json.Unmarshal(message.Type, &kind) != nil || kind != "message" { - return event, store.ErrInvalidInput + return event, sessions.ErrInvalidInput } } if err := validateInputContent(message.Content); err != nil { @@ -48,7 +48,7 @@ func decodeInputEvent(raw json.RawMessage) (decodedInputEvent, error) { case "agent.session.input.tool_result": fields = append(fields, "call_id", "turn_id", "success", "error", "output") default: - return event, store.ErrInvalidInput + return event, sessions.ErrInvalidInput } return event, decodeInputObject(raw, &event, fields...) } @@ -56,38 +56,38 @@ func decodeInputEvent(raw json.RawMessage) (decodedInputEvent, error) { func decodeInputObject(raw json.RawMessage, value any, allowed ...string) error { var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil || fields == nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } for field := range fields { if !slices.Contains(allowed, field) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } } // Nested members match exactly too; see inexactMember. The raw value is // valid JSON here, as Unmarshal accepted it. if inexactMember(raw, reflect.TypeOf(value)) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if decoder.Decode(value) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } return nil } -func functionResultInput(event decodedInputEvent) (store.Input, error) { +func functionResultInput(event decodedInputEvent) (sessions.Input, error) { if event.CallID == "" || event.TurnID == "" || event.Success == nil { - return store.Input{}, store.ErrInvalidInput + return sessions.Input{}, sessions.ErrInvalidInput } if len(event.Error) > 0 && !bytes.Equal(bytes.TrimSpace(event.Error), []byte("null")) { var message string if json.Unmarshal(event.Error, &message) != nil { - return store.Input{}, store.ErrInvalidInput + return sessions.Input{}, sessions.ErrInvalidInput } } if err := validateFunctionOutput(event.Output); err != nil { - return store.Input{}, err + return sessions.Input{}, err } result, err := json.Marshal(struct { Success bool `json:"success"` @@ -95,10 +95,10 @@ func functionResultInput(event decodedInputEvent) (store.Input, error) { Output json.RawMessage `json:"output,omitempty"` }{*event.Success, event.Error, event.Output}) if err != nil { - return store.Input{}, err + return sessions.Input{}, err } - payload, err := json.Marshal(store.FunctionResultInput{TurnID: event.TurnID, CallID: event.CallID, Result: result}) - return store.Input{Kind: "tool_result", Payload: payload}, err + payload, err := json.Marshal(sessions.FunctionResultInput{TurnID: event.TurnID, CallID: event.CallID, Result: result}) + return sessions.Input{Kind: "tool_result", Payload: payload}, err } func validateFunctionOutput(raw json.RawMessage) error { @@ -115,7 +115,7 @@ func validateFunctionOutput(raw json.RawMessage) error { func validateInputContent(raw json.RawMessage) error { var parts []json.RawMessage if json.Unmarshal(raw, &parts) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } for _, part := range parts { var value struct { @@ -124,21 +124,21 @@ func validateInputContent(raw json.RawMessage) error { ImageURL *string `json:"image_url"` } if json.Unmarshal(part, &value) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } field := "text" switch value.Type { case "input_text": if value.Text == nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } case "input_image": field = "image_url" if value.ImageURL == nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } default: - return store.ErrInvalidInput + return sessions.ErrInvalidInput } if err := decodeInputObject(part, &value, "type", field); err != nil { return err diff --git a/services/core/internal/api/function_inputs_test.go b/services/core/internal/api/function_inputs_test.go index 592edef4d..1f5bb7b5f 100644 --- a/services/core/internal/api/function_inputs_test.go +++ b/services/core/internal/api/function_inputs_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func submitResultRequest(t *testing.T, body string, failure error) (*httptest.ResponseRecorder, *inputRecorder) { @@ -36,7 +36,7 @@ func TestPublicFunctionResultsPreserveOptionalValues(t *testing.T) { if w.Code != 202 || w.Body.Len() != 0 || len(recorder.inputs) != 1 { t.Fatal(w.Code, w.Body, recorder.inputs) } - var input store.FunctionResultInput + var input sessions.FunctionResultInput if json.Unmarshal(recorder.inputs[0].Payload, &input) != nil || input.TurnID != "turn" || input.CallID != "call" { t.Fatal(input) } @@ -87,19 +87,19 @@ func TestPublicInputAdmissionErrorFields(t *testing.T) { status int want string }{ - {"cancelled_turn", fmt.Errorf("submit turn inputs: %w", store.ErrTurnConflict), 409, + {"cancelled_turn", fmt.Errorf("submit turn inputs: %w", sessions.ErrTurnConflict), 409, `{"error":{"message":"The Turn cannot accept this input in its current state.","type":"conflict_error","code":"conflict_error","param":null}}`}, - {"pending_input", fmt.Errorf("submit turn inputs: %w", store.ErrSessionInputPending), 409, + {"pending_input", fmt.Errorf("submit turn inputs: %w", sessions.ErrInputPending), 409, `{"error":{"message":"Earlier input to this Session is still pending.","type":"conflict_error","code":"conflict_error","param":null}}`}, - {"changed_result", fmt.Errorf("submit turn inputs: %w", store.ErrFunctionResultConflict), 409, + {"changed_result", fmt.Errorf("submit turn inputs: %w", sessions.ErrFunctionResultConflict), 409, `{"error":{"message":"The tool call already has a different result.","type":"conflict_error","code":"conflict_error","param":null}}`}, - {"unknown_call", fmt.Errorf("submit turn inputs: %w", store.ErrUnknownFunctionCall), 400, + {"unknown_call", fmt.Errorf("submit turn inputs: %w", sessions.ErrUnknownFunctionCall), 400, `{"error":{"message":"Unknown pending tool call.","type":"invalid_request_error","code":"invalid_request_error","param":null}}`}, - {"other_turn", fmt.Errorf("submit turn inputs: %w", store.ErrFunctionCallTurnMismatch), 400, + {"other_turn", fmt.Errorf("submit turn inputs: %w", sessions.ErrFunctionCallTurnMismatch), 400, `{"error":{"message":"The tool call belongs to a different Turn.","type":"invalid_request_error","code":"invalid_request_error","param":null}}`}, - {"missing_session", fmt.Errorf("submit turn inputs: %w", store.ErrNotFound), 404, + {"missing_session", fmt.Errorf("submit turn inputs: %w", sessions.ErrNotFound), 404, `{"error":{"message":"Resource not found.","type":"not_found_error","code":"not_found_error","param":null}}`}, - {"key_reuse", fmt.Errorf("submit turn inputs: %w", store.ErrIdempotencyConflict), 409, + {"key_reuse", fmt.Errorf("submit turn inputs: %w", sessions.ErrIdempotencyConflict), 409, `{"error":{"message":"This idempotency key was used with different input.","type":"conflict_error","code":"idempotency_conflict","param":null}}`}, {"environment_input_expired", execution.ErrEnvironmentInputExpired, 409, `{"error":{"message":"The environment input deadline elapsed before admission.","type":"conflict_error","code":"environment_input_expired","param":null}}`}, diff --git a/services/core/internal/api/function_state_test.go b/services/core/internal/api/function_state_test.go index a3a34761f..4225b8d12 100644 --- a/services/core/internal/api/function_state_test.go +++ b/services/core/internal/api/function_state_test.go @@ -7,11 +7,10 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestFunctionStateEventsUseTheirOwnSnapshot(t *testing.T) { - session := store.Session{ID: "session", CreatedAt: time.Now(), Metadata: map[string]string{}, + session := sessions.Session{ID: "session", CreatedAt: time.Now(), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"none"}}`), RequiredActions: []v1.FunctionCallAction{{CallID: "stale"}}, } diff --git a/services/core/internal/api/handler.go b/services/core/internal/api/handler.go index e4d98de18..b135762b6 100644 --- a/services/core/internal/api/handler.go +++ b/services/core/internal/api/handler.go @@ -12,22 +12,34 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/google/uuid" ) -// Sessions creates, reads, updates and deletes Sessions without execution -// work, and records their public write audit. Creation that admits work goes -// through Execution.Admission. +// SessionCreation creates Sessions without execution work and finds an +// earlier creation by its retry identity. Creation that admits work goes +// through Execution.SessionAdmission. +type SessionCreation interface { + CreateSession(context.Context, string, sessions.CreateSession) (sessions.Session, error) + CreateSessionStream(context.Context, string, sessions.CreateSession) (sessions.Creation, error) + FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (sessions.Creation, error) +} + +// SessionAdmission creates Sessions that admit work through the execution +// Worker, which validates execution support first. +type SessionAdmission interface { + CreateSession(context.Context, string, sessions.CreateSession) (sessions.Session, error) + CreateSessionStream(context.Context, string, sessions.CreateSession) (sessions.Creation, error) +} + +// Sessions reads, updates and deletes Sessions, and records their public write +// audit. type Sessions interface { - CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) - CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) - FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) - GetSession(context.Context, string, string) (store.Session, error) - ListSessions(context.Context, string, string, int, bool, *string) (store.SessionPage, error) - UpdateSessionMetadata(context.Context, string, string, map[string]string) (store.Session, error) + GetSession(context.Context, string, string) (sessions.Session, error) + ListSessions(context.Context, string, string, int, bool, *string) (sessions.Page, error) + UpdateSessionMetadata(context.Context, string, string, map[string]string) (sessions.Session, error) DeleteSession(context.Context, string, string) error AuditSessionOperation(context.Context, string, string, string) error } @@ -250,7 +262,7 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { return } executionConfiguration := sessionExecutionProjection(input, saved, inheritedProvider, provider, selectedEngine, configuration) - createInput := store.CreateSessionInput{ + createInput := sessions.CreateSession{ ExecutionConfiguration: &executionConfiguration, ModelProvider: provider, ModelProviderSource: providerSource, @@ -262,13 +274,13 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { h.createSessionStream(w, r, createInput) return } - create := h.Sessions.CreateSession + create := h.SessionCreation.CreateSession if len(initialInputs) > 0 || input.Environment.Type == "openai_hosted" { if h.Execution == nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution input is not enabled on this service.") return } - create = h.Execution.Admission.CreateSession + create = h.Execution.SessionAdmission.CreateSession } session, err := create(r.Context(), tenantID(r), createInput) if err != nil { @@ -297,11 +309,11 @@ func (h *Handler) getSession(w http.ResponseWriter, r *http.Request) { h.respondSession(w, r, session) } -func (h *Handler) respondSession(w http.ResponseWriter, r *http.Request, session store.Session) { +func (h *Handler) respondSession(w http.ResponseWriter, r *http.Request, session sessions.Session) { h.respondSessionStatus(w, r, session, http.StatusOK) } -func (h *Handler) respondSessionStatus(w http.ResponseWriter, r *http.Request, session store.Session, status int) { +func (h *Handler) respondSessionStatus(w http.ResponseWriter, r *http.Request, session sessions.Session, status int) { response, err := sessionResponse(session, h.executorURL()) if err != nil { writeStoreError(w, r, err) diff --git a/services/core/internal/api/handler_test.go b/services/core/internal/api/handler_test.go index 325794d56..cdcd8d501 100644 --- a/services/core/internal/api/handler_test.go +++ b/services/core/internal/api/handler_test.go @@ -14,7 +14,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/agents" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -22,8 +22,8 @@ import ( // fakeSessions with record. type recordingStore struct { tenant string - input store.CreateSessionInput - sessions []store.Session + input sessions.CreateSession + sessions []sessions.Session nextSessionCursor string listTenant string listAfter string @@ -31,27 +31,28 @@ type recordingStore struct { listAscending bool } -func (s *recordingStore) ListSessions(_ context.Context, tenant, after string, limit int, ascending bool, _ *string) (store.SessionPage, error) { +func (s *recordingStore) ListSessions(_ context.Context, tenant, after string, limit int, ascending bool, _ *string) (sessions.Page, error) { s.listTenant, s.listAfter, s.listLimit, s.listAscending = tenant, after, limit, ascending - return store.SessionPage{Sessions: append([]store.Session(nil), s.sessions...), NextCursor: s.nextSessionCursor}, nil + return sessions.Page{Sessions: append([]sessions.Session(nil), s.sessions...), NextCursor: s.nextSessionCursor}, nil } -func (s *recordingStore) GetSession(_ context.Context, tenant, id string) (store.Session, error) { - return store.Session{ID: id, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"}}`)}, nil +func (s *recordingStore) GetSession(_ context.Context, tenant, id string) (sessions.Session, error) { + return sessions.Session{ID: id, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"}}`)}, nil } -func (s *recordingStore) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) { - return store.SessionCreation{}, store.ErrNotFound +func (s *recordingStore) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (sessions.Creation, error) { + return sessions.Creation{}, sessions.ErrNotFound } -func (s *recordingStore) CreateSession(_ context.Context, tenant string, input store.CreateSessionInput) (store.Session, error) { +func (s *recordingStore) CreateSession(_ context.Context, tenant string, input sessions.CreateSession) (sessions.Session, error) { s.tenant, s.input = tenant, input - return store.Session{ID: uuid.NewString(), TenantID: tenant, Metadata: input.Metadata, Configuration: input.Configuration, CreatedAt: time.Unix(1700000000, 0)}, nil + return sessions.Session{ID: uuid.NewString(), TenantID: tenant, Metadata: input.Metadata, Configuration: input.Configuration, CreatedAt: time.Unix(1700000000, 0)}, nil } // record answers Session creation, reads and listing from s. func (s *recordingStore) record(f *testFakes) { - f.sessions.createSession, f.sessions.getSession, f.sessions.findSessionCreation, f.sessions.listSessions = s.CreateSession, s.GetSession, s.FindSessionCreation, s.ListSessions + f.sessionCreation.createSession, f.sessionCreation.findSessionCreation = s.CreateSession, s.FindSessionCreation + f.sessions.getSession, f.sessions.listSessions = s.GetSession, s.ListSessions } // testHandler serves strict fakes for a fresh tenant whose caller @@ -78,7 +79,7 @@ func testHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (ht // does for a Session with initial input, into the recording store. func admitSessions(d *Dependencies, f *testFakes) { d.Execution = f.execution() - f.admission.createSession = f.sessions.createSession + f.sessionAdmission.createSession = f.sessionCreation.createSession } func TestHTTPConfigurationAndTenantIdentity(t *testing.T) { diff --git a/services/core/internal/api/harness_test.go b/services/core/internal/api/harness_test.go index 047f869bd..57828de4f 100644 --- a/services/core/internal/api/harness_test.go +++ b/services/core/internal/api/harness_test.go @@ -8,7 +8,7 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestSessionHarnessAdmission(t *testing.T) { @@ -94,7 +94,7 @@ func TestSavedHarnessReplacementAndEffectiveRead(t *testing.T) { t.Fatal("mutated saved Agent") } raw, _ := json.Marshal(configuration{Agent: v1.Agent{ID: "agent", Model: "fixture", XAgentsCore: &v1.AgentsCore{Harness: "claude_sdk"}}, Environment: v1.Environment{Type: "none"}}) - response, err := sessionResponse(store.Session{Engine: "mcode", Configuration: raw}, "") + response, err := sessionResponse(sessions.Session{Engine: "mcode", Configuration: raw}, "") if err != nil || response.Agent.XAgentsCore.Harness != "mcode" { t.Fatalf("effective read=%+v %v", response, err) } @@ -102,7 +102,7 @@ func TestSavedHarnessReplacementAndEffectiveRead(t *testing.T) { func TestDefaultHarnessPreservesSessionAgentResponse(t *testing.T) { raw, _ := json.Marshal(configuration{Agent: v1.Agent{ID: "agent", Model: "fixture"}, Environment: v1.Environment{Type: "none"}}) - response, err := sessionResponse(store.Session{Engine: "codex", Configuration: raw}, "") + response, err := sessionResponse(sessions.Session{Engine: "codex", Configuration: raw}, "") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/api/hosted_environment.go b/services/core/internal/api/hosted_environment.go index efc075485..4ed45216d 100644 --- a/services/core/internal/api/hosted_environment.go +++ b/services/core/internal/api/hosted_environment.go @@ -8,7 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // errNetworkPolicy reports a network policy outside the qualified forms. The @@ -25,7 +25,7 @@ func decodeHostedEnvironment(raw json.RawMessage) (*v1.Environment, error) { func decodePreparedEnvironment(raw json.RawMessage) (*v1.Environment, error) { var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } setup, err := decodeEnvironmentSetup(fields) if err != nil { @@ -33,7 +33,7 @@ func decodePreparedEnvironment(raw json.RawMessage) (*v1.Environment, error) { } var kind string if json.Unmarshal(fields["type"], &kind) != nil || (kind != "openai_hosted" && kind != "self_hosted") { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } env := &v1.Environment{Type: kind, Network: &v1.EnvironmentNetworkInput{Access: "enabled"}} for name, value := range fields { @@ -41,7 +41,7 @@ func decodePreparedEnvironment(raw json.RawMessage) (*v1.Environment, error) { case "type": case "workspace_directory": if kind != "self_hosted" || json.Unmarshal(value, &env.WorkspaceDirectory) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } case "network": case "files": @@ -62,13 +62,13 @@ func decodePreparedEnvironment(raw json.RawMessage) (*v1.Environment, error) { packages := setup.PackageMetadata() env.Packages = &packages default: - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } } if value, supplied := fields["network"]; supplied && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) { var network v1.EnvironmentNetworkInput if decodeInputObject(value, &network, "access", "allowed_domains") != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } if (agentnetwork.Policy{Access: network.Access, AllowedDomains: network.AllowedDomains}).Validate() != nil { return nil, errNetworkPolicy @@ -76,19 +76,19 @@ func decodePreparedEnvironment(raw json.RawMessage) (*v1.Environment, error) { env.Network = &network } if kind == "openai_hosted" && agentcapabilities.ValidateDirectories(env.CapabilityDirectories) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } if kind == "self_hosted" && agentcapabilities.ValidateSourceDirectories([]string{env.WorkspaceDirectory}) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } return env, nil } // Hosted metadata describes API-managed initial installations, not workspace inventory. -func hostedSessionEnvironment(environment store.Environment) (v1.SessionEnvironment, error) { +func hostedSessionEnvironment(environment sessions.Environment) (v1.SessionEnvironment, error) { cfg, err := storedEnvironment(environment.Configuration) if err != nil || cfg.Type != "openai_hosted" { - return v1.SessionEnvironment{}, store.ErrInvalidInput + return v1.SessionEnvironment{}, sessions.ErrInvalidInput } files := cfg.Files if files == nil { @@ -103,11 +103,11 @@ func hostedSessionEnvironment(environment store.Environment) (v1.SessionEnvironm func storedEnvironment(raw json.RawMessage) (*v1.Environment, error) { var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } var kind string if json.Unmarshal(fields["type"], &kind) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } if kind != "openai_hosted" && kind != "self_hosted" { return decodeSessionEnvironment(raw) @@ -115,28 +115,28 @@ func storedEnvironment(raw json.RawMessage) (*v1.Environment, error) { // Confidential fields must never appear in the persisted public snapshot. for _, field := range []string{"env", "setup_commands"} { if _, exists := fields[field]; exists { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } } var files []json.RawMessage if value, exists := fields["files"]; exists { if json.Unmarshal(value, &files) != nil || len(files) > 50 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } for _, entry := range files { var metadata environmentconfig.InitialFileMetadata if decodeInputObject(entry, &metadata, "id", "type", "path", "file_id", "size_bytes") != nil || metadata.ID == "" || metadata.SizeBytes == nil || *metadata.SizeBytes < 0 || *metadata.SizeBytes > environmentconfig.MaxInitialFileBytes { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } if metadata.Type != "inline" && metadata.Type != "file_id" { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } } } if value, ok := fields["initialization"]; ok { var initialized bool if json.Unmarshal(value, &initialized) != nil || !initialized { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } delete(fields, "initialization") } diff --git a/services/core/internal/api/hosted_environment_test.go b/services/core/internal/api/hosted_environment_test.go index b1facbd71..471a06a4c 100644 --- a/services/core/internal/api/hosted_environment_test.go +++ b/services/core/internal/api/hosted_environment_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestHostedEnvironmentDefaultsAndExplicitGaps(t *testing.T) { @@ -66,10 +66,10 @@ func TestHostedEnvironmentResponseHasPinnedShapeAndNoConnectionAction(t *testing if !reflect.DeepEqual(got, expected) { t.Fatal("hosted response shape changed", string(raw)) } - for _, mutate := range []func(*store.Environment){ - func(e *store.Environment) { e.TenantID = "foreign" }, - func(e *store.Environment) { e.SessionID = "other" }, - func(e *store.Environment) { + for _, mutate := range []func(*sessions.Environment){ + func(e *sessions.Environment) { e.TenantID = "foreign" }, + func(e *sessions.Environment) { e.SessionID = "other" }, + func(e *sessions.Environment) { e.Configuration = json.RawMessage(`{"type":"self_hosted","workspace_directory":"/workspace"}`) }, } { @@ -90,7 +90,7 @@ func TestHostedCreationUsesExecutionAdmission(t *testing.T) { recorder := &hostedCreationRecorder{} handler, fixture := environmentCreationHandler(t, "codex", func(d *Dependencies, f *testFakes) { d.Execution, d.Sandboxes = f.execution(), f.sandboxes() - f.admission.createSession, f.admission.createSessionStream = recorder.CreateSession, recorder.CreateSessionStream + f.sessionAdmission.createSession, f.sessionAdmission.createSessionStream = recorder.CreateSession, recorder.CreateSessionStream }) input := "" if stream { @@ -117,11 +117,11 @@ type hostedCreationRecorder struct { calls int } -func (r *hostedCreationRecorder) CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) { +func (r *hostedCreationRecorder) CreateSession(context.Context, string, sessions.CreateSession) (sessions.Session, error) { r.calls++ - return store.Session{}, store.ErrInvalidInput + return sessions.Session{}, sessions.ErrInvalidInput } -func (r *hostedCreationRecorder) CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) { +func (r *hostedCreationRecorder) CreateSessionStream(context.Context, string, sessions.CreateSession) (sessions.Creation, error) { r.calls++ - return store.SessionCreation{}, store.ErrInvalidInput + return sessions.Creation{}, sessions.ErrInvalidInput } diff --git a/services/core/internal/api/hosted_failure_test.go b/services/core/internal/api/hosted_failure_test.go index c5a1d0f7e..db2dd4fa8 100644 --- a/services/core/internal/api/hosted_failure_test.go +++ b/services/core/internal/api/hosted_failure_test.go @@ -16,13 +16,12 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) const hostedFailureReason = `Failed to provision environment: script "setup_commands[0]" failed with exit code 3` -func hostedFailureSession() store.Session { +func hostedFailureSession() sessions.Session { session := environmentSession() session.Configuration = json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"openai_hosted"}}`) session.Environment.Configuration = json.RawMessage(`{"type":"openai_hosted"}`) @@ -179,17 +178,17 @@ func TestGetStreamEndsAfterHostedProvisioningFailure(t *testing.T) { // keeps the local environment_unavailable response. func TestHostedProvisioningFailureInputConflict(t *testing.T) { for err, want := range map[error]string{ - fmt.Errorf("reserve: %w", store.ErrHostedEnvironmentFailed): `{"error":{"message":"the hosted environment failed to provision","type":"conflict_error","code":"conflict_error","param":null}}`, - store.ErrEnvironmentUnavailable: `{"error":{"message":"The environment is no longer available for new input.","type":"conflict_error","code":"environment_unavailable","param":null}}`, + fmt.Errorf("reserve: %w", sessions.ErrHostedEnvironmentFailed): `{"error":{"message":"the hosted environment failed to provision","type":"conflict_error","code":"conflict_error","param":null}}`, + sessions.ErrEnvironmentUnavailable: `{"error":{"message":"The environment is no longer available for new input.","type":"conflict_error","code":"environment_unavailable","param":null}}`, } { response := httptest.NewRecorder() - writeStoreError(response, httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/session/events", nil), err) + writeSessionsError(response, httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/session/events", nil), err) body, _ := io.ReadAll(response.Body) if response.Code != http.StatusConflict || strings.TrimSpace(string(body)) != want { t.Fatal(response.Code, string(body)) } } - if !errors.Is(store.ErrHostedEnvironmentFailed, store.ErrEnvironmentUnavailable) { + if !errors.Is(sessions.ErrHostedEnvironmentFailed, sessions.ErrEnvironmentUnavailable) { t.Fatal("internal callers no longer see an unavailable Environment") } } diff --git a/services/core/internal/api/initial_files.go b/services/core/internal/api/initial_files.go index e77fe6eb4..170764789 100644 --- a/services/core/internal/api/initial_files.go +++ b/services/core/internal/api/initial_files.go @@ -5,7 +5,7 @@ import ( "encoding/json" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func decodeInitialFiles(raw json.RawMessage) ([]environmentconfig.InitialFile, error) { @@ -14,7 +14,7 @@ func decodeInitialFiles(raw json.RawMessage) ([]environmentconfig.InitialFile, e } var entries []json.RawMessage if json.Unmarshal(raw, &entries) != nil || len(entries) > 50 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } files := make([]environmentconfig.InitialFile, 0, len(entries)) for _, entry := range entries { @@ -25,7 +25,7 @@ func decodeInitialFiles(raw json.RawMessage) ([]environmentconfig.InitialFile, e FileID *string `json:"file_id"` } if decodeInputObject(entry, &in, "type", "path", "data", "file_id") != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } var fields map[string]json.RawMessage _ = json.Unmarshal(entry, &fields) @@ -33,20 +33,20 @@ func decodeInitialFiles(raw json.RawMessage) ([]environmentconfig.InitialFile, e switch in.Type { case "inline": if _, exists := fields["file_id"]; exists || in.Data == nil || len(*in.Data) > base64.StdEncoding.EncodedLen(5<<20) { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } var err error f.Data, err = base64.StdEncoding.Strict().DecodeString(*in.Data) if err != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } case "file_id": if _, exists := fields["data"]; exists || in.FileID == nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } f.FileID = *in.FileID default: - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } files = append(files, f) } diff --git a/services/core/internal/api/inputs.go b/services/core/internal/api/inputs.go index 74a9a8c1d..94f9af3e4 100644 --- a/services/core/internal/api/inputs.go +++ b/services/core/internal/api/inputs.go @@ -8,17 +8,15 @@ import ( "reflect" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) -// Admission creates Sessions that admit work and submits Session input through -// the execution Worker, which validates execution support first. -type Admission interface { - CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) - CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) - SubmitInputs(context.Context, string, string, string, []store.Input) ([]store.InputReceipt, error) +// InputAdmission submits Session input through the execution Worker, which +// validates execution support first. +type InputAdmission interface { + SubmitInputs(context.Context, string, string, string, []sessions.Input) ([]sessions.InputReceipt, error) } // @Summary Submit Session input events @@ -53,7 +51,7 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) { if key == "" { key = uuid.NewString() } - if err := store.ValidateInputKey(key); err != nil { + if err := sessions.ValidateInputKey(key); err != nil { writeStoreError(w, r, err) return } @@ -85,7 +83,7 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, err) return } - if _, err := h.Execution.Admission.SubmitInputs(r.Context(), tenantID(r), sessionID, key, inputs); err != nil { + if _, err := h.Execution.InputAdmission.SubmitInputs(r.Context(), tenantID(r), sessionID, key, inputs); err != nil { writeInputError(w, r, err) return } @@ -93,11 +91,11 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusAccepted) } -func executionInputs(events []json.RawMessage) ([]store.Input, error) { +func executionInputs(events []json.RawMessage) ([]sessions.Input, error) { if len(events) == 0 || len(events) > 64 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } - inputs := make([]store.Input, 0, len(events)) + inputs := make([]sessions.Input, 0, len(events)) for _, raw := range events { event, err := decodeInputEvent(raw) if err != nil { @@ -106,9 +104,9 @@ func executionInputs(events []json.RawMessage) ([]store.Input, error) { switch event.Type { case "agent.session.input.cancel": if event.Input != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } - inputs = append(inputs, store.Input{Kind: "cancel", Payload: json.RawMessage(`{}`)}) + inputs = append(inputs, sessions.Input{Kind: "cancel", Payload: json.RawMessage(`{}`)}) case "agent.session.input.tool_result": input, err := functionResultInput(event) if err != nil { @@ -117,27 +115,27 @@ func executionInputs(events []json.RawMessage) ([]store.Input, error) { inputs = append(inputs, input) case "agent.session.input.message": if len(event.Input) == 0 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } for _, message := range event.Input { if message.Role != "user" || (message.Type != "" && message.Type != "message") || len(message.Content) == 0 { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } converted := proto.MessageInput{{}} for _, content := range message.Content { converted[0].Content = append(converted[0].Content, proto.InputContent{Type: content.Type, Text: content.Text, ImageURL: content.ImageURL}) } if converted.Validate() != nil || converted.ValidateInlineImages() != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } } payload, err := json.Marshal(event) if err != nil { return nil, err } - inputs = append(inputs, store.Input{Kind: "message", Payload: payload}) + inputs = append(inputs, sessions.Input{Kind: "message", Payload: payload}) default: - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } } return inputs, nil diff --git a/services/core/internal/api/inputs_test.go b/services/core/internal/api/inputs_test.go index 8bd111f7e..4af2099d2 100644 --- a/services/core/internal/api/inputs_test.go +++ b/services/core/internal/api/inputs_test.go @@ -9,18 +9,18 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // inputRecorder is the Worker's input admission. It records submitted inputs // and answers with err. type inputRecorder struct { tenant, session, key string - inputs []store.Input + inputs []sessions.Input err error } -func (s *inputRecorder) SubmitInputs(_ context.Context, tenant, session, key string, inputs []store.Input) ([]store.InputReceipt, error) { +func (s *inputRecorder) SubmitInputs(_ context.Context, tenant, session, key string, inputs []sessions.Input) ([]sessions.InputReceipt, error) { s.tenant, s.session, s.key, s.inputs = tenant, session, key, inputs return nil, s.err } @@ -28,7 +28,7 @@ func (s *inputRecorder) SubmitInputs(_ context.Context, tenant, session, key str // admit enables Execution whose Worker records submitted inputs in s. func (s *inputRecorder) admit(d *Dependencies, f *testFakes) { d.Execution = f.execution() - f.admission.submitInputs = s.SubmitInputs + f.inputAdmission.submitInputs = s.SubmitInputs } func TestPublicInputAdmission(t *testing.T) { diff --git a/services/core/internal/api/items.go b/services/core/internal/api/items.go index 5ee75c336..5c294c89a 100644 --- a/services/core/internal/api/items.go +++ b/services/core/internal/api/items.go @@ -1,10 +1,19 @@ package api import ( - "github.com/go-chi/chi/v5" + "context" "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) +// Items reads a Session's root Items. +type Items interface { + ListItems(context.Context, string, string, string, int, bool) (sessions.ItemPage, error) +} + // @Summary List persisted execution Items // @Description Returns supported message and tool Items in first-observation order. Native engine fields are projected explicitly; unfinished Items on terminal Turns are incomplete. Cursors are Items of the same tenant and Session. Any other after value, including a malformed one, returns 400 invalid_request_error with the message "Invalid session item ID in `after`". // @Tags Items @@ -23,7 +32,7 @@ func (h *Handler) listItems(w http.ResponseWriter, r *http.Request) { if !ok { return } - page, err := h.SessionHistory.ListItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), options.after, options.limit, options.ascending) + page, err := h.Items.ListItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/items_test.go b/services/core/internal/api/items_test.go index 676c43714..9bae53591 100644 --- a/services/core/internal/api/items_test.go +++ b/services/core/internal/api/items_test.go @@ -8,7 +8,7 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type itemReadStore struct { @@ -17,13 +17,13 @@ type itemReadStore struct { ascending bool } -func (s *itemReadStore) ListItems(_ context.Context, tenant, session, cursor string, limit int, asc bool) (store.ItemPage, error) { +func (s *itemReadStore) ListItems(_ context.Context, tenant, session, cursor string, limit int, asc bool) (sessions.ItemPage, error) { s.tenant, s.session, s.cursor, s.limit, s.ascending = tenant, session, cursor, limit, asc - return store.ItemPage{Items: []v1.Item{}, HasMore: false}, nil + return sessions.ItemPage{Items: []v1.Item{}, HasMore: false}, nil } func TestItemRouteUsesAuthenticationAndSharedPagination(t *testing.T) { s := &itemReadStore{} - h, _, tenant := testHandler(t, func(_ *Dependencies, f *testFakes) { f.sessionHistory.listItems = s.ListItems }) + h, _, tenant := testHandler(t, func(_ *Dependencies, f *testFakes) { f.items.listItems = s.ListItems }) request := func(query, token string) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodGet, "/v1/agents/sessions/session/items"+query, nil) r.Header.Set("Authorization", "Bearer "+token) diff --git a/services/core/internal/api/native_classification_integration_test.go b/services/core/internal/api/native_classification_integration_test.go index 992f25948..64cb83480 100644 --- a/services/core/internal/api/native_classification_integration_test.go +++ b/services/core/internal/api/native_classification_integration_test.go @@ -10,7 +10,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -19,7 +18,7 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { h, _, tenant := adminTestHandler(t, databaseSessionReads(s)) for _, code := range []string{"authentication_error", "connection_failed", "secret-canary"} { t.Run(code, func(t *testing.T) { - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "native-classification"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "native-classification"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) if err != nil { t.Fatal(err) } @@ -27,7 +26,7 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } status := 503 @@ -36,7 +35,7 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: outcome}); err != nil { + if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: outcome}); err != nil { t.Fatal(err) } snap, err := s.GetTurnDiagnosticsSnapshot(t.Context(), tenant, session.ID, receipt.TurnID) diff --git a/services/core/internal/api/resource_query_test.go b/services/core/internal/api/resource_query_test.go index 864ee2461..7c6f96359 100644 --- a/services/core/internal/api/resource_query_test.go +++ b/services/core/internal/api/resource_query_test.go @@ -16,8 +16,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/agents" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -29,7 +29,7 @@ type missingResourceStore struct { func (s *missingResourceStore) missing(tenant string) error { s.tenants = append(s.tenants, tenant) - return store.ErrNotFound + return sessions.ErrNotFound } // missingAgent records the tenant and reports the Agent as missing. @@ -50,16 +50,16 @@ func (s *missingResourceStore) UpdateAgent(_ context.Context, command agents.Upd return agents.Agent{}, s.missingAgent(command.TenantID) } -func (s *missingResourceStore) GetSession(_ context.Context, tenant, _ string) (store.Session, error) { - return store.Session{}, s.missing(tenant) +func (s *missingResourceStore) GetSession(_ context.Context, tenant, _ string) (sessions.Session, error) { + return sessions.Session{}, s.missing(tenant) } func (s *missingResourceStore) DeleteSession(_ context.Context, tenant, _ string) error { return s.missing(tenant) } -func (s *missingResourceStore) UpdateSessionMetadata(_ context.Context, tenant, _ string, _ map[string]string) (store.Session, error) { - return store.Session{}, s.missing(tenant) +func (s *missingResourceStore) UpdateSessionMetadata(_ context.Context, tenant, _ string, _ map[string]string) (sessions.Session, error) { + return sessions.Session{}, s.missing(tenant) } // Environment Template operations report a missing Template with their @@ -257,7 +257,7 @@ type ownedArtifactStore struct { func (s *ownedArtifactStore) DeleteSessionArtifact(_ context.Context, tenant, session, id string) error { s.tenants = append(s.tenants, tenant) if tenant != s.owner || session != "session" || id != "artifact" { - return store.ErrNotFound + return sessions.ErrNotFound } s.deleted++ return nil diff --git a/services/core/internal/api/runtime_history_test.go b/services/core/internal/api/runtime_history_test.go index 404ea2966..9e526b61e 100644 --- a/services/core/internal/api/runtime_history_test.go +++ b/services/core/internal/api/runtime_history_test.go @@ -10,7 +10,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -141,7 +141,7 @@ func TestRuntimeHistoryRejectsUnsafeQueriesAndFailures(t *testing.T) { {runtimehistory.ErrUnsupported, http.StatusConflict}, {runtimehistory.ErrUnavailable, http.StatusServiceUnavailable}, {runtimehistory.ErrInvalidResult, http.StatusServiceUnavailable}, - {store.ErrNotFound, http.StatusNotFound}, + {sessions.ErrNotFound, http.StatusNotFound}, } { service.err = tc.err response := runtimeObservationRequest(handler, path) diff --git a/services/core/internal/api/sandbox_deployment_setup.go b/services/core/internal/api/sandbox_deployment_setup.go index c75146109..74af0ad25 100644 --- a/services/core/internal/api/sandbox_deployment_setup.go +++ b/services/core/internal/api/sandbox_deployment_setup.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -189,7 +190,7 @@ func (h *Handler) cancelSandboxReset(w http.ResponseWriter, r *http.Request) { func parseResetGeneration(r *http.Request) (uint64, error) { query, err := url.ParseQuery(r.URL.RawQuery) if err != nil || len(query) != 1 || len(query["expected_generation"]) != 1 { - return 0, store.ErrInvalidInput + return 0, sessions.ErrInvalidInput } return strconv.ParseUint(query.Get("expected_generation"), 10, 64) } diff --git a/services/core/internal/api/sandbox_selector_test.go b/services/core/internal/api/sandbox_selector_test.go index 9c225c9bb..8f20e2ee2 100644 --- a/services/core/internal/api/sandbox_selector_test.go +++ b/services/core/internal/api/sandbox_selector_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -18,9 +18,9 @@ type sandboxCreationRecorder struct { calls int } -func (r *sandboxCreationRecorder) CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) { +func (r *sandboxCreationRecorder) CreateSession(context.Context, string, sessions.CreateSession) (sessions.Session, error) { r.calls++ - return store.Session{}, store.ErrInvalidInput + return sessions.Session{}, sessions.ErrInvalidInput } // Placement is automatic. A node selector is an unknown member wherever it appears. @@ -34,7 +34,7 @@ func TestSessionCreationRejectsSandboxNodeSelector(t *testing.T) { recorder := &sandboxCreationRecorder{} handler, _ := environmentCreationHandler(t, "codex", func(d *Dependencies, f *testFakes) { d.Execution, d.Sandboxes = f.execution(), f.sandboxes() - f.admission.createSession = recorder.CreateSession + f.sessionAdmission.createSession = recorder.CreateSession }) request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) request.Header.Set("Authorization", "Bearer key") diff --git a/services/core/internal/api/session_admission_test.go b/services/core/internal/api/session_admission_test.go index f9a238ab6..49914f9f4 100644 --- a/services/core/internal/api/session_admission_test.go +++ b/services/core/internal/api/session_admission_test.go @@ -14,7 +14,8 @@ import ( // forbidSessionAccess withdraws the Session and deployment model provider reads // testHandler serves, so any access fails the test. func forbidSessionAccess(_ *Dependencies, f *testFakes) { - f.sessions.createSession, f.sessions.getSession, f.sessions.findSessionCreation, f.sessions.listSessions = nil, nil, nil, nil + f.sessionCreation.createSession, f.sessionCreation.findSessionCreation = nil, nil + f.sessions.getSession, f.sessions.listSessions = nil, nil f.modelProviders.resolve = nil } diff --git a/services/core/internal/api/session_artifacts.go b/services/core/internal/api/session_artifacts.go index 1a835b169..cdc6400ac 100644 --- a/services/core/internal/api/session_artifacts.go +++ b/services/core/internal/api/session_artifacts.go @@ -7,15 +7,15 @@ import ( "path" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" ) // Artifacts reads, streams and deletes a Session's published Artifacts. type Artifacts interface { - GetSessionArtifact(context.Context, string, string, string) (store.SessionArtifact, error) - ListSessionArtifacts(context.Context, string, string, string, string, int, bool) (store.ArtifactPage, error) - ReadSessionArtifact(context.Context, string, string, string, func(store.SessionArtifact, io.Reader) error) error + GetSessionArtifact(context.Context, string, string, string) (sessions.Artifact, error) + ListSessionArtifacts(context.Context, string, string, string, string, int, bool) (sessions.ArtifactPage, error) + ReadSessionArtifact(context.Context, string, string, string, func(sessions.Artifact, io.Reader) error) error DeleteSessionArtifact(context.Context, string, string, string) error } @@ -103,7 +103,7 @@ func (h *Handler) deleteSessionArtifact(w http.ResponseWriter, r *http.Request) // @Router /agents/sessions/{session_id}/artifacts/{artifact_id}/content [get] func (h *Handler) sessionArtifactContent(w http.ResponseWriter, r *http.Request) { err := serveStoredContent(w, r, func(ctx context.Context, consume func(string, int64, io.Reader) error) error { - return h.Artifacts.ReadSessionArtifact(ctx, tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "artifact_id"), func(a store.SessionArtifact, body io.Reader) error { + return h.Artifacts.ReadSessionArtifact(ctx, tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "artifact_id"), func(a sessions.Artifact, body io.Reader) error { return consume(path.Base(a.Path), a.SizeBytes, body) }) }) @@ -112,7 +112,7 @@ func (h *Handler) sessionArtifactContent(w http.ResponseWriter, r *http.Request) } } -func artifactResponse(a store.SessionArtifact) v1.SessionArtifact { +func artifactResponse(a sessions.Artifact) v1.SessionArtifact { return v1.SessionArtifact{ID: a.ID, CreatedAt: a.CreatedAt.Unix(), EnvironmentID: a.EnvironmentID, Object: "agent.session.artifact", Path: a.Path, SessionID: a.SessionID, SizeBytes: a.SizeBytes, TurnID: a.TurnID} } diff --git a/services/core/internal/api/session_artifacts_test.go b/services/core/internal/api/session_artifacts_test.go index 49902c616..f8ba1a65e 100644 --- a/services/core/internal/api/session_artifacts_test.go +++ b/services/core/internal/api/session_artifacts_test.go @@ -12,11 +12,11 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type artifactFixture struct { - artifact store.SessionArtifact + artifact sessions.Artifact tenant, session, id, environment, cursor string limit int ascending bool @@ -25,22 +25,22 @@ type artifactFixture struct { calls int } -func (f *artifactFixture) GetSessionArtifact(_ context.Context, tenant, session, id string) (store.SessionArtifact, error) { +func (f *artifactFixture) GetSessionArtifact(_ context.Context, tenant, session, id string) (sessions.Artifact, error) { f.calls++ f.tenant, f.session, f.id = tenant, session, id return f.artifact, f.err } -func (f *artifactFixture) ListSessionArtifacts(_ context.Context, tenant, session, environment, cursor string, limit int, ascending bool) (store.ArtifactPage, error) { +func (f *artifactFixture) ListSessionArtifacts(_ context.Context, tenant, session, environment, cursor string, limit int, ascending bool) (sessions.ArtifactPage, error) { f.calls++ f.tenant, f.session, f.environment, f.cursor, f.limit, f.ascending = tenant, session, environment, cursor, limit, ascending if f.empty { - return store.ArtifactPage{Artifacts: []store.SessionArtifact{}}, f.err + return sessions.ArtifactPage{Artifacts: []sessions.Artifact{}}, f.err } - return store.ArtifactPage{Artifacts: []store.SessionArtifact{f.artifact}, NextCursor: f.artifact.ID}, f.err + return sessions.ArtifactPage{Artifacts: []sessions.Artifact{f.artifact}, NextCursor: f.artifact.ID}, f.err } -func (f *artifactFixture) ReadSessionArtifact(ctx context.Context, tenant, session, id string, consume func(store.SessionArtifact, io.Reader) error) error { +func (f *artifactFixture) ReadSessionArtifact(ctx context.Context, tenant, session, id string, consume func(sessions.Artifact, io.Reader) error) error { a, err := f.GetSessionArtifact(ctx, tenant, session, id) if err != nil { return err @@ -64,7 +64,7 @@ type artifactResponseRecorder struct{ *httptest.ResponseRecorder } func (*artifactResponseRecorder) SetWriteDeadline(time.Time) error { return nil } func TestSessionArtifactRoutesAndPublicProjection(t *testing.T) { - f := &artifactFixture{artifact: store.SessionArtifact{ID: "artifact", SessionID: "session", EnvironmentID: "environment", TurnID: "turn", Path: "/workspace/outputs/a.bin", SizeBytes: 3, CreatedAt: time.Unix(123, 456)}} + f := &artifactFixture{artifact: sessions.Artifact{ID: "artifact", SessionID: "session", EnvironmentID: "environment", TurnID: "turn", Path: "/workspace/outputs/a.bin", SizeBytes: 3, CreatedAt: time.Unix(123, 456)}} h, _, tenant := testHandler(t, f.wire) request := func(method, suffix, beta string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, "/v1/agents/sessions/session/artifacts"+suffix, nil) @@ -123,7 +123,7 @@ func TestSessionArtifactRoutesAndPublicProjection(t *testing.T) { if w := request(route.method, route.suffix, ""); w.Code != 400 || f.calls != before { t.Fatalf("missing Beta accepted: %s %s %d", route.method, route.suffix, w.Code) } - f.err = store.ErrNotFound + f.err = sessions.ErrNotFound if w := request(route.method, route.suffix, "agents=v1"); w.Code != 404 { t.Fatalf("not-found mapping: %s %s %d", route.method, route.suffix, w.Code) } diff --git a/services/core/internal/api/session_creation_identity.go b/services/core/internal/api/session_creation_identity.go index 0afba228a..22a0bb248 100644 --- a/services/core/internal/api/session_creation_identity.go +++ b/services/core/internal/api/session_creation_identity.go @@ -7,7 +7,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // sessionCreationRequest records caller intent before mutable sources resolve: @@ -15,7 +15,7 @@ import ( // then returns the committed Session even after those sources change. Other // inline requests keep the resolved-request retry rule; the store leaves the // deployment default out of that hash, so it cannot change their identity. -func sessionCreationRequest(input sessionRequest, initial []store.Input) (json.RawMessage, error) { +func sessionCreationRequest(input sessionRequest, initial []sessions.Input) (json.RawMessage, error) { if input.Agent != nil && input.Agent.Model != nil && (input.Environment == nil || input.Environment.Type != "openai_hosted") && input.XAgentsCore == nil && input.AgentID == nil && input.templateID == "" && len(input.initialFiles) == 0 && input.initialization.Empty() && !inlineCredentialIntent(input) && input.agentFields["x_agents_core"] == nil { return nil, nil } @@ -37,7 +37,7 @@ func sessionCreationRequest(input sessionRequest, initial []store.Input) (json.R Environment any `json:"environment"` Metadata map[string]string `json:"metadata,omitempty"` VaultIDs []string `json:"vault_ids,omitempty"` - InitialInputs []store.Input `json:"initial_inputs,omitempty"` + InitialInputs []sessions.Input `json:"initial_inputs,omitempty"` }{input.XAgentsCore, agentID, input.agentFields, environment, input.Metadata, input.VaultIDs, initial}) } @@ -45,8 +45,8 @@ func (h *Handler) recoverSessionCreation(w http.ResponseWriter, r *http.Request, if len(request) == 0 { return false } - result, err := h.Sessions.FindSessionCreation(r.Context(), tenantID(r), key, request, sessionCreator(r)) - if errors.Is(err, store.ErrNotFound) { + result, err := h.SessionCreation.FindSessionCreation(r.Context(), tenantID(r), key, request, sessionCreator(r)) + if errors.Is(err, sessions.ErrNotFound) { return false } if err != nil { diff --git a/services/core/internal/api/session_creation_stream.go b/services/core/internal/api/session_creation_stream.go index 6a8801c4f..b640f5a48 100644 --- a/services/core/internal/api/session_creation_stream.go +++ b/services/core/internal/api/session_creation_stream.go @@ -7,23 +7,22 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) -func (h *Handler) createSessionStream(w http.ResponseWriter, r *http.Request, input store.CreateSessionInput) { +func (h *Handler) createSessionStream(w http.ResponseWriter, r *http.Request, input sessions.CreateSession) { var config configuration if err := json.Unmarshal(input.Configuration, &config); err != nil { - writeStoreError(w, r, store.ErrInvalidInput) + writeStoreError(w, r, sessions.ErrInvalidInput) return } - create := h.Sessions.CreateSessionStream + create := h.SessionCreation.CreateSessionStream if len(input.InitialInputs) > 0 || config.Environment.Type == "openai_hosted" { if h.Execution == nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution input is not enabled on this service.") return } - create = h.Execution.Admission.CreateSessionStream + create = h.Execution.SessionAdmission.CreateSessionStream } result, err := create(r.Context(), tenantID(r), input) if err != nil { @@ -41,7 +40,7 @@ func (h *Handler) createSessionStream(w http.ResponseWriter, r *http.Request, in // official same-key requests create distinct Sessions, so there is no retry // stream to follow. Recover with stream=false or the GET events stream. Only a // fresh creation uses snapshots. -func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Request, result store.SessionCreation) { +func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Request, result sessions.Creation) { if !result.Created { openEventStream(w, http.StatusCreated) return @@ -78,7 +77,7 @@ func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Re // sessionSettled reports that a committed projection has no admitted work left: // the Session is idle or failed, its latest Turn is not queued, running or // waiting, and its latest input reservation is not pending. -func sessionSettled(session store.Session, response v1.Session) bool { +func sessionSettled(session sessions.Session, response v1.Session) bool { if response.Status != "idle" && response.Status != "failed" { return false } diff --git a/services/core/internal/api/session_creation_stream_test.go b/services/core/internal/api/session_creation_stream_test.go index ab64fc44f..bb6dfc839 100644 --- a/services/core/internal/api/session_creation_stream_test.go +++ b/services/core/internal/api/session_creation_stream_test.go @@ -17,7 +17,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -27,7 +26,7 @@ import ( // they describe. type creationStreamFixture struct { streamFixture - creation store.SessionCreation + creation sessions.Creation recorded bool deleted bool sequence int64 @@ -38,11 +37,11 @@ type creationStreamFixture struct { // GetSession returns the committed projection that commit changes atomically // with its events. -func (f *creationStreamFixture) GetSession(_ context.Context, tenant, id string) (store.Session, error) { +func (f *creationStreamFixture) GetSession(_ context.Context, tenant, id string) (sessions.Session, error) { f.mu.Lock() defer f.mu.Unlock() if f.deleted || tenant != f.session.TenantID || id != f.session.ID { - return store.Session{}, store.ErrNotFound + return sessions.Session{}, sessions.ErrNotFound } return f.session, nil } @@ -68,7 +67,7 @@ func (f *creationStreamFixture) ListSessionEvents(_ context.Context, _, _ string // SessionStreamSnapshot returns the projection and cursor from one snapshot, // running the one-shot race hooks before and after that read. -func (f *creationStreamFixture) SessionStreamSnapshot(_ context.Context, tenant, id string) (store.Session, int64, error) { +func (f *creationStreamFixture) SessionStreamSnapshot(_ context.Context, tenant, id string) (sessions.Session, int64, error) { f.mu.Lock() before := f.beforeSnapshot f.beforeSnapshot = nil @@ -85,21 +84,21 @@ func (f *creationStreamFixture) SessionStreamSnapshot(_ context.Context, tenant, race() } if deleted || tenant != session.TenantID || id != session.ID { - return store.Session{}, 0, store.ErrNotFound + return sessions.Session{}, 0, sessions.ErrNotFound } return session, cursor, nil } -func (f *creationStreamFixture) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) { +func (f *creationStreamFixture) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (sessions.Creation, error) { if !f.recorded { - return store.SessionCreation{}, store.ErrNotFound + return sessions.Creation{}, sessions.ErrNotFound } f.mu.Lock() defer f.mu.Unlock() // Recorded-intent lookup returns only the resource row and its cursor. row := f.session row.LastTurn, row.Usage, row.RequiredActions = nil, nil, nil - return store.SessionCreation{Session: row, Cursor: 10}, nil + return sessions.Creation{Session: row, Cursor: 10}, nil } // AuditSessionOperation accepts the audit of a replayed creation. @@ -109,7 +108,7 @@ func (f *creationStreamFixture) AuditSessionOperation(context.Context, string, s // CreateSessionStream is the Worker's streamed admission: it returns the // prepared creation. -func (f *creationStreamFixture) CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) { +func (f *creationStreamFixture) CreateSessionStream(context.Context, string, sessions.CreateSession) (sessions.Creation, error) { return f.creation, nil } @@ -138,7 +137,7 @@ type creationStreamHarness struct { func newCreationStreamHarness(t *testing.T) *creationStreamHarness { t.Helper() tenant := uuid.NewString() - fixture := &creationStreamFixture{sequence: 10, streamFixture: streamFixture{session: store.Session{ + fixture := &creationStreamFixture{sequence: 10, streamFixture: streamFixture{session: sessions.Session{ ID: uuid.NewString(), TenantID: tenant, CreatedAt: time.Unix(1700000000, 0), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"none"}}`), }}} @@ -147,11 +146,12 @@ func newCreationStreamHarness(t *testing.T) *creationStreamHarness { OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("key"), TenantID: tenant, }).ResolveAPIKey - fakes.sessions.getSession, fakes.sessions.findSessionCreation, fakes.sessions.auditSessionOperation = fixture.GetSession, fixture.FindSessionCreation, fixture.AuditSessionOperation + fakes.sessions.getSession, fakes.sessions.auditSessionOperation = fixture.GetSession, fixture.AuditSessionOperation + fakes.sessionCreation.findSessionCreation = fixture.FindSessionCreation fakes.sessionEvents.sessionEventCursor, fakes.sessionEvents.sessionStreamSnapshot, fakes.sessionEvents.listSessionEvents = fixture.SessionEventCursor, fixture.SessionStreamSnapshot, fixture.ListSessionEvents fakes.modelProviders.resolve = noDeploymentModelProvider deps.Execution = fakes.execution() - fakes.admission.createSessionStream = fixture.CreateSessionStream + fakes.sessionAdmission.createSessionStream = fixture.CreateSessionStream handler := newTestHandler(t, deps) h := &creationStreamHarness{t: t, fixture: fixture} h.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -184,7 +184,7 @@ func (h *creationStreamHarness) turn(status string, usage string) *sessions.Turn // commit applies a projection change and its events atomically, as the Store // commits Session events with the state they describe. -func (h *creationStreamHarness) commit(update func(*store.Session), changes ...sessions.SessionChange) { +func (h *creationStreamHarness) commit(update func(*sessions.Session), changes ...sessions.SessionChange) { h.fixture.mu.Lock() defer h.fixture.mu.Unlock() if update != nil { @@ -336,8 +336,8 @@ const creationBody = `{"agent":{"model":"model"},"environment":{"type":"none"}," const savedBody = `{"agent_id":"agent_test","environment":{"type":"none"},"input":"First","stream":true}` -func setTurn(turn *sessions.Turn) func(*store.Session) { - return func(session *store.Session) { session.LastTurn, session.RequiredActions = turn, nil } +func setTurn(turn *sessions.Turn) func(*sessions.Session) { + return func(session *sessions.Session) { session.LastTurn, session.RequiredActions = turn, nil } } func TestCreationStreamClosesOnceSettled(t *testing.T) { @@ -351,7 +351,7 @@ func TestCreationStreamClosesOnceSettled(t *testing.T) { // The admission result is the committed post-input projection. queued := h.turn(sessions.TurnQueued, "") h.commit(setTurn(queued), turnChange("created", queued), userItemChange(queued), sessionChange("in_progress", queued, nil)) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: 10} + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: 10} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() observed := expectTypes(t, frames, "agent.session.created", "agent.session.turn.created", "agent.session.turn.item.added", "agent.session.in_progress") @@ -390,14 +390,14 @@ func TestCreationStreamStaysOpenAcrossRequiredAction(t *testing.T) { h := newCreationStreamHarness(t) queued := h.turn(sessions.TurnQueued, "") h.commit(setTurn(queued), turnChange("created", queued), userItemChange(queued), sessionChange("in_progress", queued, nil)) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: 10} + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: 10} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() expectTypes(t, frames, "agent.session.created", "agent.session.turn.created", "agent.session.turn.item.added", "agent.session.in_progress") waiting := h.turn(sessions.TurnWaiting, "") actions := []v1.FunctionCallAction{{Type: "function_call", CallID: "call_1", Name: "lookup", TurnID: waiting.ID, Arguments: json.RawMessage(`{}`)}} - h.commit(func(session *store.Session) { session.LastTurn, session.RequiredActions = waiting, actions }, + h.commit(func(session *sessions.Session) { session.LastTurn, session.RequiredActions = waiting, actions }, sessionChange("requires_action", waiting, actions)) expectTypes(t, frames, "agent.session.requires_action") expectOpen(t, frames) @@ -415,7 +415,7 @@ func TestCreationStreamStaysOpenAcrossRequiredAction(t *testing.T) { func TestCreationStreamWithoutAdmittedWorkClosesAfterCreated(t *testing.T) { h := newCreationStreamHarness(t) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: 10} + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: 10} // Work committed after the creation is not followed. queued := h.turn(sessions.TurnQueued, "") h.commit(nil, turnChange("created", queued)) @@ -435,8 +435,8 @@ func TestCreationStreamWaitsForPendingProvisioning(t *testing.T) { t.Run(outcome, func(t *testing.T) { h := newCreationStreamHarness(t) // Hosted initial input is idle, without a Turn or public action, while it provisions. - h.commit(func(session *store.Session) { session.PendingInput = true }) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: 10} + h.commit(func(session *sessions.Session) { session.PendingInput = true }) + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: 10} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() if created := expectTypes(t, frames, "agent.session.created")[0].event(t); created.Session.Status != "idle" { @@ -445,12 +445,14 @@ func TestCreationStreamWaitsForPendingProvisioning(t *testing.T) { expectOpen(t, frames) if outcome == "failure" { failed := &sessions.EnvironmentInputActivity{Status: "failed", Failure: "environment_unavailable", LastActiveAt: time.Unix(1700000005, 0)} - h.commit(func(session *store.Session) { session.PendingInput, session.EnvironmentInputActivity = false, failed }, + h.commit(func(session *sessions.Session) { + session.PendingInput, session.EnvironmentInputActivity = false, failed + }, activityChange(failed, true)) expectTypes(t, frames, "agent.session.failed") } else { // A reservation can settle without recording an event. - h.commit(func(session *store.Session) { session.PendingInput = false }) + h.commit(func(session *sessions.Session) { session.PendingInput = false }) } expectEnded(t, frames) }) @@ -461,11 +463,11 @@ func TestCreationStreamEndsWhenSessionIsDeleted(t *testing.T) { h := newCreationStreamHarness(t) queued := h.turn(sessions.TurnQueued, "") h.commit(setTurn(queued)) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: 10} + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: 10} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() expectTypes(t, frames, "agent.session.created") - h.commit(func(*store.Session) { h.fixture.deleted = true }) + h.commit(func(*sessions.Session) { h.fixture.deleted = true }) expectEnded(t, frames) } @@ -497,21 +499,21 @@ func TestCreationStreamStopsBeforeLaterWork(t *testing.T) { t.Run("same batch as the settling idle", func(t *testing.T) { h := newCreationStreamHarness(t) h.commit(setTurn(h.turn(sessions.TurnInProgress, ""))) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() expectTypes(t, frames, "agent.session.created") // The settling idle and B's new Turn commit before the next poll drains them together. completed := h.turn(sessions.TurnCompleted, "") changes := append([]sessions.SessionChange{turnChange("completed", completed), sessionChange("idle", completed, nil)}, later(h)...) - h.commit(func(session *store.Session) { session.LastTurn = changes[2].Turn }, changes...) + h.commit(func(session *sessions.Session) { session.LastTurn = changes[2].Turn }, changes...) expectTypes(t, frames, "agent.session.turn.completed", "agent.session.idle") expectEnded(t, frames) }) t.Run("between the settled snapshot and its drain", func(t *testing.T) { h := newCreationStreamHarness(t) - h.commit(func(session *store.Session) { session.PendingInput = true }) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} + h.commit(func(session *sessions.Session) { session.PendingInput = true }) + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() expectTypes(t, frames, "agent.session.created") @@ -520,14 +522,14 @@ func TestCreationStreamStopsBeforeLaterWork(t *testing.T) { // After an empty drain, the reservation settles without a status event, // alongside an unsent environment event, before the fallback read. h.fixture.beforeSnapshot = func() { - h.commit(func(session *store.Session) { session.PendingInput = false }, + h.commit(func(session *sessions.Session) { session.PendingInput = false }, sessions.SessionChange{Event: v1.SessionEvent{Type: "agent.session.environment.disconnected", EventID: uuid.NewString(), SessionID: h.fixture.session.ID, Environment: &v1.SessionEnvironmentState{ID: "environment", Type: "self_hosted", Status: "disconnected"}}}) } // B's Turn commits after that settled read and before its drain. h.fixture.afterSnapshot = func() { changes := later(h) - h.commit(func(session *store.Session) { session.LastTurn = changes[0].Turn }, changes...) + h.commit(func(session *sessions.Session) { session.LastTurn = changes[0].Turn }, changes...) } h.fixture.mu.Unlock() expectTypes(t, frames, "agent.session.environment.disconnected") @@ -538,18 +540,18 @@ func TestCreationStreamStopsBeforeLaterWork(t *testing.T) { func TestCreationStreamIgnoresConnectionIdle(t *testing.T) { h := newCreationStreamHarness(t) waiting := &sessions.EnvironmentInputActivity{Status: "requires_action", LastActiveAt: time.Unix(1700000002, 0)} - h.commit(func(session *store.Session) { session.PendingInput = true }) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} + h.commit(func(session *sessions.Session) { session.PendingInput = true }) + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() expectTypes(t, frames, "agent.session.created") // A self-hosted connection clears the action; the input is still pending. connected := &sessions.EnvironmentInputActivity{Status: "idle", LastActiveAt: waiting.LastActiveAt} - h.commit(func(session *store.Session) { session.EnvironmentInputActivity = connected }, activityChange(connected, false)) + h.commit(func(session *sessions.Session) { session.EnvironmentInputActivity = connected }, activityChange(connected, false)) expectTypes(t, frames, "agent.session.idle") expectOpen(t, frames) // Its later expiry records no event and ends the stream through the projection. - h.commit(func(session *store.Session) { session.PendingInput = false }) + h.commit(func(session *sessions.Session) { session.PendingInput = false }) expectEnded(t, frames) } @@ -557,7 +559,7 @@ func TestCreationStreamBoundsProjectionReads(t *testing.T) { h := newCreationStreamHarness(t) running := h.turn(sessions.TurnInProgress, "") h.commit(setTurn(running)) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Created: true, Cursor: h.fixture.sequence} frames, cancel := h.open(http.MethodPost, "/v1/agents/sessions", creationBody) defer cancel() expectTypes(t, frames, "agent.session.created") @@ -605,7 +607,7 @@ func TestCreationRetryStreamEndsImmediately(t *testing.T) { }, false}, {"upsert retry", func(h *creationStreamHarness) { h.commit(setTurn(h.turn(sessions.TurnInProgress, ""))) - h.fixture.creation = store.SessionCreation{Session: h.fixture.session, Cursor: 10} + h.fixture.creation = sessions.Creation{Session: h.fixture.session, Cursor: 10} }, true}, } { t.Run(test.name, func(t *testing.T) { @@ -679,7 +681,7 @@ func TestSessionSettledProjection(t *testing.T) { {"requires_action", sessions.TurnWaiting, false, false}, {"requires_action", "", true, false}, } { - session := store.Session{PendingInput: test.pending} + session := sessions.Session{PendingInput: test.pending} if test.turn != "" { session.LastTurn = &sessions.Turn{Status: test.turn} } diff --git a/services/core/internal/api/session_credentials_test.go b/services/core/internal/api/session_credentials_test.go index fde0ac9d7..7401b5d1f 100644 --- a/services/core/internal/api/session_credentials_test.go +++ b/services/core/internal/api/session_credentials_test.go @@ -7,7 +7,7 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" "github.com/google/uuid" ) @@ -49,14 +49,14 @@ func TestSessionVaultTypesAndCreationIntent(t *testing.T) { t.Fatal(err) } input, _ := request.validated() - first, _ := sessionCreationRequest(input, []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"original"}`)}}) + first, _ := sessionCreationRequest(input, []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"original"}`)}}) input.Stream = true - second, _ := sessionCreationRequest(input, []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"original"}`)}}) + second, _ := sessionCreationRequest(input, []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"original"}`)}}) if !reflect.DeepEqual(first, second) { t.Fatal("streaming changed credential-bound creation identity") } input.VaultIDs = []string{"other"} - changed, _ := sessionCreationRequest(input, []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"original"}`)}}) + changed, _ := sessionCreationRequest(input, []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"original"}`)}}) if reflect.DeepEqual(first, changed) { t.Fatal("changed attachment reused caller intent") } @@ -90,7 +90,7 @@ func TestSessionProjectionShowsSelectedMCPCredential(t *testing.T) { // The stored caller intent is checked against PostgreSQL by the storedNull // guard in TestMCPCredentialSelectionPublicPostgres. raw, _ := json.Marshal(cfg) - response, err := sessionResponse(store.Session{Configuration: raw}, "") + response, err := sessionResponse(sessions.Session{Configuration: raw}, "") if err != nil || !reflect.DeepEqual(response.VaultIDs, cfg.VaultIDs) { t.Fatal("public attachments lost", err) } @@ -132,7 +132,7 @@ func TestSessionProjectionShowsSelectedMCPCredential(t *testing.T) { changed.MCPCredentials = append([]vaults.MCPCredentialBinding(nil), cfg.MCPCredentials...) change(&changed) raw, _ := json.Marshal(changed) - response, err := sessionResponse(store.Session{Configuration: raw}, "") + response, err := sessionResponse(sessions.Session{Configuration: raw}, "") if err != nil || string(response.Agent.Tools[0]) != string(changed.Agent.Tools[0]) { t.Fatalf("unattached or unmatched binding was projected: %s, %v", response.Agent.Tools[0], err) } diff --git a/services/core/internal/api/session_deletion.go b/services/core/internal/api/session_deletion.go index 5812d2b45..ce3917733 100644 --- a/services/core/internal/api/session_deletion.go +++ b/services/core/internal/api/session_deletion.go @@ -5,7 +5,7 @@ import ( "net/http" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) @@ -32,7 +32,7 @@ func (h *Handler) deleteSession(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "session_id") parsed, err := uuid.Parse(id) if err != nil || parsed == uuid.Nil { - writeStoreError(w, r, store.ErrNotFound) + writeStoreError(w, r, sessions.ErrNotFound) return } if err := h.Sessions.DeleteSession(r.Context(), tenantID(r), id); err != nil { diff --git a/services/core/internal/api/session_diagnostics.go b/services/core/internal/api/session_diagnostics.go index d8406682d..138534f00 100644 --- a/services/core/internal/api/session_diagnostics.go +++ b/services/core/internal/api/session_diagnostics.go @@ -7,7 +7,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" ) @@ -50,10 +49,10 @@ type TurnDiagnostics struct { // SessionAdmin serves the administrator's per-Session reads: diagnostics // snapshots, the execution configuration and the managed archive state. type SessionAdmin interface { - GetSessionDiagnosticsSnapshot(context.Context, string, string) (store.Session, error) - GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (store.TurnDiagnosticsSnapshot, error) + GetSessionDiagnosticsSnapshot(context.Context, string, string) (sessions.Session, error) + GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (sessions.TurnDiagnosticsSnapshot, error) GetSessionExecutionConfiguration(context.Context, string, string) (v1.SessionExecutionConfiguration, error) - GetManagedSessionArchive(context.Context, string, string) (store.ManagedSessionArchive, error) + GetManagedSessionArchive(context.Context, string, string) (sessions.ManagedArchive, error) } // @Summary Retrieve root Session diagnostics diff --git a/services/core/internal/api/session_diagnostics_deadline_test.go b/services/core/internal/api/session_diagnostics_deadline_test.go index ff3aefa2e..e8fd9445e 100644 --- a/services/core/internal/api/session_diagnostics_deadline_test.go +++ b/services/core/internal/api/session_diagnostics_deadline_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -18,18 +17,18 @@ type diagnosticDeadlineStore struct { observed context.Context } -func (s *diagnosticDeadlineStore) GetSessionDiagnosticsSnapshot(ctx context.Context, tenant, session string) (store.Session, error) { +func (s *diagnosticDeadlineStore) GetSessionDiagnosticsSnapshot(ctx context.Context, tenant, session string) (sessions.Session, error) { s.observed = ctx return s.diagnosticSnapshotStore.GetSessionDiagnosticsSnapshot(ctx, tenant, session) } -func (s *diagnosticDeadlineStore) GetTurnDiagnosticsSnapshot(ctx context.Context, tenant, session, turn string) (store.TurnDiagnosticsSnapshot, error) { +func (s *diagnosticDeadlineStore) GetTurnDiagnosticsSnapshot(ctx context.Context, tenant, session, turn string) (sessions.TurnDiagnosticsSnapshot, error) { s.observed = ctx return s.diagnosticSnapshotStore.GetTurnDiagnosticsSnapshot(ctx, tenant, session, turn) } func TestDiagnosticsReadDeadline(t *testing.T) { id, turn := uuid.NewString(), uuid.NewString() - session := store.Session{ID: id, Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`), LastTurn: &sessions.Turn{ID: turn, SessionID: id, Status: sessions.TurnCompleted}} + session := sessions.Session{ID: id, Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`), LastTurn: &sessions.Turn{ID: turn, SessionID: id, Status: sessions.TurnCompleted}} for _, suffix := range []string{"/diagnostics", "/turns/" + turn + "/diagnostics"} { for _, shorter := range []bool{false, true} { t.Run(suffix+map[bool]string{false: "/server", true: "/caller"}[shorter], func(t *testing.T) { diff --git a/services/core/internal/api/session_diagnostics_public_compat_test.go b/services/core/internal/api/session_diagnostics_public_compat_test.go index eef777ae1..b2e4acef6 100644 --- a/services/core/internal/api/session_diagnostics_public_compat_test.go +++ b/services/core/internal/api/session_diagnostics_public_compat_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/MiniMax-AI/OpenAgentCore/services/core/migrations" "github.com/google/uuid" @@ -24,7 +25,7 @@ func TestDiagnosticPublicCompatibility(t *testing.T) { fakes.projectsReader.resolveAPIKey = projectKeys(t, key).ResolveAPIKey databaseSessionReads(s)(&deps, fakes) h := newTestHandler(t, deps) - session, err := s.CreateSession(t.Context(), key.TenantID, store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "compat-test"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) + session, err := s.CreateSession(t.Context(), key.TenantID, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "compat-test"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) if err != nil { t.Fatal(err) } @@ -61,7 +62,7 @@ func diagnosticRequest(handler http.Handler, path, token string) *httptest.Respo // databaseSessionReads serves Session, Turn, Item and diagnostic reads from s. func databaseSessionReads(s *store.Store) func(*Dependencies, *testFakes) { - return func(d *Dependencies, _ *testFakes) { d.Sessions, d.SessionHistory, d.SessionAdmin = s, s, s } + return func(d *Dependencies, _ *testFakes) { d.Sessions, d.Turns, d.Items, d.SessionAdmin = s, s, s, s } } func diagnosticDatabase(t *testing.T) (*store.Store, *pgxpool.Pool) { diff --git a/services/core/internal/api/session_diagnostics_test.go b/services/core/internal/api/session_diagnostics_test.go index d699d0169..98bdf78a4 100644 --- a/services/core/internal/api/session_diagnostics_test.go +++ b/services/core/internal/api/session_diagnostics_test.go @@ -9,14 +9,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) func TestDiagnosticsCoreHandlerDatabaseBoundary(t *testing.T) { s, pool := diagnosticDatabase(t) h, _, tenant := adminTestHandler(t, databaseSessionReads(s)) - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "diagnostic-test"}, Engine: "codex", IdempotencyKey: "diagnostics", Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "diagnostic-test"}, Engine: "codex", IdempotencyKey: "diagnostics", Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) if err != nil { t.Fatal(err) } @@ -24,10 +23,10 @@ func TestDiagnosticsCoreHandlerDatabaseBoundary(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"device_disconnected","error":"Bearer raw-secret-canary https://private.example/key","done":{"native_id":"secret-native-canary"}}`)}); err != nil { + if _, err = s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"device_disconnected","error":"Bearer raw-secret-canary https://private.example/key","done":{"native_id":"secret-native-canary"}}`)}); err != nil { t.Fatal(err) } base := adminSessionsPath + session.ID @@ -63,20 +62,20 @@ func TestDiagnosticsCoreHandlerDatabaseBoundary(t *testing.T) { } type diagnosticSnapshotStore struct { - session store.Session + session sessions.Session } -func (s diagnosticSnapshotStore) GetSessionDiagnosticsSnapshot(context.Context, string, string) (store.Session, error) { +func (s diagnosticSnapshotStore) GetSessionDiagnosticsSnapshot(context.Context, string, string) (sessions.Session, error) { return s.session, nil } -func (s diagnosticSnapshotStore) GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (store.TurnDiagnosticsSnapshot, error) { - return store.TurnDiagnosticsSnapshot{Session: s.session, Turn: *s.session.LastTurn, Items: []store.ItemDiagnosticTiming{}}, nil +func (s diagnosticSnapshotStore) GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (sessions.TurnDiagnosticsSnapshot, error) { + return sessions.TurnDiagnosticsSnapshot{Session: s.session, Turn: *s.session.LastTurn, Items: []sessions.ItemDiagnosticTiming{}}, nil } // diagnosticSnapshots answers Core diagnostic reads. type diagnosticSnapshots interface { - GetSessionDiagnosticsSnapshot(context.Context, string, string) (store.Session, error) - GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (store.TurnDiagnosticsSnapshot, error) + GetSessionDiagnosticsSnapshot(context.Context, string, string) (sessions.Session, error) + GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (sessions.TurnDiagnosticsSnapshot, error) } // serveDiagnostics answers Session and Turn diagnostic reads from source. @@ -88,7 +87,7 @@ func serveDiagnostics(source diagnosticSnapshots) func(*Dependencies, *testFakes func TestDiagnosticsFailurePrecedenceAndUnknownTime(t *testing.T) { id, turnID := uuid.NewString(), uuid.NewString() - base := store.Session{ID: id, Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`), LastTurn: &sessions.Turn{ID: turnID, SessionID: id, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"engine_failed","error":"secret-canary"}`)}} + base := sessions.Session{ID: id, Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`), LastTurn: &sessions.Turn{ID: turnID, SessionID: id, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"engine_failed","error":"secret-canary"}`)}} for _, tc := range []struct { activity *sessions.EnvironmentInputActivity code, source string diff --git a/services/core/internal/api/session_environment_http_test.go b/services/core/internal/api/session_environment_http_test.go index 830de963a..9d93c79cd 100644 --- a/services/core/internal/api/session_environment_http_test.go +++ b/services/core/internal/api/session_environment_http_test.go @@ -13,20 +13,19 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) type environmentHTTPFixture struct{ streamFixture } -func (f *environmentHTTPFixture) ListSessions(_ context.Context, tenant, _ string, _ int, _ bool, _ *string) (store.SessionPage, error) { +func (f *environmentHTTPFixture) ListSessions(_ context.Context, tenant, _ string, _ int, _ bool, _ *string) (sessions.Page, error) { if tenant != f.session.TenantID { - return store.SessionPage{}, store.ErrNotFound + return sessions.Page{}, sessions.ErrNotFound } - return store.SessionPage{Sessions: []store.Session{f.session}}, nil + return sessions.Page{Sessions: []sessions.Session{f.session}}, nil } -func (f *environmentHTTPFixture) UpdateSessionMetadata(ctx context.Context, tenant, session string, metadata map[string]string) (store.Session, error) { +func (f *environmentHTTPFixture) UpdateSessionMetadata(ctx context.Context, tenant, session string, metadata map[string]string) (sessions.Session, error) { value, err := f.GetSession(ctx, tenant, session) value.Metadata = metadata return value, err diff --git a/services/core/internal/api/session_environment_test.go b/services/core/internal/api/session_environment_test.go index 2f4634e29..a148bf4ac 100644 --- a/services/core/internal/api/session_environment_test.go +++ b/services/core/internal/api/session_environment_test.go @@ -8,16 +8,15 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) const environmentOrigin = "wss://core.example/api/v1/agent-daemon/ws" -func environmentSession() store.Session { - return store.Session{ +func environmentSession() sessions.Session { + return sessions.Session{ ID: "session", TenantID: "tenant", CreatedAt: time.Unix(1700000000, 0), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"self_hosted"}}`), - Environment: &store.Environment{ + Environment: &sessions.Environment{ ID: "environment", SessionID: "session", TenantID: "tenant", Status: "pending", Configuration: json.RawMessage(`{"type":"self_hosted","workspace_directory":"/remote/workspace","capability_directories":["/remote/capabilities"],"id":"forged","remote_url":"https://secret@private","env":{"SECRET":"private"},"setup_commands":["private"]}`), }, @@ -49,13 +48,13 @@ func TestSessionEnvironmentUsesSafeStoredAssociation(t *testing.T) { if err != nil || response.Environment.CapabilityDirectories == nil || *response.Environment.CapabilityDirectories == nil { t.Fatal("missing capability paths must project as an empty array", response, err) } - for _, change := range []func(*store.Session){ - func(s *store.Session) { s.Environment = nil }, - func(s *store.Session) { s.Environment.ID = "" }, - func(s *store.Session) { s.Environment.TenantID = "foreign" }, - func(s *store.Session) { s.Environment.SessionID = "other" }, - func(s *store.Session) { s.Environment.Configuration = json.RawMessage(`{"type":"self_hosted"}`) }, - func(s *store.Session) { + for _, change := range []func(*sessions.Session){ + func(s *sessions.Session) { s.Environment = nil }, + func(s *sessions.Session) { s.Environment.ID = "" }, + func(s *sessions.Session) { s.Environment.TenantID = "foreign" }, + func(s *sessions.Session) { s.Environment.SessionID = "other" }, + func(s *sessions.Session) { s.Environment.Configuration = json.RawMessage(`{"type":"self_hosted"}`) }, + func(s *sessions.Session) { s.Configuration = json.RawMessage(`{"agent":{"id":"agent","model":"model"},"environment":{"type":"openai_hosted"}}`) }, } { diff --git a/services/core/internal/api/session_execution_configuration_test.go b/services/core/internal/api/session_execution_configuration_test.go index 6d590a9cf..6294695e4 100644 --- a/services/core/internal/api/session_execution_configuration_test.go +++ b/services/core/internal/api/session_execution_configuration_test.go @@ -9,7 +9,7 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestExecutionConfigurationSources(t *testing.T) { @@ -74,7 +74,7 @@ func TestExecutionConfigurationReadBoundary(t *testing.T) { err error status int }{ - {"", nil, 401}, {"Bearer admin", nil, 200}, {"Bearer admin", store.ErrNotFound, 404}, + {"", nil, 401}, {"Bearer admin", nil, 200}, {"Bearer admin", sessions.ErrNotFound, 404}, } { s.err = tc.err before := s.calls diff --git a/services/core/internal/api/session_initial_input.go b/services/core/internal/api/session_initial_input.go index 4320345b6..40f3fd8e5 100644 --- a/services/core/internal/api/session_initial_input.go +++ b/services/core/internal/api/session_initial_input.go @@ -5,10 +5,10 @@ import ( "encoding/json" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -func initialSessionInputs(raw json.RawMessage) ([]store.Input, error) { +func initialSessionInputs(raw json.RawMessage) ([]sessions.Input, error) { raw = bytes.TrimSpace(raw) if len(raw) == 0 || bytes.Equal(raw, []byte("null")) { return nil, nil @@ -16,7 +16,7 @@ func initialSessionInputs(raw json.RawMessage) ([]store.Input, error) { if raw[0] == '"' { var text string if err := json.Unmarshal(raw, &text); err != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } raw, _ = json.Marshal([]v1.InputMessage{{Role: "user", Content: []v1.InputContent{{Type: "input_text", Text: &text}}}}) } @@ -26,7 +26,7 @@ func initialSessionInputs(raw json.RawMessage) ([]store.Input, error) { Input json.RawMessage `json:"input"` }{Type: "agent.session.input.message", Input: raw}) if err != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } return executionInputs([]json.RawMessage{event}) } diff --git a/services/core/internal/api/session_initial_input_test.go b/services/core/internal/api/session_initial_input_test.go index 988cb49fa..58f716c3a 100644 --- a/services/core/internal/api/session_initial_input_test.go +++ b/services/core/internal/api/session_initial_input_test.go @@ -14,7 +14,7 @@ import ( func admitInto(s *recordingStore) func(*Dependencies, *testFakes) { return func(d *Dependencies, f *testFakes) { d.Execution = f.execution() - f.admission.createSession = s.CreateSession + f.sessionAdmission.createSession = s.CreateSession } } diff --git a/services/core/internal/api/session_request.go b/services/core/internal/api/session_request.go index 28a3da043..d84edd4dc 100644 --- a/services/core/internal/api/session_request.go +++ b/services/core/internal/api/session_request.go @@ -7,7 +7,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // Keep explicit null until validation for fields whose Go zero values would @@ -44,7 +44,7 @@ func (request decodedSessionRequest) validated() (sessionRequest, error) { input := sessionRequest{CreateSessionRequest: request.CreateSessionRequest, Input: request.Input} if len(request.Execution) > 0 && !bytes.Equal(bytes.TrimSpace(request.Execution), []byte("null")) { if decodeInputObject(request.Execution, &input.XAgentsCore, "model_provider", "harness_config", "environment") != nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } var fields map[string]json.RawMessage _ = json.Unmarshal(request.Execution, &fields) @@ -52,12 +52,12 @@ func (request decodedSessionRequest) validated() (sessionRequest, error) { } var vaultIDs []*string if len(request.VaultIDs) != 0 && json.Unmarshal(request.VaultIDs, &vaultIDs) != nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } input.VaultIDs = make([]string, 0, len(vaultIDs)) for _, id := range vaultIDs { if id == nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } input.VaultIDs = append(input.VaultIDs, *id) } @@ -68,7 +68,7 @@ func (request decodedSessionRequest) validated() (sessionRequest, error) { input.originalEnvironment = request.Environment var environmentFields map[string]json.RawMessage if json.Unmarshal(canonicalEnvironment, &environmentFields) != nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } input.initialization, err = decodeEnvironmentSetup(environmentFields) if err != nil { @@ -88,25 +88,25 @@ func (request decodedSessionRequest) validated() (sessionRequest, error) { return input, err } if decodeInputObject(request.Agent, &input.Agent, "model", "instructions", "multi_agent", "reasoning", "service_tier", "text", "tools", "x_agents_core") != nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } if err := json.Unmarshal(request.Agent, &input.agentFields); err != nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } if _, supplied := input.agentFields["model"]; supplied && input.Agent.Model == nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } } if len(request.AgentID) > 0 { var id string if bytes.Equal(bytes.TrimSpace(request.AgentID), []byte("null")) || json.Unmarshal(request.AgentID, &id) != nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } input.AgentID = &id } if len(request.Stream) > 0 { if bytes.Equal(bytes.TrimSpace(request.Stream), []byte("null")) || json.Unmarshal(request.Stream, &input.Stream) != nil { - return input, store.ErrInvalidInput + return input, sessions.ErrInvalidInput } } input.Metadata, err = stringMetadata(request.Metadata) diff --git a/services/core/internal/api/session_response.go b/services/core/internal/api/session_response.go index 2a48c95d3..58e8f2911 100644 --- a/services/core/internal/api/session_response.go +++ b/services/core/internal/api/session_response.go @@ -6,10 +6,9 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) -func sessionResponse(session store.Session, executorURL string) (v1.Session, error) { +func sessionResponse(session sessions.Session, executorURL string) (v1.Session, error) { var cfg configuration if err := json.Unmarshal(session.Configuration, &cfg); err != nil || cfg.Agent.ID == "" || cfg.Agent.Model == "" { return v1.Session{}, errors.New("unsupported stored session configuration") @@ -107,7 +106,7 @@ func sessionResponse(session store.Session, executorURL string) (v1.Session, err return response, nil } -func sessionEnvironment(session store.Session, kind, executorURL string) (v1.SessionEnvironment, error) { +func sessionEnvironment(session sessions.Session, kind, executorURL string) (v1.SessionEnvironment, error) { if kind == "none" { return v1.SessionEnvironment{Type: "none"}, nil } diff --git a/services/core/internal/api/session_semantics_test.go b/services/core/internal/api/session_semantics_test.go index 43841f7f4..fe5809db0 100644 --- a/services/core/internal/api/session_semantics_test.go +++ b/services/core/internal/api/session_semantics_test.go @@ -10,7 +10,7 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type emptyEventSessionStore struct { @@ -18,13 +18,13 @@ type emptyEventSessionStore struct { reads int } -func (s *emptyEventSessionStore) GetSession(_ context.Context, tenant, id string) (store.Session, error) { +func (s *emptyEventSessionStore) GetSession(_ context.Context, tenant, id string) (sessions.Session, error) { s.tenant, s.id = tenant, id s.reads++ if id != "owned" { - return store.Session{}, store.ErrNotFound + return sessions.Session{}, sessions.ErrNotFound } - return store.Session{ID: id, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"}}`)}, nil + return sessions.Session{ID: id, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"}}`)}, nil } func (s *emptyEventSessionStore) AuditSessionOperation(context.Context, string, string, string) error { diff --git a/services/core/internal/api/session_template.go b/services/core/internal/api/session_template.go index 7e7edbf73..4333b7f36 100644 --- a/services/core/internal/api/session_template.go +++ b/services/core/internal/api/session_template.go @@ -10,7 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // Validate the reference and inline shape without looking up mutable resources. @@ -22,7 +22,7 @@ func decodeTemplateEnvironment(raw json.RawMessage) (*v1.Environment, string, js func decodePreparationTemplate(raw json.RawMessage, extension bool) (*v1.Environment, string, json.RawMessage, error) { var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil { - return nil, "", nil, store.ErrInvalidInput + return nil, "", nil, sessions.ErrInvalidInput } reference, supplied := fields["environment_template_id"] if !supplied { @@ -36,7 +36,7 @@ func decodePreparationTemplate(raw json.RawMessage, extension bool) (*v1.Environ } var id, kind string if json.Unmarshal(reference, &id) != nil || id == "" || json.Unmarshal(fields["type"], &kind) != nil || (kind != "openai_hosted" && !(extension && kind == "self_hosted")) { - return nil, "", nil, store.ErrInvalidInput + return nil, "", nil, sessions.ErrInvalidInput } delete(fields, "environment_template_id") inline, err := json.Marshal(fields) @@ -55,7 +55,7 @@ func applyTemplateEnvironment(input *sessionRequest, resolved environmenttemplat template, files := resolved.Template, resolved.Files var fields map[string]json.RawMessage if json.Unmarshal(input.templateEnvironment, &fields) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } if input.Environment.Type == "self_hosted" && template.NetworkAccess != "enabled" { return &fieldError{param: "x_agents_core.environment.environment_template_id", message: "This template requires a managed network policy; user-managed machines do not enforce it."} @@ -65,7 +65,7 @@ func applyTemplateEnvironment(input *sessionRequest, resolved environmenttemplat } effective := agentnetwork.Policy{Access: input.Environment.Network.Access, AllowedDomains: input.Environment.Network.AllowedDomains} if !effective.Narrows(agentnetwork.Policy{Access: template.NetworkAccess, AllowedDomains: template.AllowedDomains}) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } skills := input.initialization.Skills if !templateFieldOverride(fields, "skills") { @@ -98,7 +98,7 @@ func applyTemplateEnvironment(input *sessionRequest, resolved environmenttemplat var managers map[string]json.RawMessage if templateFieldOverride(fields, "packages") { if json.Unmarshal(fields["packages"], &managers) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } } if templateFieldOverride(managers, "npm") { diff --git a/services/core/internal/api/session_write_audit_test.go b/services/core/internal/api/session_write_audit_test.go index 13002c6fe..cd0987761 100644 --- a/services/core/internal/api/session_write_audit_test.go +++ b/services/core/internal/api/session_write_audit_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -23,13 +23,13 @@ type auditedSessionFixture struct { source writeaudit.Source } -func (f *auditedSessionFixture) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) { - return store.SessionCreation{Session: f.session}, nil +func (f *auditedSessionFixture) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (sessions.Creation, error) { + return sessions.Creation{Session: f.session}, nil } func (f *auditedSessionFixture) AuditSessionOperation(ctx context.Context, tenant, session, action string) error { if tenant != f.session.TenantID || session != f.session.ID { - return store.ErrNotFound + return sessions.ErrNotFound } f.source, _ = writeaudit.FromContext(ctx) f.actions = append(f.actions, action) @@ -41,7 +41,7 @@ func TestSessionAuditOnlyRoutesFailClosed(t *testing.T) { for _, fail := range []bool{false, true} { t.Run(route+map[bool]string{false: "/commit", true: "/rollback"}[fail], func(t *testing.T) { tenant, session := uuid.NewString(), uuid.NewString() - f := &auditedSessionFixture{streamFixture: streamFixture{session: store.Session{ID: session, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"},"agent":{"id":"agent","model":"test"}}`)}}} + f := &auditedSessionFixture{streamFixture: streamFixture{session: sessions.Session{ID: session, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"},"agent":{"id":"agent","model":"test"}}`)}}} if fail { f.err = errors.New("audit unavailable") } @@ -51,7 +51,7 @@ func TestSessionAuditOnlyRoutesFailClosed(t *testing.T) { fakes.sessions.getSession = f.GetSession } if route != "empty-events" { - fakes.sessions.findSessionCreation = f.FindSessionCreation + fakes.sessionCreation.findSessionCreation = f.FindSessionCreation } h := &Handler{Dependencies: deps} ctx := context.WithValue(t.Context(), principalContextKey{}, identity.Principal{ProjectScope: identity.ProjectScope{TenantID: tenant}, SubjectKind: "service_account", SubjectID: "test"}) diff --git a/services/core/internal/api/source_files_errors_test.go b/services/core/internal/api/source_files_errors_test.go index 3dc7d8eed..17b19e01b 100644 --- a/services/core/internal/api/source_files_errors_test.go +++ b/services/core/internal/api/source_files_errors_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestSourceFileMissingErrorParameters(t *testing.T) { @@ -40,28 +39,6 @@ func TestSourceFileMissingErrorParameters(t *testing.T) { } } -func TestStoreErrorOptionalParameterPreservesOtherErrors(t *testing.T) { - for _, tc := range []struct { - path string - err error - status int - code any - param []string - }{ - {"/v1/agents/agent_missing", store.ErrNotFound, 404, "not_found_error", nil}, - } { - w := httptest.NewRecorder() - writeStoreError(w, httptest.NewRequest(http.MethodGet, tc.path, nil), tc.err, tc.param...) - var body map[string]map[string]any - if w.Code != tc.status || json.Unmarshal(w.Body.Bytes(), &body) != nil { - t.Fatalf("unexpected error: %d %s", w.Code, w.Body.String()) - } - if body["error"]["code"] != tc.code || body["error"]["param"] != nil { - t.Fatalf("unrelated error changed: %s", w.Body.String()) - } - } -} - func TestFilesErrorOptionalParameterPreservesOtherErrors(t *testing.T) { for _, tc := range []struct { path string diff --git a/services/core/internal/api/stream.go b/services/core/internal/api/stream.go index 5296a605d..1168ee433 100644 --- a/services/core/internal/api/stream.go +++ b/services/core/internal/api/stream.go @@ -12,7 +12,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) @@ -22,7 +21,7 @@ import ( type SessionEvents interface { SessionEventCursor(context.Context, string, string) (int64, error) ListSessionEvents(context.Context, string, string, int64) ([]sessions.SessionChange, error) - SessionStreamSnapshot(context.Context, string, string) (store.Session, int64, error) + SessionStreamSnapshot(context.Context, string, string) (sessions.Session, int64, error) } // @Summary Stream live Session events @@ -71,7 +70,7 @@ type streamSettlement func(context.Context) (settled bool, cursor int64, err err // sends only events up to that cursor before ending. Later work drained before // that read can still be sent. The projection is re-read after a sent Session // status event and otherwise at most once a second. -func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, session store.Session, cursor int64, initial *v1.SessionEvent, status int, settlement streamSettlement) { +func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, session sessions.Session, cursor int64, initial *v1.SessionEvent, status int, settlement streamSettlement) { id, tenant := session.ID, tenantID(r) write := openEventStream(w, status) if write == nil { @@ -114,7 +113,7 @@ func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, ses return } changes, err := h.SessionEvents.ListSessionEvents(r.Context(), tenant, id, cursor) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return } if err != nil { @@ -148,7 +147,7 @@ func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, ses if settlement != nil && (recheck || time.Since(checked) >= time.Second) { recheck, checked = false, time.Now() settled, snapshot, err := settlement(r.Context()) - if errors.Is(err, store.ErrNotFound) || r.Context().Err() != nil { + if errors.Is(err, sessions.ErrNotFound) || r.Context().Err() != nil { return } if err != nil { @@ -235,7 +234,7 @@ func sessionStatusEvent(eventType string) bool { return false } -func streamResponse(session store.Session, change sessions.SessionChange, executorURL string) (v1.SessionEvent, error) { +func streamResponse(session sessions.Session, change sessions.SessionChange, executorURL string) (v1.SessionEvent, error) { event := change.Event if change.Turn == nil && change.EnvironmentInputActivity == nil && change.EnvironmentFailure == nil { return withTurnUsage(event), nil diff --git a/services/core/internal/api/stream_authority_test.go b/services/core/internal/api/stream_authority_test.go index b20327df4..701ecd547 100644 --- a/services/core/internal/api/stream_authority_test.go +++ b/services/core/internal/api/stream_authority_test.go @@ -16,7 +16,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -51,7 +50,7 @@ func TestBusyStreamRechecksAuthorityAndFailsClosed(t *testing.T) { key := callerBinding() key.TokenSHA256 = runtimedevice.HashCredential("stream") resolver := &streamAuthorityResolver{keys: projectKeys(t, key)} - f := &busyAuthorityStream{streamFixture: &streamFixture{session: store.Session{ID: uuid.NewString(), TenantID: key.TenantID, CreatedAt: time.Now(), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_fixture","model":"fixture","tools":[]},"environment":{"type":"none"}}`)}}} + f := &busyAuthorityStream{streamFixture: &streamFixture{session: sessions.Session{ID: uuid.NewString(), TenantID: key.TenantID, CreatedAt: time.Now(), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_fixture","model":"fixture","tools":[]},"environment":{"type":"none"}}`)}}} deps, fakes := testDependencies(t) fakes.projectsReader.resolveAPIKey = resolver.ResolveAPIKey f.serve(fakes) diff --git a/services/core/internal/api/stream_test.go b/services/core/internal/api/stream_test.go index d4f83ab8e..a82b707f3 100644 --- a/services/core/internal/api/stream_test.go +++ b/services/core/internal/api/stream_test.go @@ -15,22 +15,21 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) // streamFixture serves one Session and its event stream. type streamFixture struct { - session store.Session + session sessions.Session mu sync.Mutex changes []sessions.SessionChange gap bool cursors []int64 } -func (f *streamFixture) GetSession(_ context.Context, tenant, id string) (store.Session, error) { +func (f *streamFixture) GetSession(_ context.Context, tenant, id string) (sessions.Session, error) { if tenant != f.session.TenantID || id != f.session.ID { - return store.Session{}, store.ErrNotFound + return sessions.Session{}, sessions.ErrNotFound } return f.session, nil } @@ -39,11 +38,11 @@ func (f *streamFixture) SessionEventCursor(context.Context, string, string) (int return 10, nil } -func (f *streamFixture) SessionStreamSnapshot(_ context.Context, tenant, id string) (store.Session, int64, error) { +func (f *streamFixture) SessionStreamSnapshot(_ context.Context, tenant, id string) (sessions.Session, int64, error) { f.mu.Lock() defer f.mu.Unlock() if tenant != f.session.TenantID || id != f.session.ID { - return store.Session{}, 0, store.ErrNotFound + return sessions.Session{}, 0, sessions.ErrNotFound } return f.session, 10, nil } @@ -53,7 +52,7 @@ func (f *streamFixture) ListSessionEvents(_ context.Context, _, _ string, cursor defer f.mu.Unlock() f.cursors = append(f.cursors, cursor) if f.gap { - return nil, store.ErrStreamGap + return nil, sessions.ErrStreamGap } changes := f.changes f.changes = nil @@ -67,7 +66,7 @@ func (f *streamFixture) serve(fakes *testFakes) { } func TestLiveStreamAuthDisconnectRecoveryAndServerDeadline(t *testing.T) { - f := &streamFixture{session: store.Session{ID: uuid.NewString(), TenantID: uuid.NewString(), CreatedAt: time.Now(), Metadata: map[string]string{}, + f := &streamFixture{session: sessions.Session{ID: uuid.NewString(), TenantID: uuid.NewString(), CreatedAt: time.Now(), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"none"}}`)}} deps, fakes := testDependencies(t) fakes.projectsReader.resolveAPIKey = projectKeys(t, APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("key"), TenantID: f.session.TenantID}, APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("foreign"), TenantID: uuid.NewString()}).ResolveAPIKey @@ -153,7 +152,7 @@ func TestLiveStreamAuthDisconnectRecoveryAndServerDeadline(t *testing.T) { } func TestTerminalTurnEventsMirrorTurnUsage(t *testing.T) { - session := store.Session{ID: "session", Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"none"}}`)} + session := sessions.Session{ID: "session", Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"none"}}`)} measured := json.RawMessage(`{"input_tokens":7,"input_tokens_details":{"cached_tokens":2},"output_tokens":3,"output_tokens_details":{"reasoning_tokens":1},"total_tokens":10}`) child := &v1.Turn{ID: "child", Status: "cancelled"} for _, test := range []struct { @@ -195,7 +194,7 @@ func TestTerminalTurnEventsMirrorTurnUsage(t *testing.T) { // Item events carry output_index, null for input Items, and Session snapshots // carry both reasoning keys (EVT-09, SES-23). func TestStreamEventsCarryExplicitNullFields(t *testing.T) { - session := store.Session{ID: "session", Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[],"reasoning":{}},"environment":{"type":"none"}}`)} + session := sessions.Session{ID: "session", Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[],"reasoning":{}},"environment":{"type":"none"}}`)} text := "question" user := &v1.Item{ID: "item", TurnID: "turn", Type: "message", Status: "completed", Role: "user", Content: []v1.ItemContent{{Type: "input_text", Text: &text}}} result := &v1.Item{ID: "result", TurnID: "turn", Type: "function_call_output", Status: "completed", CallID: "call", Output: "value"} diff --git a/services/core/internal/api/subagents_test.go b/services/core/internal/api/subagents_test.go index 346a6fb61..fd7b6cd3a 100644 --- a/services/core/internal/api/subagents_test.go +++ b/services/core/internal/api/subagents_test.go @@ -10,7 +10,7 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type subagentReadStore struct { @@ -233,8 +233,8 @@ func TestSubagentRoutesUseExistingAuthenticationAndErrors(t *testing.T) { err error status int }{ - {store.ErrNotFound, 404}, - {store.ErrInvalidInput, 400}, + {sessions.ErrNotFound, 404}, + {sessions.ErrInvalidInput, 400}, {errors.New("SECRET native failure"), 500}, } { s.err = tc.err diff --git a/services/core/internal/api/tool_search_response_test.go b/services/core/internal/api/tool_search_response_test.go index 1f836640f..2ecf88e27 100644 --- a/services/core/internal/api/tool_search_response_test.go +++ b/services/core/internal/api/tool_search_response_test.go @@ -5,12 +5,12 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestDiscoveryResourceProjectionRetainsExecutionConfiguration(t *testing.T) { raw := json.RawMessage(`{"agent":{"id":"agent","model":"model","tools":[{"type":"tool_search"},{"type":"function","name":"lookup","description":"Lookup","parameters":{"type":"object"},"defer_loading":true}]},"environment":{"type":"none"}}`) - session := store.Session{ID: "session", Configuration: raw} + session := sessions.Session{ID: "session", Configuration: raw} resource, err := sessionResponse(session, "") if err != nil || len(resource.Agent.Tools) != 1 || !strings.Contains(string(resource.Agent.Tools[0]), `"defer_loading":true`) { t.Fatal(resource.Agent.Tools, err) diff --git a/services/core/internal/api/turns.go b/services/core/internal/api/turns.go index c970c9563..7da89680a 100644 --- a/services/core/internal/api/turns.go +++ b/services/core/internal/api/turns.go @@ -9,15 +9,13 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" ) -// SessionHistory reads a Session's root Turns and Items. -type SessionHistory interface { +// Turns reads a Session's root Turns. +type Turns interface { GetTurn(context.Context, string, string, string) (sessions.Turn, error) - ListTurns(context.Context, string, string, string, int, bool) (store.TurnPage, error) - ListItems(context.Context, string, string, string, int, bool) (store.ItemPage, error) + ListTurns(context.Context, string, string, string, int, bool) (sessions.TurnPage, error) } // @Summary Retrieve an execution Turn @@ -33,7 +31,7 @@ type SessionHistory interface { // @Router /agents/sessions/{session_id}/turns/{turn_id} [get] func (h *Handler) getTurn(w http.ResponseWriter, r *http.Request) { sessionID := chi.URLParam(r, "session_id") - turn, err := h.SessionHistory.GetTurn(r.Context(), tenantID(r), sessionID, chi.URLParam(r, "turn_id")) + turn, err := h.Turns.GetTurn(r.Context(), tenantID(r), sessionID, chi.URLParam(r, "turn_id")) if err != nil { writeStoreError(w, r, err) return @@ -75,7 +73,7 @@ func (h *Handler) listTurns(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, err) return } - page, err := h.SessionHistory.ListTurns(r.Context(), tenantID(r), sessionID, options.after, options.limit, options.ascending) + page, err := h.Turns.ListTurns(r.Context(), tenantID(r), sessionID, options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return @@ -92,7 +90,7 @@ func (h *Handler) listTurns(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, turnListResponse(response.Data, response.HasMore)) } -func turnResponse(session store.Session, turn sessions.Turn) (v1.Turn, error) { +func turnResponse(session sessions.Session, turn sessions.Turn) (v1.Turn, error) { var cfg configuration if err := json.Unmarshal(session.Configuration, &cfg); err != nil || cfg.Agent.ID == "" { return v1.Turn{}, errors.New("missing stored agent identity") diff --git a/services/core/internal/api/turns_test.go b/services/core/internal/api/turns_test.go index a06609817..47acb988a 100644 --- a/services/core/internal/api/turns_test.go +++ b/services/core/internal/api/turns_test.go @@ -11,18 +11,17 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) type turnReadStore struct { tenant, sessionID, turnID, cursor string limit int ascending bool - session store.Session + session sessions.Session turn sessions.Turn } -func (s *turnReadStore) GetSession(_ context.Context, tenant, id string) (store.Session, error) { +func (s *turnReadStore) GetSession(_ context.Context, tenant, id string) (sessions.Session, error) { s.tenant, s.sessionID = tenant, id return s.session, nil } @@ -30,15 +29,15 @@ func (s *turnReadStore) GetTurn(_ context.Context, tenant, session, id string) ( s.tenant, s.sessionID, s.turnID = tenant, session, id return s.turn, nil } -func (s *turnReadStore) ListTurns(_ context.Context, tenant, session, cursor string, limit int, asc bool) (store.TurnPage, error) { +func (s *turnReadStore) ListTurns(_ context.Context, tenant, session, cursor string, limit int, asc bool) (sessions.TurnPage, error) { s.tenant, s.sessionID, s.cursor, s.limit, s.ascending = tenant, session, cursor, limit, asc - return store.TurnPage{Turns: []sessions.Turn{s.turn}, NextCursor: s.turn.ID}, nil + return sessions.TurnPage{Turns: []sessions.Turn{s.turn}, NextCursor: s.turn.ID}, nil } func TestTurnRoutesUseAuthenticatedScopeAndSafeProjection(t *testing.T) { - s := &turnReadStore{session: store.Session{Configuration: json.RawMessage(`{"agent":{"id":"agent_snapshot"}}`)}, turn: sessions.Turn{ID: "turn", SessionID: "session", Status: sessions.TurnFailed, CreatedAt: time.Unix(1700000000, 999), Outcome: json.RawMessage(`{"error":"Bearer SECRET","done":{"metadata":{"password":"SECRET"}}}`)}} + s := &turnReadStore{session: sessions.Session{Configuration: json.RawMessage(`{"agent":{"id":"agent_snapshot"}}`)}, turn: sessions.Turn{ID: "turn", SessionID: "session", Status: sessions.TurnFailed, CreatedAt: time.Unix(1700000000, 999), Outcome: json.RawMessage(`{"error":"Bearer SECRET","done":{"metadata":{"password":"SECRET"}}}`)}} h, _, tenant := testHandler(t, func(_ *Dependencies, f *testFakes) { - f.sessions.getSession, f.sessionHistory.getTurn, f.sessionHistory.listTurns = s.GetSession, s.GetTurn, s.ListTurns + f.sessions.getSession, f.turns.getTurn, f.turns.listTurns = s.GetSession, s.GetTurn, s.ListTurns }) request := func(path string) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodGet, path, nil) @@ -84,7 +83,7 @@ func TestTurnRoutesUseAuthenticatedScopeAndSafeProjection(t *testing.T) { } func TestTurnProjectionPreservesLifecycle(t *testing.T) { - session := store.Session{Configuration: json.RawMessage(`{"agent":{"id":"agent_snapshot"}}`)} + session := sessions.Session{Configuration: json.RawMessage(`{"agent":{"id":"agent_snapshot"}}`)} for _, status := range []string{sessions.TurnQueued, sessions.TurnInProgress, sessions.TurnWaiting, sessions.TurnCompleted, sessions.TurnFailed, sessions.TurnCancelled} { turn := sessions.Turn{Status: status, CreatedAt: time.Unix(1700000000, 0), StartedAt: time.Unix(1700000001, 0), CompletedAt: time.Unix(1700000002, 0)} got, err := turnResponse(session, turn) diff --git a/services/core/internal/api/validation_errors_test.go b/services/core/internal/api/validation_errors_test.go index 7b929bc0f..2820e6e24 100644 --- a/services/core/internal/api/validation_errors_test.go +++ b/services/core/internal/api/validation_errors_test.go @@ -13,7 +13,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/agents" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgconn" @@ -40,14 +40,14 @@ func (s *validationStore) CreateVault(_ context.Context, input vaults.CreateVaul return vaults.Vault{ID: uuid.NewString(), TenantID: input.TenantID, Name: input.Name, Metadata: input.Metadata}, nil } -func (s *validationStore) UpdateSessionMetadata(_ context.Context, tenant, id string, metadata map[string]string) (store.Session, error) { +func (s *validationStore) UpdateSessionMetadata(_ context.Context, tenant, id string, metadata map[string]string) (sessions.Session, error) { s.writes++ - return store.Session{ID: id, TenantID: tenant, Metadata: metadata, Configuration: json.RawMessage(`{"agent":{"id":"agent_validation","model":"validation-model"},"environment":{"type":"none"}}`)}, nil + return sessions.Session{ID: id, TenantID: tenant, Metadata: metadata, Configuration: json.RawMessage(`{"agent":{"id":"agent_validation","model":"validation-model"},"environment":{"type":"none"}}`)}, nil } -func (s *validationStore) CreateSession(_ context.Context, tenant string, input store.CreateSessionInput) (store.Session, error) { +func (s *validationStore) CreateSession(_ context.Context, tenant string, input sessions.CreateSession) (sessions.Session, error) { s.writes++ - return store.Session{ID: uuid.NewString(), TenantID: tenant, Metadata: input.Metadata, Configuration: input.Configuration}, nil + return sessions.Session{ID: uuid.NewString(), TenantID: tenant, Metadata: input.Metadata, Configuration: input.Configuration}, nil } func (s *validationStore) CreateEnvironmentTemplate(context.Context, environmenttemplates.CreateCommand) (environmenttemplates.Template, error) { @@ -64,7 +64,7 @@ func (s *validationStore) UpdateEnvironmentTemplate(_ context.Context, command e // with initial input into s. Session reads are unexpected. func (s *validationStore) serve(d *Dependencies, f *testFakes) { d.Execution = f.execution() - f.admission.createSession = s.CreateSession + f.sessionAdmission.createSession = s.CreateSession f.agents.create, f.agents.update = s.CreateAgent, s.UpdateAgent f.vaults.createVault = s.CreateVault f.sessions.getSession, f.sessions.updateSessionMetadata = nil, s.UpdateSessionMetadata diff --git a/services/core/internal/api/write_audit.go b/services/core/internal/api/write_audit.go index 690a45081..154899a05 100644 --- a/services/core/internal/api/write_audit.go +++ b/services/core/internal/api/write_audit.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" ) @@ -43,7 +43,7 @@ func (h *Handler) writeAuditScope(w http.ResponseWriter, r *http.Request, allowe } tenant, ok := r.Context().Value(adminTenantContextKey{}).(string) if !ok || tenant == "" { - writeStoreError(w, r, store.ErrNotFound) + writeStoreError(w, r, sessions.ErrNotFound) return nil, "", false } return values, tenant, true diff --git a/services/core/internal/execution/archive_cancellation_cleanup_test.go b/services/core/internal/execution/archive_cancellation_cleanup_test.go index 370b8b7a0..8c7a2f739 100644 --- a/services/core/internal/execution/archive_cancellation_cleanup_test.go +++ b/services/core/internal/execution/archive_cancellation_cleanup_test.go @@ -24,7 +24,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/gorilla/websocket" ) @@ -78,7 +77,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) { if err != nil { t.Fatal(err) } - session, err := s.CreateSession(t.Context(), project.TenantID, store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.fixture.example/v1", APIKey: "fixture-key"}, ModelProviderSource: v1.ModelProviderSourceSession}) + session, err := s.CreateSession(t.Context(), project.TenantID, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.fixture.example/v1", APIKey: "fixture-key"}, ModelProviderSource: v1.ModelProviderSourceSession}) if err != nil { t.Fatal(err) } @@ -97,7 +96,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) { } // This fixture isolates lifecycle ordering. Protocol-driven waiting is // independently exercised in TestArchiveWaitingCancellationReceipts. - for _, transition := range []store.TurnTransition{{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}, {ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnWaiting}} { + for _, transition := range []sessions.TurnTransition{{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}, {ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnWaiting}} { if _, err := writer.TransitionTurn(t.Context(), project.TenantID, session.ID, input.TurnID, transition); err != nil { t.Fatal(err) } @@ -182,7 +181,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) { t.Fatal(pending, err) } // Controlled terminal receipt fixture; no native cancellation claim. - if _, err := writer.TransitionTurn(t.Context(), project.TenantID, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnWaiting, Status: sessions.TurnCancelled}); err != nil { + if _, err := writer.TransitionTurn(t.Context(), project.TenantID, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnWaiting, Status: sessions.TurnCancelled}); err != nil { t.Fatal(err) } expectedStatus = sessions.TurnCancelled diff --git a/services/core/internal/execution/artifacts.go b/services/core/internal/execution/artifacts.go index 9d6e42eb4..fd9fe9f9a 100644 --- a/services/core/internal/execution/artifacts.go +++ b/services/core/internal/execution/artifacts.go @@ -9,10 +9,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) -func (d *Dispatcher) captureCompletedArtifacts(ctx context.Context, peer *runtimegateway.Session, session store.Session, environment store.Environment, bound store.ExecutionDevice, turnID string, result Result, status string) (Result, string) { +func (d *Dispatcher) captureCompletedArtifacts(ctx context.Context, peer *runtimegateway.Session, session sessions.Session, environment sessions.Environment, bound sessions.ExecutionDevice, turnID string, result Result, status string) (Result, string) { if status != sessions.TurnCompleted || !LocalWorkspaceConfiguration(environment.Configuration) { return result, status } @@ -31,7 +30,7 @@ func (d *Dispatcher) captureCompletedArtifacts(ctx context.Context, peer *runtim } // Do not expose native diagnostics or publish partial output after a failed capture. result.ErrorCode = "artifact_capture_failed" - if errors.Is(err, store.ErrUnappliedInputs) { + if errors.Is(err, sessions.ErrUnappliedInputs) { result.ErrorCode = "input_not_applied" } status = sessions.TurnFailed diff --git a/services/core/internal/execution/deployment_provider_observations_test.go b/services/core/internal/execution/deployment_provider_observations_test.go index c1d7ea6d6..845cdc7ef 100644 --- a/services/core/internal/execution/deployment_provider_observations_test.go +++ b/services/core/internal/execution/deployment_provider_observations_test.go @@ -29,7 +29,7 @@ type finishObservationFixture struct { pool *pgxpool.Pool defaults *modelconfigurationpg.Store tenant string - session store.Session + session sessions.Session dispatcher Dispatcher } @@ -67,7 +67,7 @@ func newFinishObservationFixture(t *testing.T, maxConnections int32) finishObser } tenant := uuid.NewString() model, harness := "fixture-model", "codex" - input := store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: harness, IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"fixture-model"},"environment":{"type":"none"}}`), ModelProvider: snapshot.Provider, ModelProviderSource: "deployment", DeploymentProviderRevision: snapshot.Revision, + input := sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: harness, IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"fixture-model"},"environment":{"type":"none"}}`), ModelProvider: snapshot.Provider, ModelProviderSource: "deployment", DeploymentProviderRevision: snapshot.Revision, ExecutionConfiguration: &v1.SessionExecutionConfiguration{Model: v1.ExecutionSelection{Value: &model, Source: "session"}, Harness: v1.ExecutionSelection{Value: &harness, Source: "deployment"}, ModelProvider: v1.ExecutionProviderSelection{Source: "deployment"}}} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { @@ -75,13 +75,13 @@ func newFinishObservationFixture(t *testing.T, maxConnections int32) finishObser } return finishObservationFixture{s, owner.Store, owner.Lease, pool, defaults, tenant, session, Dispatcher{Store: owner.Store, Observer: defaults}} } -func (f finishObservationFixture) start(t *testing.T) store.InputReceipt { +func (f finishObservationFixture) start(t *testing.T) sessions.InputReceipt { t.Helper() receipt, err := f.s.SubmitMessage(t.Context(), f.tenant, f.session.ID, uuid.NewString(), json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"fixture"}]}]}`)) if err != nil { t.Fatal(err) } - if _, err = f.writer.TransitionTurn(t.Context(), f.tenant, f.session.ID, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = f.writer.TransitionTurn(t.Context(), f.tenant, f.session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } return receipt @@ -135,7 +135,7 @@ func TestFinishRunObservesOnlyFinalCommittedOutcome(t *testing.T) { t.Fatal(err) } _, err = f.dispatcher.finishRun(f.tenant, f.session.ID, receipt.TurnID, "fixture-model", result, tc.status) - if !errors.Is(err, store.ErrTurnConflict) { + if !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("expected completion conflict", err) } used, code = f.fields(t) diff --git a/services/core/internal/execution/device_authority.go b/services/core/internal/execution/device_authority.go index 9f923feb2..ef1ff6599 100644 --- a/services/core/internal/execution/device_authority.go +++ b/services/core/internal/execution/device_authority.go @@ -4,6 +4,7 @@ import ( "context" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -24,7 +25,7 @@ func authorizedRuntimePeer(ctx context.Context, s *store.Store, registry *runtim if drainErr != nil || !draining { peer.Close("Runtime authorization changed") } - return nil, store.ErrNotFound + return nil, sessions.ErrNotFound } if peer.IsClosed() { return nil, runtimegateway.ErrSessionClosed diff --git a/services/core/internal/execution/directory_preparation.go b/services/core/internal/execution/directory_preparation.go index 55c90d76e..427e400cb 100644 --- a/services/core/internal/execution/directory_preparation.go +++ b/services/core/internal/execution/directory_preparation.go @@ -6,10 +6,10 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -func (d *Dispatcher) readPreparedDirectory(ctx context.Context, peer *runtimegateway.Session, session store.Session, environment store.Environment, bound store.ExecutionDevice, read proto.WorkspaceReadPayload) directoryReadResult { +func (d *Dispatcher) readPreparedDirectory(ctx context.Context, peer *runtimegateway.Session, session sessions.Session, environment sessions.Environment, bound sessions.ExecutionDevice, read proto.WorkspaceReadPayload) directoryReadResult { owner, cancel := context.WithTimeout(ctx, 45*time.Second) defer cancel() var result directoryReadResult @@ -24,7 +24,7 @@ func (d *Dispatcher) readPreparedDirectory(ctx context.Context, peer *runtimegat return result } -func (d *Dispatcher) withPreparedWorkspace(owner context.Context, peer *runtimegateway.Session, session store.Session, environment store.Environment, bound store.ExecutionDevice, consume func(context.Context, string) error) error { +func (d *Dispatcher) withPreparedWorkspace(owner context.Context, peer *runtimegateway.Session, session sessions.Session, environment sessions.Environment, bound sessions.ExecutionDevice, consume func(context.Context, string) error) error { req := proto.PromptRequestPayload{AgentKind: session.Engine, AgentStateKey: "agents-api-" + session.ID, StrictResume: true, ReleaseOnCompletion: true, WorkspaceReadOnly: true} if err := d.configurePreparedEnvironment(session, environment, bound, &req); err != nil { return ErrExecutionUnavailable diff --git a/services/core/internal/execution/directory_preparation_test.go b/services/core/internal/execution/directory_preparation_test.go index 7c81cbeca..e6388203b 100644 --- a/services/core/internal/execution/directory_preparation_test.go +++ b/services/core/internal/execution/directory_preparation_test.go @@ -5,14 +5,14 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestDirectoryPreparationRejectsForeignBindingBeforeTransport(t *testing.T) { result := (&Dispatcher{}).readPreparedDirectory(t.Context(), nil, - store.Session{ID: "session", TenantID: "tenant"}, - store.Environment{ID: "environment", SessionID: "session", TenantID: "tenant", Configuration: []byte(`{"type":"self_hosted","workspace_directory":"/workspace"}`)}, - store.ExecutionDevice{EnvironmentID: "other"}, proto.WorkspaceReadPayload{}) + sessions.Session{ID: "session", TenantID: "tenant"}, + sessions.Environment{ID: "environment", SessionID: "session", TenantID: "tenant", Configuration: []byte(`{"type":"self_hosted","workspace_directory":"/workspace"}`)}, + sessions.ExecutionDevice{EnvironmentID: "other"}, proto.WorkspaceReadPayload{}) if !errors.Is(result.err, ErrExecutionUnavailable) { t.Fatal("foreign binding reached transport", result.err) } diff --git a/services/core/internal/execution/disabled_tools_test.go b/services/core/internal/execution/disabled_tools_test.go index 747499919..53aa85f46 100644 --- a/services/core/internal/execution/disabled_tools_test.go +++ b/services/core/internal/execution/disabled_tools_test.go @@ -9,7 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestDisabledToolsUseCommonOperationQualification(t *testing.T) { @@ -38,7 +38,7 @@ func TestDisabledToolRequestPreservesIntentOnResume(t *testing.T) { } before, _ := json.Marshal(snapshot) for _, nativeID := range []string{"", "native-session"} { - request, err := (&Dispatcher{}).executionRequest(t.Context(), store.Session{ID: "session"}, snapshot, runtimedevice.KindCapabilities{}, store.SessionExecutionBinding{NativeSessionID: nativeID}) + request, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{NativeSessionID: nativeID}) if err != nil || request.ExecutionControls.DisableProgrammaticToolCalling != disabled || request.ExecutionControls.WebSearch != "disabled" || request.AgentSessionID != nativeID { t.Fatal(request, err) } @@ -68,7 +68,7 @@ func TestEnabledWebSearchNeverReachesDispatch(t *testing.T) { t.Fatal(tool, err) } snapshot := Snapshot{Agent: v1.Agent{Model: "model", Tools: tools}} - if _, err := (&Dispatcher{}).executionRequest(t.Context(), store.Session{ID: "session"}, snapshot, runtimedevice.KindCapabilities{}, store.SessionExecutionBinding{}); err == nil { + if _, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); err == nil { t.Fatal("dispatch request built for", tool) } raw := json.RawMessage(`{"agent":{"model":"model","tools":[` + tool + `]},"environment":{"type":"none"}}`) diff --git a/services/core/internal/execution/dispatcher.go b/services/core/internal/execution/dispatcher.go index 43bf3680a..e5ca6b691 100644 --- a/services/core/internal/execution/dispatcher.go +++ b/services/core/internal/execution/dispatcher.go @@ -78,14 +78,14 @@ func (d *Dispatcher) Run(ctx context.Context, tenantID, sessionID, turnID string } var snapshot Snapshot if json.Unmarshal(session.Configuration, &snapshot) != nil || strings.TrimSpace(snapshot.Agent.Model) == "" { - return sessions.Turn{}, store.ErrInvalidInput + return sessions.Turn{}, sessions.ErrInvalidInput } caps, err := d.engineCapabilities(peer, session.Engine, snapshot) if err != nil { return sessions.Turn{}, err } if !environmentNone(snapshot) { - return sessions.Turn{}, store.ErrInvalidInput + return sessions.Turn{}, sessions.ErrInvalidInput } text, through, err := d.initialInput(ctx, tenantID, sessionID, turnID) if err != nil { @@ -104,7 +104,7 @@ func (d *Dispatcher) Run(ctx context.Context, tenantID, sessionID, turnID string return sessions.Turn{}, err } defer prepared.close() - if _, err := d.Store.TransitionTurn(ctx, tenantID, sessionID, turnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := d.Store.TransitionTurn(ctx, tenantID, sessionID, turnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { return sessions.Turn{}, err } req.ConversationID, req.RunID, req.Input = sessionID, turnID, text @@ -131,7 +131,7 @@ func (d *Dispatcher) finishRun(tenantID, sessionID, turnID, model string, result status, nativeID = sessions.TurnFailed, "" } turn, err := d.Store.CompleteExecution(finishCtx, tenantID, sessionID, turnID, status, encoded, nativeID, result.AppliedThrough) - if errors.Is(err, store.ErrUnappliedInputs) { + if errors.Is(err, sessions.ErrUnappliedInputs) { result.ErrorCode = "input_not_applied" encoded, _ = json.Marshal(result) turn, err = d.Store.CompleteExecution(finishCtx, tenantID, sessionID, turnID, sessions.TurnFailed, encoded, nativeID, result.AppliedThrough) diff --git a/services/core/internal/execution/engine_profile.go b/services/core/internal/execution/engine_profile.go index 7dec002a7..15b60fc6c 100644 --- a/services/core/internal/execution/engine_profile.go +++ b/services/core/internal/execution/engine_profile.go @@ -6,12 +6,12 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func profileError(err error) error { if errors.Is(err, engine.ErrInvalidInput) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } return err } @@ -50,7 +50,7 @@ func validateProfileConfiguration(profile engine.Profile, snapshot Snapshot) err return err } -func validateProfileInputs(profile engine.Profile, placement string, inputs []store.Input) error { +func validateProfileInputs(profile engine.Profile, placement string, inputs []sessions.Input) error { for _, input := range inputs { if input.Kind == "message" { messages, err := messageInput(input.Payload) @@ -68,13 +68,13 @@ func validateProfileInputs(profile engine.Profile, placement string, inputs []st if input.Kind != "tool_result" || profile.FunctionResultValidation == engine.CommonValidationOnly { continue } - var value store.FunctionResultInput + var value sessions.FunctionResultInput if json.Unmarshal(input.Payload, &value) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } - result, err := functionResult(store.FunctionCall{CallID: value.CallID, Result: value.Result}) + result, err := functionResult(sessions.FunctionCall{CallID: value.CallID, Result: value.Result}) if err != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } if err := profile.ValidateFunctionResult(placement, result); err != nil { return profileError(err) diff --git a/services/core/internal/execution/engine_profile_test.go b/services/core/internal/execution/engine_profile_test.go index 24df3c90b..754e370c9 100644 --- a/services/core/internal/execution/engine_profile_test.go +++ b/services/core/internal/execution/engine_profile_test.go @@ -10,7 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestAcceptedEnginePlacements(t *testing.T) { @@ -67,11 +67,11 @@ func TestAdditionalProfileUsesCommonAdmission(t *testing.T) { t.Fatal("additional engine admission failed", err) } snapshot.Agent.Model = "invalid" - if err := validateProfileConfiguration(profile, snapshot); !errors.Is(err, store.ErrInvalidInput) { + if err := validateProfileConfiguration(profile, snapshot); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("profile configuration lost public error mapping", err) } - inputs := []store.Input{{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":"response"}}`)}} - if err := validateProfileInputs(profile, "none", inputs); !errors.Is(err, store.ErrInvalidInput) || !resultChecked { + inputs := []sessions.Input{{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":"response"}}`)}} + if err := validateProfileInputs(profile, "none", inputs); !errors.Is(err, sessions.ErrInvalidInput) || !resultChecked { t.Fatal("profile result lost public error mapping", err) } } @@ -144,7 +144,7 @@ func TestCommonOnlyValidationPreservesFunctionResults(t *testing.T) { profile := enginetest.Profile(nil) catalog := engine.NewCatalog(map[string]engine.Profile{"fixture": profile}) profile, _ = catalog.Lookup("fixture") - inputs := []store.Input{{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":"response"}}`)}} + inputs := []sessions.Input{{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":"response"}}`)}} if err := validateProfileInputs(profile, "none", inputs); err != nil { t.Fatal("common-only result acquired a native restriction", err) } diff --git a/services/core/internal/execution/environment_admission.go b/services/core/internal/execution/environment_admission.go index 38def27b0..835027d90 100644 --- a/services/core/internal/execution/environment_admission.go +++ b/services/core/internal/execution/environment_admission.go @@ -7,7 +7,7 @@ import ( "slices" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) var ( @@ -25,12 +25,12 @@ func preparedEnvironmentConfiguration(configuration json.RawMessage) bool { func (w *Worker) validateEnvironmentAdmission(ctx context.Context, engine string, configuration json.RawMessage) error { var snapshot Snapshot if json.Unmarshal(configuration, &snapshot) != nil || snapshot.Environment == nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } switch snapshot.Environment.Type { case "self_hosted": if w.dispatcher.Registry == nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } case "openai_hosted": if w.runtimes == nil { @@ -44,12 +44,12 @@ func (w *Worker) validateEnvironmentAdmission(ctx context.Context, engine string return ErrExecutionUnavailable } default: - return store.ErrInvalidInput + return sessions.ErrInvalidInput } return w.dispatcher.ValidateSessionConfiguration(engine, configuration) } -func (w *Worker) validateCreation(ctx context.Context, input store.CreateSessionInput) error { +func (w *Worker) validateCreation(ctx context.Context, input sessions.CreateSession) error { if err := w.dispatcher.validateEngineInputs(input.Engine, input.Configuration, input.InitialInputs); err != nil { return err } @@ -59,7 +59,7 @@ func (w *Worker) validateCreation(ctx context.Context, input store.CreateSession } var snapshot Snapshot if err := json.Unmarshal(input.Configuration, &snapshot); err != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } if len(input.InitialInputs) == 0 && snapshot.Environment.Type == "self_hosted" { return nil @@ -67,12 +67,12 @@ func (w *Worker) validateCreation(ctx context.Context, input store.CreateSession return w.checkAdmissionOwnership(ctx) } if !w.dispatcher.canAdmitInputs(input.Engine, input.Configuration) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } return nil } -func (w *Worker) submitEnvironmentInputs(ctx context.Context, session store.Session, key string, inputs []store.Input) ([]store.InputReceipt, error) { +func (w *Worker) submitEnvironmentInputs(ctx context.Context, session sessions.Session, key string, inputs []sessions.Input) ([]sessions.InputReceipt, error) { if err := w.validateEnvironmentAdmission(ctx, session.Engine, session.Configuration); err != nil { return nil, err } @@ -83,7 +83,7 @@ func (w *Worker) submitEnvironmentInputs(ctx context.Context, session store.Sess if len(inputs) > 0 { kind = inputs[0].Kind } - if (kind == "cancel" || kind == "tool_result") && !slices.ContainsFunc(inputs, func(input store.Input) bool { return input.Kind != kind }) { + if (kind == "cancel" || kind == "tool_result") && !slices.ContainsFunc(inputs, func(input sessions.Input) bool { return input.Kind != kind }) { // Neither kind creates a Turn. The Session lock preserves target and retry identity. return w.admitInputs(ctx, session.TenantID, session.ID, key, inputs) } @@ -103,20 +103,20 @@ func (w *Worker) submitEnvironmentInputs(ctx context.Context, session store.Sess return nil, err } w.wakeScheduler() - if reservation.State == store.EnvironmentInputPending && !reservation.IsInitial { + if reservation.State == sessions.EnvironmentInputPending && !reservation.IsInitial { w.hintRuntimeWake(ctx, session) } ticker := time.NewTicker(250 * time.Millisecond) defer ticker.Stop() for { switch reservation.State { - case store.EnvironmentInputAdmitted: + case sessions.EnvironmentInputAdmitted: return reservation.Receipts, nil - case store.EnvironmentInputFailed: - return nil, store.ErrEnvironmentUnavailable - case store.EnvironmentInputExpired: + case sessions.EnvironmentInputFailed: + return nil, sessions.ErrEnvironmentUnavailable + case sessions.EnvironmentInputExpired: return nil, ErrEnvironmentInputExpired - case store.EnvironmentInputCancelled: + case sessions.EnvironmentInputCancelled: return nil, ErrEnvironmentInputCancelled } select { @@ -135,7 +135,7 @@ func (w *Worker) submitEnvironmentInputs(ctx context.Context, session store.Sess } } -func (w *Worker) environmentInputOutcome(ctx context.Context, session store.Session, reservation store.EnvironmentInputReservation) (store.EnvironmentInputReservation, error) { +func (w *Worker) environmentInputOutcome(ctx context.Context, session sessions.Session, reservation sessions.EnvironmentInputReservation) (sessions.EnvironmentInputReservation, error) { read, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() // The database rechecks its clock under the Session lock before settlement. diff --git a/services/core/internal/execution/environment_capabilities_test.go b/services/core/internal/execution/environment_capabilities_test.go index b5678c2a7..3529872e5 100644 --- a/services/core/internal/execution/environment_capabilities_test.go +++ b/services/core/internal/execution/environment_capabilities_test.go @@ -6,15 +6,15 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestSelfHostedCapabilitySourcesAreFrozenAndStrict(t *testing.T) { - session := store.Session{ID: "session", TenantID: "tenant"} - environment := store.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, + session := sessions.Session{ID: "session", TenantID: "tenant"} + environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"self_hosted","workspace_directory":"/home/user/project","capability_directories":["/home/user/skills","/opt/plugins"]}`)} var request proto.PromptRequestPayload - if err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: environment.ID}, &request); err != nil { + if err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &request); err != nil { t.Fatal(err) } local := request.LocalEnvironment @@ -36,10 +36,10 @@ func TestSelfHostedCapabilitySourcesAreFrozenAndStrict(t *testing.T) { func TestSelfHostedPreparedPathsArePlatformNeutral(t *testing.T) { for _, directory := range []string{`C:\work`, `D:/skills`, `\\server\share\project`, `/Users/user/work`} { raw, _ := json.Marshal(map[string]any{"type": "self_hosted", "workspace_directory": directory, "capability_directories": []string{directory}}) - session := store.Session{ID: "session", TenantID: "tenant"} - environment := store.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: raw} + session := sessions.Session{ID: "session", TenantID: "tenant"} + environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: raw} var request proto.PromptRequestPayload - err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: environment.ID}, &request) + err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &request) if err != nil || request.LocalEnvironment.WorkspaceDirectory != directory || request.LocalEnvironment.CapabilitySources.Directories[0] != directory { t.Fatal("Core interpreted a Runtime source path", directory, err) } diff --git a/services/core/internal/execution/environment_directory.go b/services/core/internal/execution/environment_directory.go index cf05316ca..8b6c61562 100644 --- a/services/core/internal/execution/environment_directory.go +++ b/services/core/internal/execution/environment_directory.go @@ -7,7 +7,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) type directoryReadResult struct { @@ -17,7 +16,7 @@ type directoryReadResult struct { type directoryReadRequest struct { ctx context.Context - environment store.Environment + environment sessions.Environment path string result chan directoryReadResult } @@ -25,7 +24,7 @@ type directoryReadRequest struct { func (r directoryReadRequest) reply(result directoryReadResult) { r.result <- result } // ReadEnvironmentDirectory observes a Worker-owned read without admitting model input. -func (w *Worker) ReadEnvironmentDirectory(ctx context.Context, environment store.Environment, path string) (proto.WorkspaceDirectoryResult, error) { +func (w *Worker) ReadEnvironmentDirectory(ctx context.Context, environment sessions.Environment, path string) (proto.WorkspaceDirectoryResult, error) { ctx, cancel := context.WithTimeout(ctx, 45*time.Second) defer cancel() current, err := w.admission.GetEnvironment(ctx, environment.TenantID, environment.ID) @@ -33,7 +32,7 @@ func (w *Worker) ReadEnvironmentDirectory(ctx context.Context, environment store return proto.WorkspaceDirectoryResult{}, err } if current.SessionID != environment.SessionID { - return proto.WorkspaceDirectoryResult{}, store.ErrNotFound + return proto.WorkspaceDirectoryResult{}, sessions.ErrNotFound } if err := w.waitRuntimeAwake(ctx, current); err != nil { return proto.WorkspaceDirectoryResult{}, err @@ -79,7 +78,7 @@ func (w *Worker) runDirectoryRead(owner context.Context, request directoryReadRe return } if environment.SessionID != request.environment.SessionID { - result.err = store.ErrNotFound + result.err = sessions.ErrNotFound return } placement, err := parseEnvironmentPlacement(environment.Configuration) @@ -149,7 +148,7 @@ func readEnvironmentDirectory(ctx context.Context, peer *runtimegateway.Session, return directoryReadResult{directory: proto.WorkspaceDirectoryResult{Entries: []proto.WorkspaceDirectoryEntry{}}} } if err == nil && result.Outcome == "rejected" && result.ErrorCode == "not_found" { - return directoryReadResult{err: store.ErrNotFound} + return directoryReadResult{err: sessions.ErrNotFound} } return directoryReadResult{err: ErrExecutionUnavailable} } diff --git a/services/core/internal/execution/environment_file_write.go b/services/core/internal/execution/environment_file_write.go index 5976d09c0..b0b43f194 100644 --- a/services/core/internal/execution/environment_file_write.go +++ b/services/core/internal/execution/environment_file_write.go @@ -9,16 +9,16 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" "github.com/google/uuid" ) // Known Files.create destination refusals reported by the Runtime installer. -// They wrap store.ErrInvalidInput; the rejected write installed nothing. +// They wrap sessions.ErrInvalidInput; the rejected write installed nothing. var ( - ErrEnvironmentFileDirectory = fmt.Errorf("%w: environment file destination is a directory", store.ErrInvalidInput) - ErrEnvironmentFileUnsafe = fmt.Errorf("%w: environment file destination exists or traverses a link", store.ErrInvalidInput) + ErrEnvironmentFileDirectory = fmt.Errorf("%w: environment file destination is a directory", sessions.ErrInvalidInput) + ErrEnvironmentFileUnsafe = fmt.Errorf("%w: environment file destination exists or traverses a link", sessions.ErrInvalidInput) ) type fileWriteResult struct { @@ -27,7 +27,7 @@ type fileWriteResult struct { } type fileWriteRequest struct { ctx context.Context - environment store.Environment + environment sessions.Environment path string data []byte result chan fileWriteResult @@ -35,9 +35,9 @@ type fileWriteRequest struct { // WriteEnvironmentFile observes a Worker-owned mutation. Caller detachment never // clears the durable write intent or starts a replacement operation. -func (w *Worker) WriteEnvironmentFile(ctx context.Context, environment store.Environment, path string, data []byte) (int64, error) { +func (w *Worker) WriteEnvironmentFile(ctx context.Context, environment sessions.Environment, path string, data []byte) (int64, error) { if len(data) > proto.WorkspaceWriteMaxBytes { - return 0, store.ErrInvalidInput + return 0, sessions.ErrInvalidInput } if err := w.waitRuntimeAwake(ctx, environment); err != nil { return 0, err @@ -79,7 +79,7 @@ func (w *Worker) runFileWrite(owner context.Context, request fileWriteRequest) f return fileWriteResult{err: err} } if environment.SessionID != request.environment.SessionID { - return fileWriteResult{err: store.ErrNotFound} + return fileWriteResult{err: sessions.ErrNotFound} } placement, err := parseEnvironmentPlacement(environment.Configuration) if err != nil || (placement.Type != "openai_hosted" && placement.Type != "self_hosted") { @@ -106,9 +106,9 @@ func (w *Worker) runFileWrite(owner context.Context, request fileWriteRequest) f digest := sha256.Sum256(body) wire := proto.WorkspaceWritePayload{Step: "begin", EnvironmentID: environment.ID, SessionID: session.ID, Path: request.path, SizeBytes: len(request.data), SHA256: hex.EncodeToString(dataDigest[:])} if !proto.ValidWorkspaceWriteRequest(wire) { - return fileWriteResult{err: store.ErrInvalidInput} + return fileWriteResult{err: sessions.ErrInvalidInput} } - key := store.FileWriteIdentity{ID: uuid.NewString(), DeviceID: bound.ID, RequestSHA256: hex.EncodeToString(digest[:])} + key := sessions.FileWriteIdentity{ID: uuid.NewString(), DeviceID: bound.ID, RequestSHA256: hex.EncodeToString(digest[:])} intent, err := w.dispatcher.Store.ReserveEnvironmentFileWrite(ctx, environment.TenantID, environment.ID, key) if err != nil { return fileWriteResult{err: err} @@ -136,7 +136,7 @@ func (w *Worker) runFileWrite(owner context.Context, request fileWriteRequest) f case result.ErrorCode == "write_rejected" && result.Reason == proto.WorkspaceWriteReasonUnsafe: return fileWriteResult{err: ErrEnvironmentFileUnsafe} case result.ErrorCode == "invalid_request" || result.ErrorCode == "write_rejected": - return fileWriteResult{err: store.ErrInvalidInput} + return fileWriteResult{err: sessions.ErrInvalidInput} } return unavailable } diff --git a/services/core/internal/execution/environment_placement.go b/services/core/internal/execution/environment_placement.go index 9b2f26725..fe57e0ede 100644 --- a/services/core/internal/execution/environment_placement.go +++ b/services/core/internal/execution/environment_placement.go @@ -10,7 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type environmentPlacement struct { @@ -34,7 +34,7 @@ func LocalWorkspaceConfiguration(configuration json.RawMessage) bool { func parseEnvironmentPlacement(configuration json.RawMessage) (environmentPlacement, error) { var placement environmentPlacement if json.Unmarshal(configuration, &placement) != nil { - return placement, store.ErrInvalidInput + return placement, sessions.ErrInvalidInput } var local struct { Plugins []agentplugin.Metadata `json:"plugins,omitempty"` @@ -50,48 +50,48 @@ func parseEnvironmentPlacement(configuration json.RawMessage) (environmentPlacem decoder := json.NewDecoder(bytes.NewReader(configuration)) decoder.DisallowUnknownFields() if decoder.Decode(&local) != nil || agentcapabilities.ValidateSourceDirectories(local.CapabilityDirectories) != nil { - return placement, store.ErrInvalidInput + return placement, sessions.ErrInvalidInput } // Placement selects a workspace; all preparation fields are shared. switch placement.Type { case "openai_hosted": if local.WorkspaceDirectory != "" || agentcapabilities.ValidateDirectories(local.CapabilityDirectories) != nil { - return placement, store.ErrInvalidInput + return placement, sessions.ErrInvalidInput } placement.WorkspaceDirectory = "/workspace" case "self_hosted": if !validSelfHostedPlacement(placement) { - return placement, store.ErrInvalidInput + return placement, sessions.ErrInvalidInput } default: - return placement, store.ErrInvalidInput + return placement, sessions.ErrInvalidInput } placement.NetworkAccess = "enabled" if local.Network != nil { if (agentnetwork.Policy{Access: local.Network.Access, AllowedDomains: local.Network.AllowedDomains}).Validate() != nil { - return placement, store.ErrInvalidInput + return placement, sessions.ErrInvalidInput } placement.NetworkAccess, placement.AllowedDomains = local.Network.Access, append([]string(nil), local.Network.AllowedDomains...) } return placement, nil } -func environmentDeviceMatches(session store.Session, environment store.Environment, bound store.ExecutionDevice) bool { +func environmentDeviceMatches(session sessions.Session, environment sessions.Environment, bound sessions.ExecutionDevice) bool { if environment.SessionID != session.ID || environment.TenantID != session.TenantID { return false } return bound.EnvironmentID == environment.ID } -func (d *Dispatcher) configurePreparedEnvironment(session store.Session, environment store.Environment, bound store.ExecutionDevice, req *proto.PromptRequestPayload) error { +func (d *Dispatcher) configurePreparedEnvironment(session sessions.Session, environment sessions.Environment, bound sessions.ExecutionDevice, req *proto.PromptRequestPayload) error { placement, err := parseEnvironmentPlacement(environment.Configuration) if err != nil || !environmentDeviceMatches(session, environment, bound) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } sources := &agentcapabilities.Input{Plugins: append([]agentplugin.Metadata(nil), placement.Plugins...), Directories: append([]string(nil), placement.CapabilityDirectories...)} for _, metadata := range placement.Skills { if metadata.ValidateInstalled() != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } sources.Skills = append(sources.Skills, (environmentconfig.Skill{Metadata: metadata}).InstallationMetadata()) } diff --git a/services/core/internal/execution/environment_placement_test.go b/services/core/internal/execution/environment_placement_test.go index 227cff346..4d980e058 100644 --- a/services/core/internal/execution/environment_placement_test.go +++ b/services/core/internal/execution/environment_placement_test.go @@ -7,15 +7,15 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestSkillReferenceIdentityStopsAtCoreBoundary(t *testing.T) { - session := store.Session{ID: "session", TenantID: "tenant"} - environment := store.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, + session := sessions.Session{ID: "session", TenantID: "tenant"} + environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"openai_hosted","initialization":true,"skills":[{"type":"skill_reference","skill_id":"skill-private","version":"1","name":"proof","description":"A proof."}]}`)} var request proto.PromptRequestPayload - err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: environment.ID}, &request) + err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &request) if err != nil || request.LocalEnvironment == nil || !request.LocalEnvironment.Capabilities || len(request.LocalEnvironment.Skills) != 0 { t.Fatal("resolved Skill did not use the common installation descriptor", err) } @@ -27,7 +27,7 @@ func TestSkillReferenceIdentityStopsAtCoreBoundary(t *testing.T) { if !LocalWorkspaceConfiguration(environment.Configuration) { t.Fatal("admission demanded installed metadata before the creation transaction") } - if err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: environment.ID}, &proto.PromptRequestPayload{}); err == nil { + if err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &proto.PromptRequestPayload{}); err == nil { t.Fatal("execution received an unresolved Skill selector") } } @@ -44,12 +44,12 @@ func TestLocalEnvironmentRequiresQualifiedProfileAndExactAuthority(t *testing.T) t.Fatalf("unqualified private profile accepted: %s", configuration) } } - session := store.Session{ID: "session", TenantID: "tenant"} - environment := store.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"openai_hosted","network":{"access":"disabled"}}`)} + session := sessions.Session{ID: "session", TenantID: "tenant"} + environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"openai_hosted","network":{"access":"disabled"}}`)} d := &Dispatcher{} for _, scope := range []string{"", "other", environment.ID} { var req proto.PromptRequestPayload - err := d.configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: scope}, &req) + err := d.configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: scope}, &req) if scope == environment.ID { if err != nil || req.LocalEnvironment == nil || req.LocalEnvironment.ID != environment.ID { t.Fatal("local identity was not preserved", err) @@ -61,16 +61,16 @@ func TestLocalEnvironmentRequiresQualifiedProfileAndExactAuthority(t *testing.T) } func TestNetworkPolicySurvivesPreparedBinding(t *testing.T) { - session := store.Session{ID: "session", TenantID: "tenant"} + session := sessions.Session{ID: "session", TenantID: "tenant"} for _, network := range []string{`{"access":"disabled"}`, `{"access":"restricted","allowed_domains":["Example.com","api.example.com"]}`} { - environment := store.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, + environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"openai_hosted","network":` + network + `}`)} placement, err := parseEnvironmentPlacement(environment.Configuration) if err != nil { t.Fatal(err) } var req proto.PromptRequestPayload - err = (&Dispatcher{}).configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: environment.ID}, &req) + err = (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &req) if err != nil || req.LocalEnvironment == nil || req.LocalEnvironment.NetworkAccess != placement.NetworkAccess || !slices.Equal(req.LocalEnvironment.AllowedDomains, placement.AllowedDomains) { t.Fatal("prepared binding lost policy", req.LocalEnvironment, err) } @@ -78,10 +78,10 @@ func TestNetworkPolicySurvivesPreparedBinding(t *testing.T) { } func TestToolEnvironmentRemainsInExecutionBinding(t *testing.T) { - session := store.Session{ID: "session", TenantID: "tenant"} - environment := store.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"openai_hosted","initialization":true,"packages":{"npm":["is-number"]}}`)} + session := sessions.Session{ID: "session", TenantID: "tenant"} + environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"openai_hosted","initialization":true,"packages":{"npm":["is-number"]}}`)} var req proto.PromptRequestPayload - err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: environment.ID}, &req) + err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &req) if err != nil || req.LocalEnvironment == nil || !req.LocalEnvironment.ToolEnvironment { t.Fatal("tool initialization requirement lost", err) } diff --git a/services/core/internal/execution/executor_preparation.go b/services/core/internal/execution/executor_preparation.go index 7b7ccfed8..fad7e9c36 100644 --- a/services/core/internal/execution/executor_preparation.go +++ b/services/core/internal/execution/executor_preparation.go @@ -7,7 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // prepareTurnExecutor reserves one admission on the Runtime-owned Executor. @@ -42,7 +42,7 @@ func (d *Dispatcher) awaitTurnExecutor(ctx context.Context, tenant, session, tur return err } if current.Status != expectedStatus || !current.CancelRequestedAt.IsZero() { - return store.ErrTurnConflict + return sessions.ErrTurnConflict } case env, ok := <-prepared.sub.Events: if !ok { diff --git a/services/core/internal/execution/function_images_test.go b/services/core/internal/execution/function_images_test.go index eb6e596d4..b393598fa 100644 --- a/services/core/internal/execution/function_images_test.go +++ b/services/core/internal/execution/function_images_test.go @@ -8,7 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestFunctionImageAdmission(t *testing.T) { @@ -27,8 +27,8 @@ func TestFunctionImageAdmission(t *testing.T) { t.Run(tc.name, func(t *testing.T) { output := []any{map[string]any{"type": "input_text", "text": "before"}, map[string]any{"type": "input_image", "image_url": tc.url}, map[string]any{"type": "input_text", "text": "after"}} raw, _ := json.Marshal(map[string]any{"call_id": "call", "result": map[string]any{"success": tc.success, "output": output}}) - err := validateProfileInputs(profile, "none", []store.Input{{Kind: "tool_result", Payload: raw}}) - if tc.valid && err != nil || !tc.valid && !errors.Is(err, store.ErrInvalidInput) { + err := validateProfileInputs(profile, "none", []sessions.Input{{Kind: "tool_result", Payload: raw}}) + if tc.valid && err != nil || !tc.valid && !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal(err) } }) diff --git a/services/core/internal/execution/functions.go b/services/core/internal/execution/functions.go index d1b8e32eb..5f4c4102d 100644 --- a/services/core/internal/execution/functions.go +++ b/services/core/internal/execution/functions.go @@ -70,7 +70,7 @@ func (f *functionExchange) record(ctx context.Context, env proto.Envelope) error if !declared { return errors.New("undeclared function callback") } - err := f.store.RecordFunctionCall(ctx, f.tenant, f.session, f.turn, store.FunctionCall{ + err := f.store.RecordFunctionCall(ctx, f.tenant, f.session, f.turn, sessions.FunctionCall{ CallID: items.Identity(f.turn, "tool:"+call.CallID), ExecutorCallID: call.CallID, Name: call.Name, Arguments: call.Arguments, }) return f.unlessCancelling(ctx, err) @@ -123,7 +123,7 @@ func (f *functionExchange) confirm(ctx context.Context, reply functionReply) err } func (f *functionExchange) unlessCancelling(ctx context.Context, err error) error { - if errors.Is(err, store.ErrTurnConflict) { + if errors.Is(err, sessions.ErrTurnConflict) { turn, lookupErr := f.store.GetTurn(ctx, f.tenant, f.session, f.turn) if lookupErr == nil && !turn.CancelRequestedAt.IsZero() { return nil @@ -153,7 +153,7 @@ func (f *functionExchange) complete(ctx context.Context) error { return nil } -func functionResult(call store.FunctionCall) (proto.FunctionResultPayload, error) { +func functionResult(call sessions.FunctionCall) (proto.FunctionResultPayload, error) { var value struct { Success *bool `json:"success"` Output json.RawMessage `json:"output"` diff --git a/services/core/internal/execution/functions_test.go b/services/core/internal/execution/functions_test.go index 57086ac78..87ed54ee6 100644 --- a/services/core/internal/execution/functions_test.go +++ b/services/core/internal/execution/functions_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestFunctionDefinitionsRejectUnsupportedConfiguration(t *testing.T) { @@ -40,13 +40,13 @@ func TestFunctionResultPreservesCompleteContent(t *testing.T) { {`{"success":false,"error":"failed"}`, false, []proto.InputContent{text("failed")}}, {`{"success":false,"output":[{"type":"input_text","text":"before"},{"type":"input_image","image_url":"data:image/png;base64,test"},{"type":"input_text","text":""}],"error":"failed"}`, false, []proto.InputContent{text("before"), {Type: "input_image", ImageURL: &imageURL}, text(""), text("failed")}}, } { - result, err := functionResult(store.FunctionCall{CallID: "public", ExecutorCallID: "native", Result: json.RawMessage(test.raw)}) + result, err := functionResult(sessions.FunctionCall{CallID: "public", ExecutorCallID: "native", Result: json.RawMessage(test.raw)}) if err != nil || result.CallID != "native" || result.DeliveryID != "function:public" || result.Success != test.success || !reflect.DeepEqual(result.Content, test.content) { t.Fatal(result, err) } } for _, raw := range []string{`{}`, `{"success":null}`, `{"success":true,"output":{}}`, `{"success":true,"output":[{"type":"input_text"}]}`, `{"success":true,"output":[{"type":"input_audio","audio_url":"a"}]}`} { - if _, err := functionResult(store.FunctionCall{Result: json.RawMessage(raw)}); err == nil { + if _, err := functionResult(sessions.FunctionCall{Result: json.RawMessage(raw)}); err == nil { t.Fatal("invalid stored result converted", raw) } } diff --git a/services/core/internal/execution/journal.go b/services/core/internal/execution/journal.go index b94d37393..aaa79fbe4 100644 --- a/services/core/internal/execution/journal.go +++ b/services/core/internal/execution/journal.go @@ -6,21 +6,21 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type journal struct { store eventWriter tenant, session, turn string next int32 - batch []store.ExecutionEvent + batch []sessions.ExecutionEvent bytes int pendingCount int observeSubagents bool } type eventWriter interface { - AppendTurnEvents(context.Context, string, string, string, int32, []store.ExecutionEvent) error + AppendTurnEvents(context.Context, string, string, string, int32, []sessions.ExecutionEvent) error } func recordCancellation(ctx context.Context, journal *journal, reply cancellationResult, result *Result) error { @@ -55,7 +55,7 @@ func (j *journal) observe(ctx context.Context, env proto.Envelope) error { func (j *journal) enqueue(env proto.Envelope) error { if (env.Type == proto.TypeSubagentIdentity || env.Type == proto.TypeSubagentLifecycle || env.Type == proto.TypeSubagentTurn || env.Type == proto.TypeSubagentItem || env.Type == proto.TypeSubagentCoordination) && !j.observeSubagents { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } switch env.Type { case proto.TypeDelta, proto.TypeOutputMessage, proto.TypeThinking, proto.TypeToolCall, proto.TypeCommandOutput, proto.TypeUsage, @@ -64,9 +64,9 @@ func (j *journal) enqueue(env proto.Envelope) error { return nil } if len(env.Payload) > 512*1024 { - return store.ErrEventLimit + return sessions.ErrEventLimit } - j.batch = append(j.batch, store.ExecutionEvent{Kind: env.Type, Payload: env.Payload}) + j.batch = append(j.batch, sessions.ExecutionEvent{Kind: env.Type, Payload: env.Payload}) j.bytes += len(env.Payload) return nil } diff --git a/services/core/internal/execution/journal_test.go b/services/core/internal/execution/journal_test.go index defd66b2a..d39b047c3 100644 --- a/services/core/internal/execution/journal_test.go +++ b/services/core/internal/execution/journal_test.go @@ -8,12 +8,12 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type recoveringWriter struct { fail bool - events []store.ExecutionEvent + events []sessions.ExecutionEvent } type ambiguousWriter struct { @@ -21,14 +21,14 @@ type ambiguousWriter struct { written int } -func (w *ambiguousWriter) AppendTurnEvents(_ context.Context, _, _, _ string, first int32, events []store.ExecutionEvent) error { +func (w *ambiguousWriter) AppendTurnEvents(_ context.Context, _, _, _ string, first int32, events []sessions.ExecutionEvent) error { if w.firstCount == 0 { w.firstCount, w.written = len(events), len(events) return context.DeadlineExceeded } if first == 1 { if len(events) != w.firstCount { - return store.ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } return nil } @@ -63,7 +63,7 @@ func TestJournalKeepsBatchIdentityAfterAnUncertainCommit(t *testing.T) { } } -func (w *recoveringWriter) AppendTurnEvents(_ context.Context, _, _, _ string, first int32, events []store.ExecutionEvent) error { +func (w *recoveringWriter) AppendTurnEvents(_ context.Context, _, _, _ string, first int32, events []sessions.ExecutionEvent) error { if w.fail { w.fail = false return context.DeadlineExceeded diff --git a/services/core/internal/execution/mcp_support_test.go b/services/core/internal/execution/mcp_support_test.go index d02cd278e..ab3901dcf 100644 --- a/services/core/internal/execution/mcp_support_test.go +++ b/services/core/internal/execution/mcp_support_test.go @@ -9,7 +9,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" "github.com/google/uuid" ) @@ -56,8 +56,8 @@ func TestMCPPublicBearerPolicyIsIndependentOfRuntimeCapabilities(t *testing.T) { t.Fatal("runtime capabilities widened public admission", err) } credentials := &recordingCredentials{token: "scoped-token"} - session := store.Session{TenantID: uuid.NewString(), Engine: engine} - request, err := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), session, snapshot, caps, store.SessionExecutionBinding{}) + session := sessions.Session{TenantID: uuid.NewString(), Engine: engine} + request, err := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), session, snapshot, caps, sessions.ExecutionBinding{}) if !allowed { if err == nil || err.Error() != "The configured engine does not support this MCP connection origin." || request.MCPHTTPServers != nil || len(credentials.requests) != 0 { t.Fatal("unverified profile bypassed public policy", err) @@ -111,7 +111,7 @@ func TestMCPExecutionChecksRequireVerifiedCapabilityCombinations(t *testing.T) { } if !allowed { credentials := &recordingCredentials{token: "scoped-token"} - request, requestErr := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), store.Session{Engine: "codex"}, snapshot, caps, store.SessionExecutionBinding{}) + request, requestErr := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, caps, sessions.ExecutionBinding{}) if requestErr == nil || request.MCPHTTPServers != nil || len(credentials.requests) != 0 { t.Fatal("request bypassed capability checks before credential lookup", requestErr) } @@ -143,7 +143,7 @@ func TestMCPAnonymousExecutionPreservesFrozenDecision(t *testing.T) { t.Fatal("anonymous binding validation changed", err) } credentials := &recordingCredentials{token: "scoped-token"} - request, err := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), store.Session{Engine: engine}, snapshot, caps, store.SessionExecutionBinding{}) + request, err := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), sessions.Session{Engine: engine}, snapshot, caps, sessions.ExecutionBinding{}) if len(credentials.requests) != 0 { t.Fatal("anonymous or invalid binding reached credential lookup") } diff --git a/services/core/internal/execution/message_input.go b/services/core/internal/execution/message_input.go index 5a296501b..87e174edc 100644 --- a/services/core/internal/execution/message_input.go +++ b/services/core/internal/execution/message_input.go @@ -5,7 +5,7 @@ import ( "encoding/json" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func messageInput(raw json.RawMessage) (proto.MessageInput, error) { @@ -14,7 +14,7 @@ func messageInput(raw json.RawMessage) (proto.MessageInput, error) { Input []v1.InputMessage `json:"input"` } if json.Unmarshal(raw, &input) != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } var messages proto.MessageInput if len(input.Input) == 0 && input.Text != nil { @@ -22,7 +22,7 @@ func messageInput(raw json.RawMessage) (proto.MessageInput, error) { } for _, message := range input.Input { if message.Role != "user" { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } converted := proto.InputMessage{} for _, part := range message.Content { @@ -31,7 +31,7 @@ func messageInput(raw json.RawMessage) (proto.MessageInput, error) { messages = append(messages, converted) } if messages.Validate() != nil { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } return messages, nil } @@ -46,7 +46,7 @@ func (d *Dispatcher) initialInput(ctx context.Context, tenant, session, turn str size := 0 for _, input := range inputs { if input.Kind != "message" { - return nil, 0, store.ErrInvalidInput + return nil, 0, sessions.ErrInvalidInput } batch, err := messageInput(input.Payload) if err != nil { @@ -60,7 +60,7 @@ func (d *Dispatcher) initialInput(ctx context.Context, tenant, session, turn str through = input.Sequence } if len(messages) == 0 { - return nil, 0, store.ErrInvalidInput + return nil, 0, sessions.ErrInvalidInput } return messages, through, nil } diff --git a/services/core/internal/execution/message_support.go b/services/core/internal/execution/message_support.go index f32a2ab11..39f1a4526 100644 --- a/services/core/internal/execution/message_support.go +++ b/services/core/internal/execution/message_support.go @@ -7,7 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // ErrWhitespaceOnlyText is a declared native limitation reported before any @@ -51,7 +51,7 @@ func validateMessageImageProfile(profile engine.Profile, _ string, input proto.M return nil } if !profile.MessageImages.IsSupported() || input.ValidateInlineImages() != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } return nil } @@ -63,7 +63,7 @@ func (p Policy) messageInputSupport(peer *runtimegateway.Session, kind string, s } profile, ok := p.Engines.Lookup(kind) if !ok { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } placement := "" if snapshot.Environment != nil { diff --git a/services/core/internal/execution/message_support_test.go b/services/core/internal/execution/message_support_test.go index 112f4a11d..773893fe6 100644 --- a/services/core/internal/execution/message_support_test.go +++ b/services/core/internal/execution/message_support_test.go @@ -8,7 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestMessageImageQualificationIsOperationSpecific(t *testing.T) { @@ -21,7 +21,7 @@ func TestMessageImageQualificationIsOperationSpecific(t *testing.T) { if err := validateMessageImageProfile(profile, "self_hosted", input); err != nil { t.Fatal("qualified user machine rejected", err) } - if err := validateMessageImageProfile(enginetest.Profile(nil), "none", input); !errors.Is(err, store.ErrInvalidInput) { + if err := validateMessageImageProfile(enginetest.Profile(nil), "none", input); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("unqualified profile accepted", err) } // Text admission and dispatch must not gain an online/image requirement. @@ -33,8 +33,8 @@ func TestMessageImageQualificationIsOperationSpecific(t *testing.T) { } // Message validation applies even when no function-result validator exists. raw, _ := json.Marshal(map[string]any{"input": []any{map[string]any{"role": "user", "content": input[0].Content}}}) - batch := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"valid first"}`)}, {Kind: "message", Payload: raw}} - if err := validateProfileInputs(enginetest.Profile(nil), "none", batch); !errors.Is(err, store.ErrInvalidInput) { + batch := []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"valid first"}`)}, {Kind: "message", Payload: raw}} + if err := validateProfileInputs(enginetest.Profile(nil), "none", batch); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("image escaped profile validation", err) } if err := validateProfileInputs(profile, "none", batch); err != nil { @@ -45,7 +45,7 @@ func TestMessageImageQualificationIsOperationSpecific(t *testing.T) { // Whitespace-only text is a declared per-harness qualification, not rewritten input. func TestWhitespaceOnlyTextQualificationUsesEngineProfiles(t *testing.T) { url := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII=" - message := func(messages ...string) store.Input { + message := func(messages ...string) sessions.Input { raw, _ := json.Marshal(map[string]any{"input": func() []any { var input []any for _, text := range messages { @@ -53,12 +53,12 @@ func TestWhitespaceOnlyTextQualificationUsesEngineProfiles(t *testing.T) { } return input }()}) - return store.Input{Kind: "message", Payload: raw} + return sessions.Input{Kind: "message", Payload: raw} } - whitespace := []store.Input{message(" "), message("ok", "\n\t")} + whitespace := []sessions.Input{message(" "), message("ok", "\n\t")} codex, _ := (engine.Catalog{}).Lookup("codex") for _, input := range whitespace { - if err := validateProfileInputs(codex, "none", []store.Input{input}); err != nil { + if err := validateProfileInputs(codex, "none", []sessions.Input{input}); err != nil { t.Fatal(err) } } @@ -66,7 +66,7 @@ func TestWhitespaceOnlyTextQualificationUsesEngineProfiles(t *testing.T) { profile, _ := (engine.Catalog{}).Lookup(kind) for _, input := range whitespace { for _, placement := range []string{"none", "openai_hosted", "self_hosted"} { - if err := validateProfileInputs(profile, placement, []store.Input{input}); !errors.Is(err, ErrWhitespaceOnlyText) { + if err := validateProfileInputs(profile, placement, []sessions.Input{input}); !errors.Is(err, ErrWhitespaceOnlyText) { t.Fatalf("%s %s %s: %v", kind, placement, input.Payload, err) } } @@ -74,13 +74,13 @@ func TestWhitespaceOnlyTextQualificationUsesEngineProfiles(t *testing.T) { } claude, _ := (engine.Catalog{}).Lookup("claude_sdk") // Legacy text payloads use the same rule. - if err := validateProfileInputs(claude, "none", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":" \t"}`)}}); !errors.Is(err, ErrWhitespaceOnlyText) { + if err := validateProfileInputs(claude, "none", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":" \t"}`)}}); !errors.Is(err, ErrWhitespaceOnlyText) { t.Fatal(err) } mixed, _ := json.Marshal(map[string]any{"input": []any{map[string]any{"role": "user", "content": []any{ map[string]any{"type": "input_text", "text": " "}, map[string]any{"type": "input_text", "text": "text"}}}, map[string]any{"role": "user", "content": []any{map[string]any{"type": "input_text", "text": " "}, map[string]any{"type": "input_image", "image_url": url}}}}}) - if err := validateProfileInputs(claude, "none", []store.Input{{Kind: "message", Payload: mixed}}); err != nil { + if err := validateProfileInputs(claude, "none", []sessions.Input{{Kind: "message", Payload: mixed}}); err != nil { t.Fatal("non-whitespace text or image rejected", err) } } diff --git a/services/core/internal/execution/model_execution.go b/services/core/internal/execution/model_execution.go index fea9abaae..ad02143c7 100644 --- a/services/core/internal/execution/model_execution.go +++ b/services/core/internal/execution/model_execution.go @@ -5,10 +5,10 @@ import ( "errors" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -func (d *Dispatcher) sessionModelOptions(ctx context.Context, session store.Session) (map[string]any, error) { +func (d *Dispatcher) sessionModelOptions(ctx context.Context, session sessions.Session) (map[string]any, error) { if d.Store == nil { return nil, errors.New("session model configuration is unavailable") } diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index 6ca2e6354..f6fd32bc6 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -8,18 +8,18 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestSessionModelExecutionNeverFallsBack(t *testing.T) { var d Dispatcher - if _, err := d.executionRequest(t.Context(), store.Session{Engine: "codex"}, Snapshot{ModelProviderConfigured: true}, runtimedevice.KindCapabilities{}, store.SessionExecutionBinding{}); err == nil { + if _, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, Snapshot{ModelProviderConfigured: true}, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); err == nil { t.Fatal("missing Session credentials fell back") } // Hosted and self-hosted Runtimes have no model configuration of their own. for _, environment := range []string{"openai_hosted", "self_hosted"} { snapshot := Snapshot{Environment: &v1.Environment{Type: environment}} - if _, err := d.executionRequest(t.Context(), store.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, store.SessionExecutionBinding{}); !errors.Is(err, ErrModelProviderRequired) { + if _, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, ErrModelProviderRequired) { t.Fatal("provider-free Session dispatched", environment, err) } } @@ -27,7 +27,7 @@ func TestSessionModelExecutionNeverFallsBack(t *testing.T) { // Core sends only the Agent's model and instructions. instructions := "Keep this instruction." snapshot := Snapshot{Agent: v1.Agent{Model: "device-model", Instructions: &instructions}, Environment: &v1.Environment{Type: "none"}} - request, err := d.executionRequest(t.Context(), store.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, store.SessionExecutionBinding{}) + request, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}) if err != nil || !reflect.DeepEqual(request.AgentOptions, map[string]any{"model": "device-model", "system_prompt": &instructions}) { t.Fatal("none Session received adapter options Core does not own", request.AgentOptions, err) } diff --git a/services/core/internal/execution/preparation.go b/services/core/internal/execution/preparation.go index ee5cccca9..59ea9bfb4 100644 --- a/services/core/internal/execution/preparation.go +++ b/services/core/internal/execution/preparation.go @@ -7,7 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -89,7 +89,7 @@ func (p *preparedStart) observation(env proto.Envelope) (proto.PreparationStatus } } -func (d *Dispatcher) awaitPreparation(ctx context.Context, tenant, session string, pending store.EnvironmentInputReservation, prepared *preparedStart) (store.EnvironmentInputReservation, error) { +func (d *Dispatcher) awaitPreparation(ctx context.Context, tenant, session string, pending sessions.EnvironmentInputReservation, prepared *preparedStart) (sessions.EnvironmentInputReservation, error) { tick := time.NewTicker(250 * time.Millisecond) defer tick.Stop() for { @@ -98,7 +98,7 @@ func (d *Dispatcher) awaitPreparation(ctx context.Context, tenant, session strin return pending, ctx.Err() case <-tick.C: current, err := d.Store.ExpireEnvironmentInput(ctx, tenant, session, pending.ID) - if err != nil || current.State != store.EnvironmentInputPending { + if err != nil || current.State != sessions.EnvironmentInputPending { return current, err } case env, ok := <-prepared.sub.Events: diff --git a/services/core/internal/execution/prepared_dispatch.go b/services/core/internal/execution/prepared_dispatch.go index 28def9440..e8d8d8377 100644 --- a/services/core/internal/execution/prepared_dispatch.go +++ b/services/core/internal/execution/prepared_dispatch.go @@ -9,11 +9,10 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) type EnvironmentRun struct { - Reservation store.EnvironmentInputReservation + Reservation sessions.EnvironmentInputReservation Turn sessions.Turn } @@ -24,7 +23,7 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t return run, err } run.Reservation, err = d.Store.ExpireEnvironmentInput(ctx, tenantID, sessionID, reservationID) - if err != nil || run.Reservation.State != store.EnvironmentInputPending { + if err != nil || run.Reservation.State != sessions.EnvironmentInputPending { return run, err } session, err := d.Store.GetSession(ctx, tenantID, sessionID) @@ -37,7 +36,7 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t } var snapshot Snapshot if json.Unmarshal(session.Configuration, &snapshot) != nil || strings.TrimSpace(snapshot.Agent.Model) == "" { - return run, store.ErrInvalidInput + return run, sessions.ErrInvalidInput } if !snapshot.ModelProviderConfigured && snapshot.Environment != nil && v1.ModelProviderRequired(snapshot.Environment.Type) { // Reserved before providers were required; the caller settles it as failed. @@ -64,7 +63,7 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t var messages proto.MessageInput for _, input := range run.Reservation.Inputs { if input.Kind != "message" { - return run, store.ErrInvalidInput + return run, sessions.ErrInvalidInput } text, err := messageInput(input.Payload) if err != nil { @@ -87,14 +86,14 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t return run, err } run.Reservation, err = d.awaitPreparation(owner, tenantID, sessionID, run.Reservation, prepared) - if err != nil || run.Reservation.State != store.EnvironmentInputPending { + if err != nil || run.Reservation.State != sessions.EnvironmentInputPending { return run, err } if err := d.messageInputSupport(peer, session.Engine, snapshot, messages); err != nil { return run, err } promoted, err := d.Store.PromoteEnvironmentInput(owner, tenantID, sessionID, reservationID) - if errors.Is(err, store.ErrTurnConflict) { + if errors.Is(err, sessions.ErrTurnConflict) { // A rejected claim leaves the reservation pending for a later attempt. return run, err } @@ -102,7 +101,7 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t d.notifications.notify(tenantID, sessionID) } run.Reservation = promoted - if err != nil || run.Reservation.State != store.EnvironmentInputAdmitted { + if err != nil || run.Reservation.State != sessions.EnvironmentInputAdmitted { return run, err } if run.Reservation.Receipts[0].Replayed { diff --git a/services/core/internal/execution/recovery_test.go b/services/core/internal/execution/recovery_test.go index 3ed193273..e71df9b27 100644 --- a/services/core/internal/execution/recovery_test.go +++ b/services/core/internal/execution/recovery_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestExistingSessionRecoveryRequiresVerifiedCapability(t *testing.T) { @@ -13,7 +13,7 @@ func TestExistingSessionRecoveryRequiresVerifiedCapability(t *testing.T) { for _, nativeID := range []string{"", "native"} { for _, capable := range []bool{false, true} { wantRecovery := started && nativeID == "" - req, err := (&Dispatcher{}).executionRequest(t.Context(), store.Session{ID: "session", Engine: engine}, Snapshot{}, runtimedevice.KindCapabilities{NativeSessionRecovery: capable}, store.SessionExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) + req, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{}, runtimedevice.KindCapabilities{NativeSessionRecovery: capable}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) if wantRecovery && !capable { if err == nil { t.Fatal("unverified recovery admitted", engine) diff --git a/services/core/internal/execution/request.go b/services/core/internal/execution/request.go index 4d1a2e8dd..82ba8ade8 100644 --- a/services/core/internal/execution/request.go +++ b/services/core/internal/execution/request.go @@ -8,7 +8,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" ) @@ -16,7 +16,7 @@ import ( // frozen model provider and therefore cannot run. var ErrModelProviderRequired = errors.New("the Session has no model provider") -func (d *Dispatcher) executionRequest(ctx context.Context, session store.Session, snapshot Snapshot, caps runtimedevice.KindCapabilities, bound store.SessionExecutionBinding) (proto.PromptRequestPayload, error) { +func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Session, snapshot Snapshot, caps runtimedevice.KindCapabilities, bound sessions.ExecutionBinding) (proto.PromptRequestPayload, error) { recoverNativeSession := bound.HasStartedTurn && bound.NativeSessionID == "" if recoverNativeSession && !caps.NativeSessionRecovery { return proto.PromptRequestPayload{}, errors.New("native session recovery is unavailable") diff --git a/services/core/internal/execution/runtime_compute.go b/services/core/internal/execution/runtime_compute.go index 42bac479b..858588f7c 100644 --- a/services/core/internal/execution/runtime_compute.go +++ b/services/core/internal/execution/runtime_compute.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -175,7 +176,7 @@ func (r *runtimeLifecycle) idleCompute(ctx context.Context, p sandbox.Checkpoint // The Session-locked phase commit checks pending work and wake requests. // A competing request keeps its queue position and resumes this source. suspending, err := r.saveCompute(ctx, next, "suspending", state, &until) - if errors.Is(err, store.ErrTurnConflict) { + if errors.Is(err, sessions.ErrTurnConflict) { state.Rollback = true next, err = r.saveCompute(ctx, next, "waking", state, &until) if err != nil { diff --git a/services/core/internal/execution/runtime_compute_wake.go b/services/core/internal/execution/runtime_compute_wake.go index 3692f3580..a5dbd35f9 100644 --- a/services/core/internal/execution/runtime_compute_wake.go +++ b/services/core/internal/execution/runtime_compute_wake.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -19,7 +20,7 @@ func (r *runtimeLifecycle) wakeCompute(ctx context.Context, p sandbox.Checkpoint } peer, err := authorizedRuntimePeer(ctx, r.store, r.registry, owner.DeviceID) if err != nil { - if !errors.Is(err, store.ErrNotFound) && !errors.Is(err, runtimegateway.ErrDeviceNotRegistered) && !errors.Is(err, runtimegateway.ErrSessionClosed) { + if !errors.Is(err, sessions.ErrNotFound) && !errors.Is(err, runtimegateway.ErrDeviceNotRegistered) && !errors.Is(err, runtimegateway.ErrSessionClosed) { return err } // This idempotent control signal is fenced by guest PID/start time and the @@ -103,12 +104,12 @@ func (r *runtimeLifecycle) cleanupCompute(ctx context.Context, p sandbox.Checkpo // waitRuntimeAwake is called only for live Environment file operations, before // entering the Worker's work queues. Persisted history/artifact reads bypass it. -func (w *Worker) waitRuntimeAwake(ctx context.Context, environment store.Environment) error { +func (w *Worker) waitRuntimeAwake(ctx context.Context, environment sessions.Environment) error { if w.runtimes == nil { return nil } owner, err := w.admission.GetRuntimeAllocation(ctx, environment.TenantID, environment.ID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return nil } if err != nil { diff --git a/services/core/internal/execution/runtime_connections.go b/services/core/internal/execution/runtime_connections.go index a074dff05..1b58ed7e9 100644 --- a/services/core/internal/execution/runtime_connections.go +++ b/services/core/internal/execution/runtime_connections.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -21,21 +22,21 @@ type runtimeConnection struct { func (r *runtimeLifecycle) observeConnection(ctx context.Context, owner store.RuntimeAllocation) error { bound, err := r.store.GetSessionRuntimeDevice(ctx, owner.TenantID, owner.SessionID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return nil } if err != nil { return err } if bound.ID != owner.DeviceID || bound.EnvironmentID != owner.EnvironmentID { - return store.ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } if !owner.CreateSettled || owner.State != "running" { return nil } peer, err := authorizedRuntimePeer(ctx, r.store, r.registry, owner.DeviceID) connected := err == nil - if err != nil && !errors.Is(err, store.ErrNotFound) && !errors.Is(err, runtimegateway.ErrSessionClosed) && !errors.Is(err, runtimegateway.ErrDeviceNotRegistered) { + if err != nil && !errors.Is(err, sessions.ErrNotFound) && !errors.Is(err, runtimegateway.ErrSessionClosed) && !errors.Is(err, runtimegateway.ErrDeviceNotRegistered) { return err } return observeRuntimeConnection(ctx, r.store, r.connections, owner.TenantID, owner.EnvironmentID, peer, connected) @@ -74,11 +75,11 @@ func (w *Worker) observeEnrolledRuntimes(ctx context.Context) error { live[bound.EnvironmentID] = true peer, err := w.dispatcher.authorizedPeer(ctx, bound.DeviceID) connected := err == nil - if err != nil && !errors.Is(err, store.ErrNotFound) && !errors.Is(err, runtimegateway.ErrSessionClosed) && !errors.Is(err, runtimegateway.ErrDeviceNotRegistered) { + if err != nil && !errors.Is(err, sessions.ErrNotFound) && !errors.Is(err, runtimegateway.ErrSessionClosed) && !errors.Is(err, runtimegateway.ErrDeviceNotRegistered) { return err } if err := observeRuntimeConnection(ctx, w.dispatcher.Store, w.enrolledConnections, bound.TenantID, bound.EnvironmentID, peer, connected); err != nil { - if errors.Is(err, store.ErrNotFound) || errors.Is(err, store.ErrInvalidInput) { + if errors.Is(err, sessions.ErrNotFound) || errors.Is(err, sessions.ErrInvalidInput) { continue } return err diff --git a/services/core/internal/execution/runtime_initialization.go b/services/core/internal/execution/runtime_initialization.go index 8d7147a02..5f236e894 100644 --- a/services/core/internal/execution/runtime_initialization.go +++ b/services/core/internal/execution/runtime_initialization.go @@ -11,7 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // The leased Worker owns preparation for every Environment. Compute managers @@ -48,7 +48,7 @@ func (w *Worker) runEnvironmentInitializations(ctx context.Context) error { } if owner.State == "running" { // Lost process-local progress cannot prove which side effects ran. - if err := w.dispatcher.Store.FailEnvironmentInitialization(ctx, owner, store.ProvisioningFailure{}); err != nil && !errors.Is(err, store.ErrNotFound) { + if err := w.dispatcher.Store.FailEnvironmentInitialization(ctx, owner, sessions.ProvisioningFailure{}); err != nil && !errors.Is(err, sessions.ErrNotFound) { return err } continue @@ -58,7 +58,7 @@ func (w *Worker) runEnvironmentInitializations(ctx context.Context) error { } peer, err := w.dispatcher.authorizedPeer(ctx, owner.DeviceID) if err != nil { - if errors.Is(err, store.ErrNotFound) || errors.Is(err, runtimegateway.ErrSessionClosed) || errors.Is(err, runtimegateway.ErrDeviceNotRegistered) { + if errors.Is(err, sessions.ErrNotFound) || errors.Is(err, runtimegateway.ErrSessionClosed) || errors.Is(err, runtimegateway.ErrDeviceNotRegistered) { continue } return err @@ -68,20 +68,20 @@ func (w *Worker) runEnvironmentInitializations(ctx context.Context) error { continue } if !found || !harness.Available { - if err := w.dispatcher.Store.FailEnvironmentInitialization(ctx, owner, store.ProvisioningFailure{Step: store.ProvisioningHarness}); err != nil && !errors.Is(err, store.ErrNotFound) { + if err := w.dispatcher.Store.FailEnvironmentInitialization(ctx, owner, sessions.ProvisioningFailure{Step: sessions.ProvisioningHarness}); err != nil && !errors.Is(err, sessions.ErrNotFound) { return err } continue } if err := w.dispatcher.Store.ClaimEnvironmentInitialization(ctx, owner); err != nil { - if errors.Is(err, store.ErrNotFound) || errors.Is(err, store.ErrTurnConflict) { + if errors.Is(err, sessions.ErrNotFound) || errors.Is(err, sessions.ErrTurnConflict) { continue } return err } active[owner.EnvironmentID] = true running.Add(1) - go func(owner store.EnvironmentInitialization) { + go func(owner sessions.EnvironmentInitialization) { defer running.Done() w.initializeEnvironment(ctx, owner) done <- owner.EnvironmentID @@ -90,10 +90,10 @@ func (w *Worker) runEnvironmentInitializations(ctx context.Context) error { } } -func (w *Worker) initializeEnvironment(ctx context.Context, owner store.EnvironmentInitialization) { +func (w *Worker) initializeEnvironment(ctx context.Context, owner sessions.EnvironmentInitialization) { operation, cancel := context.WithTimeout(ctx, 30*time.Minute) defer cancel() - failure := store.ProvisioningFailure{} + failure := sessions.ProvisioningFailure{} err := w.prepareEnvironment(operation, owner, &failure) if err == nil { err = w.dispatcher.Store.CompleteEnvironmentInitialization(operation, owner) @@ -107,7 +107,7 @@ func (w *Worker) initializeEnvironment(ctx context.Context, owner store.Environm } } -func (w *Worker) prepareEnvironment(ctx context.Context, owner store.EnvironmentInitialization, failure *store.ProvisioningFailure) error { +func (w *Worker) prepareEnvironment(ctx context.Context, owner sessions.EnvironmentInitialization, failure *sessions.ProvisioningFailure) error { environment, err := w.dispatcher.Store.GetEnvironment(ctx, owner.TenantID, owner.EnvironmentID) if err != nil { return err @@ -116,7 +116,7 @@ func (w *Worker) prepareEnvironment(ctx context.Context, owner store.Environment Files []environmentconfig.InitialFileMetadata `json:"files"` } if json.Unmarshal(environment.Configuration, &cfg) != nil || len(cfg.Files) > 50 { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } setup, err := w.dispatcher.Store.ReadEnvironmentSetup(ctx, owner.TenantID, owner.SessionID) if err != nil { @@ -138,7 +138,7 @@ func (w *Worker) prepareEnvironment(ctx context.Context, owner store.Environment err = errors.New("Runtime connection changed during initialization") } } - candidate := store.ProvisioningFailure{Step: store.ProvisioningInitialFile} + candidate := sessions.ProvisioningFailure{Step: sessions.ProvisioningInitialFile} if err == nil && index < len(cfg.Files) { var metadata environmentconfig.InitialFileMetadata var body []byte diff --git a/services/core/internal/execution/runtime_lifecycle.go b/services/core/internal/execution/runtime_lifecycle.go index 0a9fee385..981506c6d 100644 --- a/services/core/internal/execution/runtime_lifecycle.go +++ b/services/core/internal/execution/runtime_lifecycle.go @@ -16,6 +16,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -180,7 +181,7 @@ func (r *runtimeLifecycle) provision(ctx context.Context, tenant, environment, p if err != nil || placement.Type != "openai_hosted" { return store.RuntimeAllocation{}, sandbox.ErrInvalid } - if _, err := r.store.GetRuntimeAllocation(ctx, tenant, environment); errors.Is(err, store.ErrNotFound) { + if _, err := r.store.GetRuntimeAllocation(ctx, tenant, environment); errors.Is(err, sessions.ErrNotFound) { if r.config.AdmissionPaused && r.config.Generation == 0 { return store.RuntimeAllocation{}, ErrExecutionUnavailable } diff --git a/services/core/internal/execution/runtime_setup.go b/services/core/internal/execution/runtime_setup.go index 945ee8589..d463d8a02 100644 --- a/services/core/internal/execution/runtime_setup.go +++ b/services/core/internal/execution/runtime_setup.go @@ -7,7 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -23,16 +23,16 @@ type runtimeStepFailure struct{ exitCode int } func (*runtimeStepFailure) Error() string { return "environment initialization operation failed" } -func (operation runtimeSetupOperation) provisioningFailure(exitCode int) store.ProvisioningFailure { +func (operation runtimeSetupOperation) provisioningFailure(exitCode int) sessions.ProvisioningFailure { action := operation.Request.Action if operation.Request.Initialization != nil { action = operation.Request.Initialization.Action } switch action { case "setup", "python", "npm", "skill": - return store.ProvisioningFailure{Step: action, Index: operation.Index, ExitCode: exitCode} + return sessions.ProvisioningFailure{Step: action, Index: operation.Index, ExitCode: exitCode} } - return store.ProvisioningFailure{} + return sessions.ProvisioningFailure{} } func setupOperations(setup environmentconfig.Setup) []runtimeSetupOperation { diff --git a/services/core/internal/execution/runtime_setup_test.go b/services/core/internal/execution/runtime_setup_test.go index 877462464..be2a99461 100644 --- a/services/core/internal/execution/runtime_setup_test.go +++ b/services/core/internal/execution/runtime_setup_test.go @@ -9,7 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) const setupCanary = "CANARY-runtime-setup-9b3e" @@ -54,7 +54,7 @@ func TestRuntimeSetupReceiptOutcomes(t *testing.T) { } } func TestRuntimeSetupFailureLabels(t *testing.T) { - for action, want := range map[string]store.ProvisioningFailure{"setup": {Step: store.ProvisioningSetupCommand, Index: 2, ExitCode: 3}, "python": {Step: store.ProvisioningPythonPackages, Index: 2, ExitCode: 3}, "npm": {Step: store.ProvisioningNPMPackages, Index: 2, ExitCode: 3}, "skill": {Step: store.ProvisioningSkill, Index: 2, ExitCode: 3}, "configure": {}, "": {}} { + for action, want := range map[string]sessions.ProvisioningFailure{"setup": {Step: sessions.ProvisioningSetupCommand, Index: 2, ExitCode: 3}, "python": {Step: sessions.ProvisioningPythonPackages, Index: 2, ExitCode: 3}, "npm": {Step: sessions.ProvisioningNPMPackages, Index: 2, ExitCode: 3}, "skill": {Step: sessions.ProvisioningSkill, Index: 2, ExitCode: 3}, "configure": {}, "": {}} { op := runtimeSetupOperation{Request: proto.RuntimePreparePayload{Action: action}, Index: 2} if action != "skill" { op.Request = proto.RuntimePreparePayload{Action: "initialize", Initialization: &proto.RuntimeInitialization{Action: action}} diff --git a/services/core/internal/execution/runtime_wake_hint.go b/services/core/internal/execution/runtime_wake_hint.go index 8c41d2c52..852cf7be1 100644 --- a/services/core/internal/execution/runtime_wake_hint.go +++ b/services/core/internal/execution/runtime_wake_hint.go @@ -4,12 +4,12 @@ import ( "context" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // A hint only accelerates observation of an already committed input. Lookup or // delivery failure leaves that input for the normal maintenance scan. -func (w *Worker) hintRuntimeWake(ctx context.Context, session store.Session) { +func (w *Worker) hintRuntimeWake(ctx context.Context, session sessions.Session) { r := w.runtimes if r == nil { return diff --git a/services/core/internal/execution/sandbox_reset.go b/services/core/internal/execution/sandbox_reset.go index 493f645fd..bf5b437b9 100644 --- a/services/core/internal/execution/sandbox_reset.go +++ b/services/core/internal/execution/sandbox_reset.go @@ -7,6 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -107,7 +108,7 @@ func (m *runtimeManager) resetPage(parent, ctx context.Context) error { } _, err := m.store.ArchiveSandboxResetSession(ctx, candidate.TenantID, candidate.SessionID, current.Generation, current.Reset.RequestedAt) m.resetCursor = candidate.SessionID - if err != nil && !errors.Is(err, store.ErrSandboxResetSessionBusy) && !errors.Is(err, store.ErrNotFound) { + if err != nil && !errors.Is(err, store.ErrSandboxResetSessionBusy) && !errors.Is(err, sessions.ErrNotFound) { // Do not log a provider body, request, credential or stored provenance. log.Warn(ctx, "Sandbox reset archive remains pending", "session_id", candidate.SessionID) } diff --git a/services/core/internal/execution/session_archive.go b/services/core/internal/execution/session_archive.go index 269e5192d..65ed5ba16 100644 --- a/services/core/internal/execution/session_archive.go +++ b/services/core/internal/execution/session_archive.go @@ -3,11 +3,11 @@ package execution import ( "context" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // ArchiveManagedSession records administrative cleanup intent through the same // execution owner that settles resources. Provider work remains in the lifecycle. -func (w *Worker) ArchiveManagedSession(ctx context.Context, tenant, session string, generation uint64) (store.ManagedSessionArchive, error) { +func (w *Worker) ArchiveManagedSession(ctx context.Context, tenant, session string, generation uint64) (sessions.ManagedArchive, error) { return w.dispatcher.Store.ArchiveManagedSession(ctx, tenant, session, generation) } diff --git a/services/core/internal/execution/structured_output_test.go b/services/core/internal/execution/structured_output_test.go index 7d3ecd9a3..bff723dd3 100644 --- a/services/core/internal/execution/structured_output_test.go +++ b/services/core/internal/execution/structured_output_test.go @@ -10,7 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestStructuredOutputNeedsOperationQualification(t *testing.T) { @@ -32,7 +32,7 @@ func TestStructuredOutputRequestKeepsFrozenSchemaAndInstructions(t *testing.T) { schema := json.RawMessage(`{"type":"object","properties":{"number":{"const":9007199254740992}}}`) instructions := "Keep these original instructions." snapshot := Snapshot{Agent: v1.Agent{Model: "model", Instructions: &instructions, Text: v1.TextConfig{Format: v1.TextFormat{Type: "json_schema", Schema: schema}}}} - request, err := (&Dispatcher{}).executionRequest(context.Background(), store.Session{}, snapshot, runtimedevice.KindCapabilities{MessageItems: true}, store.SessionExecutionBinding{}) + request, err := (&Dispatcher{}).executionRequest(context.Background(), sessions.Session{}, snapshot, runtimedevice.KindCapabilities{MessageItems: true}, sessions.ExecutionBinding{}) if err != nil || request.ExecutionControls.OutputFormat == nil { t.Fatal(err) } diff --git a/services/core/internal/execution/support.go b/services/core/internal/execution/support.go index 6caefc3cb..388a4d544 100644 --- a/services/core/internal/execution/support.go +++ b/services/core/internal/execution/support.go @@ -8,18 +8,18 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // ValidateSessionConfiguration checks engine placement and configuration before persistence. func (p Policy) ValidateSessionConfiguration(engine string, configuration json.RawMessage) error { var snapshot Snapshot if json.Unmarshal(configuration, &snapshot) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } profile, ok := p.Engines.Lookup(engine) if !ok || (snapshot.Environment != nil && !profile.Accepts(snapshot.Environment.Type)) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } _, err := p.mcpCredentialBindings(engine, snapshot) if err != nil { @@ -27,18 +27,18 @@ func (p Policy) ValidateSessionConfiguration(engine string, configuration json.R } if snapshot.Environment != nil && snapshot.Environment.Type == "self_hosted" { if strings.TrimSpace(snapshot.Agent.Model) == "" || !validSelfHostedPlacement(environmentPlacement{WorkspaceDirectory: snapshot.Environment.WorkspaceDirectory, CapabilityDirectories: snapshot.Environment.CapabilityDirectories}) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } } if snapshot.Environment != nil && snapshot.Environment.Type == "openai_hosted" { // Only this placement/engine combination has current native qualification. // Runtime capability checks still apply before any execution claim. if strings.TrimSpace(snapshot.Agent.Model) == "" { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } configuration, err := json.Marshal(snapshot.Environment) if err != nil || !LocalWorkspaceConfiguration(configuration) { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } } if snapshot.Environment != nil && snapshot.Environment.Type != "none" { @@ -60,14 +60,14 @@ func (p Policy) canAdmitInputs(engine string, configuration json.RawMessage) boo return p.ValidateSessionConfiguration(engine, configuration) == nil } -func (p Policy) validateEngineInputs(engine string, configuration json.RawMessage, inputs []store.Input) error { +func (p Policy) validateEngineInputs(engine string, configuration json.RawMessage, inputs []sessions.Input) error { profile, ok := p.Engines.Lookup(engine) if !ok { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } var snapshot Snapshot if json.Unmarshal(configuration, &snapshot) != nil { - return store.ErrInvalidInput + return sessions.ErrInvalidInput } placement := "" if snapshot.Environment != nil { diff --git a/services/core/internal/execution/worker.go b/services/core/internal/execution/worker.go index 45eb241c3..731fe7419 100644 --- a/services/core/internal/execution/worker.go +++ b/services/core/internal/execution/worker.go @@ -113,7 +113,7 @@ func (w *Worker) CheckOwnership(ctx context.Context) error { return err } -func (w *Worker) SubmitInputs(ctx context.Context, tenant, session, key string, inputs []store.Input) ([]store.InputReceipt, error) { +func (w *Worker) SubmitInputs(ctx context.Context, tenant, session, key string, inputs []sessions.Input) ([]sessions.InputReceipt, error) { value, err := w.admission.GetSession(ctx, tenant, session) if err != nil { return nil, err @@ -125,15 +125,15 @@ func (w *Worker) SubmitInputs(ctx context.Context, tenant, session, key string, return w.submitEnvironmentInputs(ctx, value, key, inputs) } if !w.dispatcher.canAdmitInputs(value.Engine, value.Configuration) { - return nil, store.ErrInvalidInput + return nil, sessions.ErrInvalidInput } return w.admitInputs(ctx, tenant, session, key, inputs) } // CreateSession validates execution support before reserving or admitting initial work. -func (w *Worker) CreateSession(ctx context.Context, tenant string, input store.CreateSessionInput) (store.Session, error) { +func (w *Worker) CreateSession(ctx context.Context, tenant string, input sessions.CreateSession) (sessions.Session, error) { if err := w.validateCreation(ctx, input); err != nil { - return store.Session{}, err + return sessions.Session{}, err } session, err := w.admission.CreateSession(ctx, tenant, input) if err == nil && len(input.InitialInputs) > 0 { @@ -143,9 +143,9 @@ func (w *Worker) CreateSession(ctx context.Context, tenant string, input store.C } // CreateSessionStream applies the same execution admission before creating a stream. -func (w *Worker) CreateSessionStream(ctx context.Context, tenant string, input store.CreateSessionInput) (store.SessionCreation, error) { +func (w *Worker) CreateSessionStream(ctx context.Context, tenant string, input sessions.CreateSession) (sessions.Creation, error) { if err := w.validateCreation(ctx, input); err != nil { - return store.SessionCreation{}, err + return sessions.Creation{}, err } creation, err := w.admission.CreateSessionStream(ctx, tenant, input) if err == nil && len(input.InitialInputs) > 0 { @@ -352,8 +352,8 @@ func (w *Worker) reconcile(ctx context.Context) error { return nil } for _, item := range work { - _, err := w.dispatcher.Store.TransitionTurn(ctx, item.TenantID, item.SessionID, item.TurnID, store.TurnTransition{ExpectedStatus: item.Status, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"execution_interrupted"}`)}) - if err != nil && !errors.Is(err, store.ErrTurnConflict) { + _, err := w.dispatcher.Store.TransitionTurn(ctx, item.TenantID, item.SessionID, item.TurnID, sessions.TurnTransition{ExpectedStatus: item.Status, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"execution_interrupted"}`)}) + if err != nil && !errors.Is(err, sessions.ErrTurnConflict) { return err } cursor = item.TurnID @@ -361,9 +361,9 @@ func (w *Worker) reconcile(ctx context.Context) error { } } -func (w *Worker) runClaim(ctx context.Context, item store.ExecutionWork) error { +func (w *Worker) runClaim(ctx context.Context, item sessions.ExecutionWork) error { _, err := w.dispatcher.Run(ctx, item.TenantID, item.SessionID, item.TurnID) - if err == nil || errors.Is(err, store.ErrTurnConflict) { + if err == nil || errors.Is(err, sessions.ErrTurnConflict) { return nil } var rejection *preparationRejection @@ -387,8 +387,8 @@ func (w *Worker) runClaim(ctx context.Context, item store.ExecutionWork) error { return nil } log.Ctx(ctx).Error("oac-core dispatch did not complete", "turn_id", item.TurnID) - _, err = w.dispatcher.Store.TransitionTurn(finish, item.TenantID, item.SessionID, item.TurnID, store.TurnTransition{ExpectedStatus: turn.Status, Status: sessions.TurnFailed, Outcome: outcome}) - if errors.Is(err, store.ErrTurnConflict) { + _, err = w.dispatcher.Store.TransitionTurn(finish, item.TenantID, item.SessionID, item.TurnID, sessions.TurnTransition{ExpectedStatus: turn.Status, Status: sessions.TurnFailed, Outcome: outcome}) + if errors.Is(err, sessions.ErrTurnConflict) { return nil } return err diff --git a/services/core/internal/execution/worker_device.go b/services/core/internal/execution/worker_device.go index 83e3df3ef..6999868e9 100644 --- a/services/core/internal/execution/worker_device.go +++ b/services/core/internal/execution/worker_device.go @@ -7,18 +7,17 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) -func (w *Worker) bind(ctx context.Context, item store.ExecutionWork) (bool, error) { +func (w *Worker) bind(ctx context.Context, item sessions.ExecutionWork) (bool, error) { input, _, inputErr := w.dispatcher.initialInput(ctx, item.TenantID, item.SessionID, item.TurnID) - if inputErr != nil && !errors.Is(inputErr, store.ErrInvalidInput) && !errors.Is(inputErr, store.ErrNotFound) { + if inputErr != nil && !errors.Is(inputErr, sessions.ErrInvalidInput) && !errors.Is(inputErr, sessions.ErrNotFound) { return false, inputErr } // Candidate selection is a snapshot. Cancellation can append a control input // before this read, so recheck eligibility after reading the input history. turn, err := w.dispatcher.Store.GetTurn(ctx, item.TenantID, item.SessionID, item.TurnID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return false, nil } if err != nil { @@ -31,14 +30,14 @@ func (w *Worker) bind(ctx context.Context, item store.ExecutionWork) (bool, erro return false, inputErr } ready, err := w.bindDevice(ctx, item.TenantID, item.SessionID, input) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return false, nil } - if !errors.Is(err, store.ErrDeviceBindingConflict) { + if !errors.Is(err, sessions.ErrDeviceBindingConflict) { return ready, err } - _, err = w.dispatcher.Store.TransitionTurn(ctx, item.TenantID, item.SessionID, item.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"execution_device_unavailable"}`)}) - if errors.Is(err, store.ErrTurnConflict) { + _, err = w.dispatcher.Store.TransitionTurn(ctx, item.TenantID, item.SessionID, item.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"execution_device_unavailable"}`)}) + if errors.Is(err, sessions.ErrTurnConflict) { err = nil } return false, err @@ -46,7 +45,7 @@ func (w *Worker) bind(ctx context.Context, item store.ExecutionWork) (bool, erro func (w *Worker) bindDevice(ctx context.Context, tenantID, sessionID string, input proto.MessageInput) (bool, error) { session, err := w.dispatcher.Store.GetSession(ctx, tenantID, sessionID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return false, nil } if err != nil { @@ -68,10 +67,10 @@ func (w *Worker) bindDevice(ctx context.Context, tenantID, sessionID string, inp }) } -func (w *Worker) bindSessionDevice(ctx context.Context, session store.Session, ready func(string) bool) (bool, error) { +func (w *Worker) bindSessionDevice(ctx context.Context, session sessions.Session, ready func(string) bool) (bool, error) { var snapshot Snapshot if json.Unmarshal(session.Configuration, &snapshot) != nil { - return false, store.ErrInvalidInput + return false, sessions.ErrInvalidInput } if snapshot.Environment != nil && (snapshot.Environment.Type == "openai_hosted" || snapshot.Environment.Type == "self_hosted") { environment, err := w.dispatcher.Store.GetSessionEnvironment(ctx, session.TenantID, session.ID) @@ -82,7 +81,7 @@ func (w *Worker) bindSessionDevice(ctx context.Context, session store.Session, r return false, nil } allocation, err := w.dispatcher.Store.GetRuntimeAllocation(ctx, session.TenantID, environment.ID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { if snapshot.Environment.Type == "openai_hosted" { return false, nil } @@ -94,7 +93,7 @@ func (w *Worker) bindSessionDevice(ctx context.Context, session store.Session, r return false, nil } bound, err := w.dispatcher.Store.GetSessionDevice(ctx, session.TenantID, session.ID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return false, nil } return err == nil && environmentDeviceMatches(session, environment, bound) && ready(bound.ID), err @@ -103,7 +102,7 @@ func (w *Worker) bindSessionDevice(ctx context.Context, session store.Session, r if err == nil { return bound.EnvironmentID == "" && ready(bound.ID), nil } - if !errors.Is(err, store.ErrNotFound) { + if !errors.Is(err, sessions.ErrNotFound) { return false, err } devices, err := w.dispatcher.Store.ListExecutionDevices(ctx, session.TenantID) diff --git a/services/core/internal/execution/worker_schedule.go b/services/core/internal/execution/worker_schedule.go index 80534e7dc..2a901f5f2 100644 --- a/services/core/internal/execution/worker_schedule.go +++ b/services/core/internal/execution/worker_schedule.go @@ -7,7 +7,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) type workerSchedule struct { @@ -17,7 +16,7 @@ type workerSchedule struct { } type scheduledWork struct { - store.ExecutionWork + sessions.ExecutionWork reservationID string } @@ -33,7 +32,7 @@ func (s *workerSchedule) selectWork(ctx context.Context, w *Worker, devices []st return nil, err } } - var environments []store.EnvironmentInputWork + var environments []sessions.EnvironmentInputWork if !time.Now().Before(s.nextEnvironmentScan) { environments, err = w.dispatcher.Store.ListEnvironmentInputWork(ctx, s.environmentCursor, devices) if err != nil { @@ -57,7 +56,7 @@ func (s *workerSchedule) selectWork(ctx context.Context, w *Worker, devices []st value := environments[0] environments = environments[1:] s.environmentCursor = value.ReservationID - item = scheduledWork{ExecutionWork: store.ExecutionWork{TenantID: value.TenantID, SessionID: value.SessionID}, reservationID: value.ReservationID} + item = scheduledWork{ExecutionWork: sessions.ExecutionWork{TenantID: value.TenantID, SessionID: value.SessionID}, reservationID: value.ReservationID} s.environmentFirst = false } else { item.ExecutionWork = turns[0] @@ -73,7 +72,7 @@ func (s *workerSchedule) selectWork(ctx context.Context, w *Worker, devices []st ready, err = w.bind(ctx, item.ExecutionWork) } else { ready, err = w.bindDevice(ctx, item.TenantID, item.SessionID, nil) - if errors.Is(err, store.ErrNotFound) || errors.Is(err, store.ErrDeviceBindingConflict) { + if errors.Is(err, sessions.ErrNotFound) || errors.Is(err, sessions.ErrDeviceBindingConflict) { continue } } @@ -97,16 +96,16 @@ func (w *Worker) runEnvironmentInput(ctx context.Context, item scheduledWork) er if errors.Is(err, ErrModelProviderRequired) { return w.dispatcher.Store.FailEnvironmentInput(ctx, item.TenantID, item.SessionID, item.reservationID, "model_provider_required") } - if errors.Is(err, errPreparationFailed) && run.Reservation.State == store.EnvironmentInputPending { + if errors.Is(err, errPreparationFailed) && run.Reservation.State == sessions.EnvironmentInputPending { return w.dispatcher.Store.FailEnvironmentInput(ctx, item.TenantID, item.SessionID, item.reservationID, "runtime_preparation_failed") } - if run.Reservation.State == store.EnvironmentInputAdmitted { + if run.Reservation.State == sessions.EnvironmentInputAdmitted { return err } - if run.Reservation.State != store.EnvironmentInputPending && !errors.Is(err, store.ErrNotFound) { + if run.Reservation.State != sessions.EnvironmentInputPending && !errors.Is(err, sessions.ErrNotFound) { return err } - if ctx.Err() == nil && !errors.Is(err, store.ErrNotFound) { + if ctx.Err() == nil && !errors.Is(err, sessions.ErrNotFound) { log.Ctx(ctx).Warn("oac-core environment preparation did not complete", "reservation_id", item.reservationID) } return nil diff --git a/services/core/internal/execution/worker_wakeup.go b/services/core/internal/execution/worker_wakeup.go index 3c90a1f88..403e2bead 100644 --- a/services/core/internal/execution/worker_wakeup.go +++ b/services/core/internal/execution/worker_wakeup.go @@ -3,7 +3,7 @@ package execution import ( "context" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // wakeScheduler only hints at committed work. The existing loop retains lease, @@ -15,7 +15,7 @@ func (w *Worker) wakeScheduler() { } } -func (w *Worker) admitInputs(ctx context.Context, tenant, session, key string, inputs []store.Input) ([]store.InputReceipt, error) { +func (w *Worker) admitInputs(ctx context.Context, tenant, session, key string, inputs []sessions.Input) ([]sessions.InputReceipt, error) { receipts, err := w.admission.SubmitInputs(ctx, tenant, session, key, inputs) if err == nil { w.wakeScheduler() diff --git a/services/core/internal/execution/workspace_images_test.go b/services/core/internal/execution/workspace_images_test.go index 85e71c473..1132490e9 100644 --- a/services/core/internal/execution/workspace_images_test.go +++ b/services/core/internal/execution/workspace_images_test.go @@ -6,17 +6,17 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestImageQualificationIsIndependentOfEnvironmentSource(t *testing.T) { image := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII=" - message := store.Input{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"inspect"},{"type":"input_image","image_url":"` + image + `"}]}]}`)} - result := store.Input{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":[{"type":"input_image","image_url":"` + image + `"}]}}`)} + message := sessions.Input{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"inspect"},{"type":"input_image","image_url":"` + image + `"}]}]}`)} + result := sessions.Input{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":[{"type":"input_image","image_url":"` + image + `"}]}}`)} for _, kind := range []string{"codex", "claude_sdk", "mcode"} { profile, _ := (engine.Catalog{}).Lookup(kind) for _, placement := range []string{"none", "openai_hosted", "self_hosted"} { - err := validateProfileInputs(profile, placement, []store.Input{message}) + err := validateProfileInputs(profile, placement, []sessions.Input{message}) want := kind != "mcode" if (err == nil) != want { t.Fatalf("%s/%s: %v", kind, placement, err) @@ -24,11 +24,11 @@ func TestImageQualificationIsIndependentOfEnvironmentSource(t *testing.T) { } } profile, _ := (engine.Catalog{}).Lookup("claude_sdk") - if err := validateProfileInputs(profile, "openai_hosted", []store.Input{result}); err != nil { + if err := validateProfileInputs(profile, "openai_hosted", []sessions.Input{result}); err != nil { t.Fatal(err) } result.Payload = json.RawMessage(`{"call_id":"call","result":{"success":false,"output":[{"type":"input_image","image_url":"` + image + `"}]}}`) - if err := validateProfileInputs(profile, "openai_hosted", []store.Input{result}); !errors.Is(err, store.ErrInvalidInput) { + if err := validateProfileInputs(profile, "openai_hosted", []sessions.Input{result}); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("native failed image admitted", err) } } diff --git a/services/core/internal/persistence/postgres/modelconfigurationpg/fixture_test.go b/services/core/internal/persistence/postgres/modelconfigurationpg/fixture_test.go index e054af589..a4fb2120f 100644 --- a/services/core/internal/persistence/postgres/modelconfigurationpg/fixture_test.go +++ b/services/core/internal/persistence/postgres/modelconfigurationpg/fixture_test.go @@ -18,6 +18,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/modelconfigurationpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -84,8 +85,8 @@ type observed struct { fixture sessions *store.Store tenant string - input store.CreateSessionInput - session store.Session + input sessions.CreateSession + session sessions.Session } func newObserved(t *testing.T) observed { @@ -94,7 +95,7 @@ func newObserved(t *testing.T) observed { f.replace(t, fixtureProvider) snapshot := f.resolve(t) model, harness := "frozen-model", "codex" - input := store.CreateSessionInput{ + input := sessions.CreateSession{ Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: harness, IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"frozen-model"},"environment":{"type":"none"}}`), ModelProvider: snapshot.Provider, ModelProviderSource: "deployment", DeploymentProviderRevision: snapshot.Revision, diff --git a/services/core/internal/runtimeenrollment/connection.go b/services/core/internal/runtimeenrollment/connection.go index ae35cdee5..8cbb2b0f6 100644 --- a/services/core/internal/runtimeenrollment/connection.go +++ b/services/core/internal/runtimeenrollment/connection.go @@ -11,13 +11,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type ConnectionStore interface { AuthenticateEnvironmentExecutor(context.Context, string, string) (string, error) - GetEnvironment(context.Context, string, string) (store.Environment, error) - GetSessionDevice(context.Context, string, string) (store.ExecutionDevice, error) + GetEnvironment(context.Context, string, string) (sessions.Environment, error) + GetSessionDevice(context.Context, string, string) (sessions.ExecutionDevice, error) GetDeviceCredential(context.Context, string) (runtimedevice.Credential, bool, error) } @@ -48,9 +48,9 @@ func ConnectionHandler(s ConnectionStore, registry *runtimegateway.Registry) htt defer cancel() connected, err := RuntimeConnected(ctx, s, registry, environment, digest) switch { - case errors.Is(err, store.ErrNotFound): + case errors.Is(err, sessions.ErrNotFound): fail(http.StatusUnauthorized) - case errors.Is(err, store.ErrDeviceBindingConflict): + case errors.Is(err, sessions.ErrDeviceBindingConflict): fail(http.StatusConflict) case err != nil: fail(http.StatusServiceUnavailable) @@ -80,27 +80,27 @@ func RuntimeConnected(ctx context.Context, s ConnectionStore, registry *runtimeg return false, err } if current.Status == "failed" || current.Status == "expired" { - return false, store.ErrNotFound + return false, sessions.ErrNotFound } bound, err := s.GetSessionDevice(ctx, tenant, current.SessionID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return false, nil } if err != nil { return false, err } if bound.EnvironmentID != environment { - return false, store.ErrDeviceBindingConflict + return false, sessions.ErrDeviceBindingConflict } credential, found, err := s.GetDeviceCredential(ctx, bound.ID) if err != nil { return false, err } if !found { - return false, store.ErrNotFound + return false, sessions.ErrNotFound } if credential.CredentialHash != digest { - return false, store.ErrDeviceBindingConflict + return false, sessions.ErrDeviceBindingConflict } if registry == nil { return false, nil @@ -127,10 +127,10 @@ func RuntimeConnected(ctx context.Context, s ConnectionStore, registry *runtimeg return false, err } if !found { - return false, store.ErrNotFound + return false, sessions.ErrNotFound } if credential.CredentialHash != digest { - return false, store.ErrDeviceBindingConflict + return false, sessions.ErrDeviceBindingConflict } return !peer.IsClosed() && peer.AuthenticatedWith(digest), nil } diff --git a/services/core/internal/runtimeenrollment/connection_test.go b/services/core/internal/runtimeenrollment/connection_test.go index a713cdb73..23fd1aff2 100644 --- a/services/core/internal/runtimeenrollment/connection_test.go +++ b/services/core/internal/runtimeenrollment/connection_test.go @@ -13,7 +13,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type connectionStub struct { @@ -24,15 +24,15 @@ type connectionStub struct { func (s *connectionStub) AuthenticateEnvironmentExecutor(_ context.Context, environment, digest string) (string, error) { s.calls++ if environment != "environment" || digest != runtimedevice.HashCredential("test-key") { - return "", store.ErrNotFound + return "", sessions.ErrNotFound } return "tenant", s.err } -func (*connectionStub) GetEnvironment(context.Context, string, string) (store.Environment, error) { - return store.Environment{ID: "environment", SessionID: "session", Status: "pending"}, nil +func (*connectionStub) GetEnvironment(context.Context, string, string) (sessions.Environment, error) { + return sessions.Environment{ID: "environment", SessionID: "session", Status: "pending"}, nil } -func (*connectionStub) GetSessionDevice(context.Context, string, string) (store.ExecutionDevice, error) { - return store.ExecutionDevice{}, store.ErrNotFound +func (*connectionStub) GetSessionDevice(context.Context, string, string) (sessions.ExecutionDevice, error) { + return sessions.ExecutionDevice{}, sessions.ErrNotFound } func (*connectionStub) GetDeviceCredential(context.Context, string) (runtimedevice.Credential, bool, error) { panic("unbound lookup") @@ -50,7 +50,7 @@ func TestConnectionReadContract(t *testing.T) { {"GET", "environment_id=environment&environment_id=other", "Bearer test-key", nil, 400, 0}, {"GET", "environment_id=environment&other=1", "Bearer test-key", nil, 400, 0}, {"GET", "environment_id=%zz", "Bearer test-key", nil, 400, 0}, - {"GET", "environment_id=environment", "Bearer test-key", store.ErrNotFound, 401, 1}, + {"GET", "environment_id=environment", "Bearer test-key", sessions.ErrNotFound, 401, 1}, {"GET", "environment_id=environment", "Bearer test-key", errors.New("private detail"), 503, 1}, } { s := &connectionStub{err: tc.err} @@ -89,16 +89,16 @@ func (s *liveConnectionStore) AuthenticateEnvironmentExecutor(context.Context, s return "", s.recheckError } if s.revokeAtRecheck { - return "", store.ErrNotFound + return "", sessions.ErrNotFound } } return "tenant", nil } -func (s *liveConnectionStore) GetEnvironment(context.Context, string, string) (store.Environment, error) { - return store.Environment{ID: "environment", SessionID: "session", Status: "connected"}, nil +func (s *liveConnectionStore) GetEnvironment(context.Context, string, string) (sessions.Environment, error) { + return sessions.Environment{ID: "environment", SessionID: "session", Status: "connected"}, nil } -func (s *liveConnectionStore) GetSessionDevice(context.Context, string, string) (store.ExecutionDevice, error) { - return store.ExecutionDevice{ID: "device", EnvironmentID: "environment"}, nil +func (s *liveConnectionStore) GetSessionDevice(context.Context, string, string) (sessions.ExecutionDevice, error) { + return sessions.ExecutionDevice{ID: "device", EnvironmentID: "environment"}, nil } func (s *liveConnectionStore) GetDeviceCredential(context.Context, string) (runtimedevice.Credential, bool, error) { s.credentialCalls++ @@ -141,10 +141,10 @@ func TestRuntimeConnectedCurrentAuthorityAfterPeer(t *testing.T) { digest = s.digest case "revoked after peer": s.revokeAtRecheck = true - wantErr = store.ErrNotFound + wantErr = sessions.ErrNotFound case "device revoked after peer", "retired after peer": s.deviceRevokedAtRecheck = true - wantErr = store.ErrNotFound + wantErr = sessions.ErrNotFound case "store error after peer": s.recheckError = errors.New("database unavailable") wantErr = s.recheckError diff --git a/services/core/internal/runtimeenrollment/enrollment.go b/services/core/internal/runtimeenrollment/enrollment.go index 5c33f463f..d2f0d2e5c 100644 --- a/services/core/internal/runtimeenrollment/enrollment.go +++ b/services/core/internal/runtimeenrollment/enrollment.go @@ -10,6 +10,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -45,9 +46,9 @@ func EnrollmentHandler(s EnrollmentStore) http.Handler { defer cancel() binding, err := s.EnrollRuntime(ctx, input.EnvironmentID, runtimedevice.HashCredential(authorization[1])) switch { - case errors.Is(err, store.ErrNotFound): + case errors.Is(err, sessions.ErrNotFound): fail(http.StatusUnauthorized) - case errors.Is(err, store.ErrDeviceBindingConflict): + case errors.Is(err, sessions.ErrDeviceBindingConflict): fail(http.StatusConflict) case err != nil: fail(http.StatusServiceUnavailable) diff --git a/services/core/internal/runtimeenrollment/enrollment_test.go b/services/core/internal/runtimeenrollment/enrollment_test.go index eb73fc0b7..42359248c 100644 --- a/services/core/internal/runtimeenrollment/enrollment_test.go +++ b/services/core/internal/runtimeenrollment/enrollment_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -34,8 +35,8 @@ func TestEnrollmentConnectionContract(t *testing.T) { {"missing authority", `{"environment_id":"environment"}`, "", nil, 401, 0}, {"caller binding", `{"environment_id":"environment","session_id":"other"}`, "Bearer private-test-token", nil, 400, 0}, {"extra input", `{"environment_id":"environment"}{}`, "Bearer private-test-token", nil, 400, 0}, - {"foreign", `{"environment_id":"environment"}`, "Bearer private-test-token", store.ErrNotFound, 401, 1}, - {"conflict", `{"environment_id":"environment"}`, "Bearer private-test-token", store.ErrDeviceBindingConflict, 409, 1}, + {"foreign", `{"environment_id":"environment"}`, "Bearer private-test-token", sessions.ErrNotFound, 401, 1}, + {"conflict", `{"environment_id":"environment"}`, "Bearer private-test-token", sessions.ErrDeviceBindingConflict, 409, 1}, {"internal failure", `{"environment_id":"environment"}`, "Bearer private-test-token", errors.New("private database detail"), 503, 1}, } { t.Run(test.name, func(t *testing.T) { diff --git a/services/core/internal/runtimehistory/storeresolver/resolver.go b/services/core/internal/runtimehistory/storeresolver/resolver.go index f2db2fa84..040de4bda 100644 --- a/services/core/internal/runtimehistory/storeresolver/resolver.go +++ b/services/core/internal/runtimehistory/storeresolver/resolver.go @@ -7,11 +7,11 @@ import ( "fmt" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type environmentStore interface { - GetSessionEnvironment(context.Context, string, string) (store.Environment, error) + GetSessionEnvironment(context.Context, string, string) (sessions.Environment, error) } type Resolver struct{ store environmentStore } diff --git a/services/core/internal/runtimehistory/storeresolver/resolver_test.go b/services/core/internal/runtimehistory/storeresolver/resolver_test.go index 033c19bb4..83cf2939f 100644 --- a/services/core/internal/runtimehistory/storeresolver/resolver_test.go +++ b/services/core/internal/runtimehistory/storeresolver/resolver_test.go @@ -6,22 +6,22 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type resolverStore struct { - environment store.Environment + environment sessions.Environment err error calls int } -func (s *resolverStore) GetSessionEnvironment(context.Context, string, string) (store.Environment, error) { +func (s *resolverStore) GetSessionEnvironment(context.Context, string, string) (sessions.Environment, error) { s.calls++ return s.environment, s.err } func TestResolverAuthorizesManagedSessionWithoutSelectingCurrentAllocation(t *testing.T) { - backend := &resolverStore{environment: store.Environment{ + backend := &resolverStore{environment: sessions.Environment{ ID: environmentID, TenantID: tenantID, SessionID: sessionID, Configuration: []byte(`{"type":"openai_hosted"}`), }} @@ -39,19 +39,19 @@ func TestResolverAuthorizesManagedSessionWithoutSelectingCurrentAllocation(t *te } func TestResolverPreservesTenantScopedNotFound(t *testing.T) { - backend := &resolverStore{err: store.ErrNotFound} + backend := &resolverStore{err: sessions.ErrNotFound} resolver, err := NewResolver(backend) if err != nil { t.Fatal(err) } _, err = resolver.ResolveRuntimeHistoryScope(t.Context(), tenantID, sessionID) - if !errors.Is(err, store.ErrNotFound) { + if !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("tenant-scoped not found was not preserved: %v", err) } } func TestResolverRejectsUnsupportedOrMismatchedEnvironment(t *testing.T) { - for _, environment := range []store.Environment{ + for _, environment := range []sessions.Environment{ {ID: environmentID, TenantID: tenantID, SessionID: sessionID, Configuration: []byte(`{"type":"self_hosted"}`)}, {ID: environmentID, TenantID: "55555555-5555-4555-8555-555555555555", SessionID: sessionID, Configuration: []byte(`{"type":"openai_hosted"}`)}, {ID: environmentID, TenantID: tenantID, SessionID: "66666666-6666-4666-8666-666666666666", Configuration: []byte(`{"type":"openai_hosted"}`)}, diff --git a/services/core/internal/runtimeobs/storeresolver/resolver.go b/services/core/internal/runtimeobs/storeresolver/resolver.go index c89ebdc53..817378085 100644 --- a/services/core/internal/runtimeobs/storeresolver/resolver.go +++ b/services/core/internal/runtimeobs/storeresolver/resolver.go @@ -9,11 +9,12 @@ import ( "io" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) type sessionStore interface { - GetSession(context.Context, string, string) (store.Session, error) + GetSession(context.Context, string, string) (sessions.Session, error) MeasuredSessionUsage(context.Context, string, string) (json.RawMessage, error) GetRuntimeAllocation(context.Context, string, string) (store.RuntimeAllocation, error) ListRuntimeObservationSessions(context.Context, string, int) (store.RuntimeObservationSessionPage, error) @@ -77,7 +78,7 @@ func (r *Resolver) Resolve(ctx context.Context, tenantID, sessionID string) (run } target.EnvironmentID = session.Environment.ID allocation, err := r.store.GetRuntimeAllocation(ctx, tenantID, target.EnvironmentID) - if errors.Is(err, store.ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return target, runtimeobs.ErrUnavailable } if err != nil { diff --git a/services/core/internal/runtimeobs/storeresolver/resolver_test.go b/services/core/internal/runtimeobs/storeresolver/resolver_test.go index 4bc31abce..e9c028d9e 100644 --- a/services/core/internal/runtimeobs/storeresolver/resolver_test.go +++ b/services/core/internal/runtimeobs/storeresolver/resolver_test.go @@ -7,18 +7,19 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) type resolverStore struct { - session store.Session + session sessions.Session measured json.RawMessage allocation store.RuntimeAllocation allocationErr error page store.RuntimeObservationSessionPage } -func (s resolverStore) GetSession(context.Context, string, string) (store.Session, error) { +func (s resolverStore) GetSession(context.Context, string, string) (sessions.Session, error) { return s.session, nil } @@ -53,7 +54,7 @@ func TestResolverListsOnlyProviderNeutralSessionIdentity(t *testing.T) { func TestResolverBindsManagedSessionEnvironmentAndAllocation(t *testing.T) { r, err := NewResolver(resolverStore{ - session: store.Session{ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"openai_hosted"}}`), Environment: &store.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}}, + session: sessions.Session{ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"openai_hosted"}}`), Environment: &sessions.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}}, measured: []byte(`{"input_tokens":120,"input_tokens_details":{"cached_tokens":20},"output_tokens":30,"output_tokens_details":{"reasoning_tokens":10},"total_tokens":150}`), allocation: store.RuntimeAllocation{ ID: "allocation", TenantID: "tenant", SessionID: "session", EnvironmentID: "environment", @@ -89,7 +90,7 @@ func TestResolverRejectsInvalidCanonicalSessionUsage(t *testing.T) { `{"input_tokens":0,"input_tokens_details":{"cached_tokens":-1},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":0}`, `{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":0,"unknown":0}`, } { - resolver, err := NewResolver(resolverStore{session: store.Session{ + resolver, err := NewResolver(resolverStore{session: sessions.Session{ ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"none"}}`), }, measured: []byte(usage)}) if err != nil { @@ -102,7 +103,7 @@ func TestResolverRejectsInvalidCanonicalSessionUsage(t *testing.T) { } func TestResolverKeepsNullCanonicalSessionUsageAbsent(t *testing.T) { - resolver, err := NewResolver(resolverStore{session: store.Session{ + resolver, err := NewResolver(resolverStore{session: sessions.Session{ ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"none"}}`), }, measured: []byte(" \n null \t")}) if err != nil { @@ -125,7 +126,7 @@ func TestResolverUsesMeasuredRatherThanPublicSessionUsage(t *testing.T) { {"null", measured, &runtimeobs.TokenUsage{InputTokens: 7, OutputTokens: 3}}, {measured, "null", nil}, } { - resolver, err := NewResolver(resolverStore{session: store.Session{ + resolver, err := NewResolver(resolverStore{session: sessions.Session{ ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"none"}}`), Usage: []byte(test.public), }, measured: []byte(test.measured)}) if err != nil { @@ -141,12 +142,12 @@ func TestResolverUsesMeasuredRatherThanPublicSessionUsage(t *testing.T) { func TestResolverKeepsUnsupportedModesDistinct(t *testing.T) { for _, tc := range []struct { mode string - environment *store.Environment + environment *sessions.Environment }{ {mode: "none"}, - {mode: "self_hosted", environment: &store.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}}, + {mode: "self_hosted", environment: &sessions.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}}, } { - r, err := NewResolver(resolverStore{session: store.Session{ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"` + tc.mode + `"}}`), Environment: tc.environment}}) + r, err := NewResolver(resolverStore{session: sessions.Session{ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"` + tc.mode + `"}}`), Environment: tc.environment}}) if err != nil { t.Fatal(err) } @@ -159,8 +160,8 @@ func TestResolverKeepsUnsupportedModesDistinct(t *testing.T) { func TestResolverReportsManagedAllocationAsUnavailable(t *testing.T) { r, err := NewResolver(resolverStore{ - session: store.Session{ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"openai_hosted"}}`), Environment: &store.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}}, - allocationErr: store.ErrNotFound, + session: sessions.Session{ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"openai_hosted"}}`), Environment: &sessions.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}}, + allocationErr: sessions.ErrNotFound, }) if err != nil { t.Fatal(err) @@ -173,11 +174,11 @@ func TestResolverReportsManagedAllocationAsUnavailable(t *testing.T) { func TestResolverRejectsMismatchedEnvironmentOwnership(t *testing.T) { for _, mode := range []string{"self_hosted", "openai_hosted"} { - for _, environment := range []store.Environment{ + for _, environment := range []sessions.Environment{ {ID: "environment", TenantID: "other", SessionID: "session"}, {ID: "environment", TenantID: "tenant", SessionID: "other"}, } { - resolver, err := NewResolver(resolverStore{session: store.Session{ + resolver, err := NewResolver(resolverStore{session: sessions.Session{ ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"` + mode + `"}}`), Environment: &environment, }}) @@ -204,9 +205,9 @@ func TestResolverRejectsMismatchedAllocationOwnership(t *testing.T) { allocation := base mutate(&allocation) resolver, err := NewResolver(resolverStore{ - session: store.Session{ + session: sessions.Session{ ID: "session", TenantID: "tenant", Configuration: []byte(`{"environment":{"type":"openai_hosted"}}`), - Environment: &store.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}, + Environment: &sessions.Environment{ID: "environment", TenantID: "tenant", SessionID: "session"}, }, allocation: allocation, }) diff --git a/services/core/internal/sandbox/providers/configuration_flow_test.go b/services/core/internal/sandbox/providers/configuration_flow_test.go index 05a5eb8b4..6802289f4 100644 --- a/services/core/internal/sandbox/providers/configuration_flow_test.go +++ b/services/core/internal/sandbox/providers/configuration_flow_test.go @@ -128,11 +128,24 @@ func TestAdditionalConfigurationProviderUsesCommonAPIAndStore(t *testing.T) { EnvironmentTemplates: struct{ api.EnvironmentTemplates }{}, EnvironmentTemplatesReader: struct{ api.EnvironmentTemplatesReader }{}, Skills: struct{ api.Skills }{}, SkillsReader: struct{ api.SkillsReader }{}, Agents: struct{ api.Agents }{}, AgentsReader: struct{ api.AgentsReader }{}, - Sessions: s, SessionEvents: s, SessionHistory: s, Subagents: s, Artifacts: s, - SessionAdmin: s, Environments: s, Admin: s, AdminAudit: struct{ api.AdminAudit }{}, WriteAudit: struct{ api.WriteAudit }{}, + Sessions: s, + SessionCreation: s, + SessionEvents: s, + Turns: s, + Items: s, + Subagents: s, + Artifacts: s, + SessionAdmin: s, + Environments: s, Admin: s, AdminAudit: struct{ api.AdminAudit }{}, WriteAudit: struct{ api.WriteAudit }{}, ExecutorConnections: struct{ api.ExecutorConnections }{}, Metrics: struct{ api.Metrics }{}, RuntimeObservations: struct{ api.RuntimeObservations }{}, RuntimeHistory: struct{ api.RuntimeHistory }{}, - Execution: &api.Execution{ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws", Admission: s, SessionArchive: s, Workspaces: struct{ api.EnvironmentWorkspaces }{}}, + Execution: &api.Execution{ + ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws", + SessionAdmission: s, + InputAdmission: s, + SessionArchive: s, + Workspaces: struct{ api.EnvironmentWorkspaces }{}, + }, Sandboxes: &api.Sandboxes{Deployment: service, NodeAllocations: s, DeploymentChanges: leaseSetup{t: t, changes: changes, installation: installation}, DeploymentReset: leaseSetup{t: t, changes: changes, installation: installation}, ConfigurationDiscovery: struct{ api.ConfigurationDiscovery }{}}, }) diff --git a/services/core/internal/sessions/doc.go b/services/core/internal/sessions/doc.go index 4af577523..d40126ef8 100644 --- a/services/core/internal/sessions/doc.go +++ b/services/core/internal/sessions/doc.go @@ -1,5 +1,7 @@ -// Package sessions owns the Session change vocabulary, including Turns and -// their statuses, and decides what Session writes publish: the public changes -// that report Turn and Session transitions, what a Turn that ends settles, -// measured Turn usage and the Session activity each change reports. +// Package sessions owns the Session vocabulary: Sessions, Turns and their +// statuses, inputs, Environments and their provisioning failures, function +// calls, Item and Artifact reads, executor credentials, and the errors Session +// operations return. It also decides what Session writes publish: the public +// changes that report Turn and Session transitions, what a Turn that ends +// settles, measured Turn usage and the Session activity each change reports. package sessions diff --git a/services/core/internal/sessions/environment.go b/services/core/internal/sessions/environment.go index b7a8333ff..59b1bc497 100644 --- a/services/core/internal/sessions/environment.go +++ b/services/core/internal/sessions/environment.go @@ -1,6 +1,29 @@ package sessions -import "time" +import ( + "encoding/hex" + "encoding/json" + "time" + + "github.com/google/uuid" +) + +// Environment retains execution ownership; its configuration is an internal snapshot, not a public response. +type Environment struct { + Initialization string + ID string + SessionID string + TenantID string + Status string + CreatedAt time.Time + Configuration json.RawMessage +} + +// EnvironmentInitialization owns preparation independently of compute ownership. +// A running record without its process-local owner is unknown, never replayable. +type EnvironmentInitialization struct { + EnvironmentID, SessionID, TenantID, DeviceID, State, Engine string +} // EnvironmentInputActivity is the reservation-owned override before a newer Turn exists. type EnvironmentInputActivity struct { @@ -10,18 +33,52 @@ type EnvironmentInputActivity struct { LastActiveAt time.Time `json:"last_active_at"` } -// EnvironmentFailure is an Environment's recorded provisioning failure. It -// makes the Session failed with this reason and last activity time. -type EnvironmentFailure struct { - Reason string `json:"reason"` - FailedAt time.Time `json:"failed_at"` - Detail *ProvisioningFailureDetail `json:"-"` +const ( + EnvironmentInputPending = "pending" + EnvironmentInputAdmitted = "admitted" + EnvironmentInputExpired = "expired" + EnvironmentInputCancelled = "cancelled" + EnvironmentInputFailed = "failed" +) + +// EnvironmentInputReservation is private admission state, not a public Session projection. +type EnvironmentInputReservation struct { + ID string + SessionID string + State string + IsInitial bool + Inputs []Input + CreatedAt time.Time + Deadline time.Time + SettledAt *time.Time + Receipts []InputReceipt +} + +type EnvironmentInputWork struct{ TenantID, SessionID, ReservationID string } + +// FileWriteIdentity binds a private mutation to one dedicated local Runtime. RequestSHA256 covers the canonical destination, byte count and data digest. +// The caller must qualify that binding and validate native receipts independently; +// persistence alone is neither placement authority nor permission to send bytes. +type FileWriteIdentity struct { + ID, DeviceID, RequestSHA256 string +} + +func (k FileWriteIdentity) Valid() bool { + for _, value := range []string{k.ID, k.DeviceID} { + id, err := uuid.Parse(value) + if err != nil || id == uuid.Nil || id.String() != value { + return false + } + } + digest, err := hex.DecodeString(k.RequestSHA256) + return err == nil && len(digest) == 32 && hex.EncodeToString(digest) == k.RequestSHA256 } -// ProvisioningFailureDetail is private, fixed-category evidence from a confirmed -// initialization receipt. It never contains command text, paths or Runtime output. -type ProvisioningFailureDetail struct { - Step *string `json:"step"` - Index *int `json:"index"` - ExitCode *int `json:"exit_code"` +type EnvironmentFileWrite struct { + Identity FileWriteIdentity + EnvironmentID, SessionID string + State string + CreatedAt time.Time + SettledAt *time.Time + Replayed bool } diff --git a/services/core/internal/sessions/errors.go b/services/core/internal/sessions/errors.go new file mode 100644 index 000000000..116914bb3 --- /dev/null +++ b/services/core/internal/sessions/errors.go @@ -0,0 +1,75 @@ +package sessions + +import ( + "errors" + "fmt" +) + +var ( + ErrInvalidInput = errors.New("invalid session input") + ErrEnvironmentUnavailable = errors.New("environment is no longer available") + ErrNotFound = errors.New("session not found") + ErrIdempotencyConflict = errors.New("idempotency key was already used with different input") +) + +var ErrTurnConflict = errors.New("turn state changed or cancellation was requested") + +// ErrInputPending rejects a new input batch while earlier Session input +// still waits for admission. It remains a Turn conflict for internal callers. +var ErrInputPending = fmt.Errorf("%w: session input is still pending", ErrTurnConflict) + +// ErrHostedEnvironmentFailed rejects new input on a Session whose hosted +// Environment failed to provision. It remains ErrEnvironmentUnavailable for +// internal callers; an expired Environment keeps that plain error. +var ErrHostedEnvironmentFailed = fmt.Errorf("%w: the hosted environment failed to provision", ErrEnvironmentUnavailable) + +// ErrNotIdle rejects deletion of a Session that still has work or input +// pending. Callers cancel first and delete after the Session settles. +var ErrNotIdle = errors.New("session must be durably idle or failed without required actions before deletion") + +var ErrEventLimit = errors.New("execution event storage limit exceeded") + +var ErrUnappliedInputs = errors.New("turn has messages without an executor receipt") + +var ErrStreamGap = errors.New("live event buffer exceeded; recover through Session and Items reads") + +var ErrDeviceBindingConflict = errors.New("session is already bound to a different device") + +var ErrInstallationAuthorization = errors.New("installation authorization is invalid or expired; obtain a new command from the Session") + +var ErrExecutorCredentialExists = errors.New("executor key ID already exists; rotate explicitly") + +// Result targets are resolved only inside a tenant-owned Session, after its +// lookup, so a missing or foreign Session still returns ErrNotFound (EVT-11). +var ( + // ErrUnknownFunctionCall rejects a result whose call_id names no function + // call in the Session. + ErrUnknownFunctionCall = errors.New("unknown pending tool call") + // ErrFunctionCallTurnMismatch rejects a result whose call exists in the + // Session but not in the named Turn, including a malformed or unknown Turn. + ErrFunctionCallTurnMismatch = errors.New("tool call belongs to a different turn") +) + +// ErrFunctionResultConflict rejects a result that differs from the one already +// saved for its call, including after the Turn ended (EVT-12). +var ErrFunctionResultConflict = errors.New("tool call already has a different result") + +// CursorError reports a list `after` cursor that does not name a +// resource of that list once its parents have resolved. Message is the list +// family's observed official message; the API layer selects the family's code +// and param. Missing, malformed, other-type, other-parent and foreign cursors +// all produce the same error, so a cursor never reveals another tenant's +// resources. +type CursorError struct{ Message string } + +func (e *CursorError) Error() string { return e.Message } + +// Official messages for lists that reject an unresolved cursor with 400. +var ( + // Session Items, Subagent Items and Subagent Turn Items. + ErrItemCursor = &CursorError{Message: "Invalid session item ID in `after`"} + // Subagents and Subagent Turns. + ErrResourceCursor = &CursorError{Message: "Invalid resource ID in `after`"} + // Session Artifacts. + ErrArtifactCursor = &CursorError{Message: "after is not a valid artifact ID"} +) diff --git a/services/core/internal/sessions/executor_credentials.go b/services/core/internal/sessions/executor_credentials.go new file mode 100644 index 000000000..311124c0c --- /dev/null +++ b/services/core/internal/sessions/executor_credentials.go @@ -0,0 +1,48 @@ +package sessions + +import ( + "time" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" +) + +type IssuedExecutorCredential struct { + KeyID string `json:"key_id"` + EnvironmentID string `json:"environment_id,omitempty"` + Token string `json:"executor_token"` +} + +// ExecutorCredential is the metadata of one Environment executor credential. +// Its secret is returned only when issued or rotated. +type ExecutorCredential struct { + KeyID string `json:"key_id" format:"uuid"` + CreatedAt time.Time `json:"created_at"` + RevokedAt *time.Time `json:"revoked_at" extensions:"x-nullable"` +} + +// ExecutorConnectionState is an internal durable observation, never a wire payload. +// In particular the current credential digest must not be serialized. +type ExecutorConnectionState struct { + DeviceID string `json:"-"` + BoundKeyID *string `json:"-"` + EnrolledAt *time.Time `json:"-"` + LastSeenAt *time.Time `json:"-"` + CredentialHash string `json:"-"` + EnvironmentStatus string `json:"-"` +} + +type ExecutorCredentialState struct { + EnvironmentID string `json:"-"` + Credentials []ExecutorCredential + Connection ExecutorConnectionState +} + +// InstallationAuthorization permits claiming one Environment's connect-only key. +// The Environment UUID is reserved as that key's ID. Reissuing an authorization +// never rotates or revives the key, and a retry must prove the same local secret. +type InstallationAuthorization struct { + Principal identity.Principal `json:"principal"` + Environment string `json:"environment_id"` + Version string `json:"version"` + ExpiresAt int64 `json:"expires_at"` +} diff --git a/services/core/internal/sessions/inputs.go b/services/core/internal/sessions/inputs.go new file mode 100644 index 000000000..e041795b5 --- /dev/null +++ b/services/core/internal/sessions/inputs.go @@ -0,0 +1,53 @@ +package sessions + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +// Input is a validated execution command, not an upstream wire type. +// The API validates event fields before constructing this storage input. +type Input struct { + Kind string `json:"kind"` + Payload json.RawMessage `json:"payload"` +} + +type InputReceipt struct { + Sequence int64 + TurnID string // Empty for a cancellation accepted while the Session was idle. + Replayed bool +} + +type TurnInput struct { + Sequence int64 + Kind string + Payload json.RawMessage + CreatedAt time.Time +} + +// ValidateInputKey enforces the shared request identity limit, including no-op requests. +func ValidateInputKey(key string) error { + if strings.TrimSpace(key) == "" || len(key) > 128 { + return fmt.Errorf("%w: idempotency key is required and limited to 128 bytes", ErrInvalidInput) + } + return nil +} + +// FunctionCall retains public identity and its opaque execution-adapter reference. +type FunctionCall struct { + CallID, ExecutorCallID, Name string + Arguments json.RawMessage + Result json.RawMessage + Applied bool +} + +// FunctionResultInput identifies a persisted call; Result is validated by the API. +// It is an internal command, not an upstream input event. TurnID is the caller's +// value and is resolved within the Session at admission. +type FunctionResultInput struct { + TurnID string `json:"turn_id"` + CallID string `json:"call_id"` + Result json.RawMessage `json:"result"` +} diff --git a/services/core/internal/sessions/provisioning.go b/services/core/internal/sessions/provisioning.go new file mode 100644 index 000000000..da5882344 --- /dev/null +++ b/services/core/internal/sessions/provisioning.go @@ -0,0 +1,107 @@ +package sessions + +import ( + "fmt" + "time" +) + +// EnvironmentFailure is an Environment's recorded provisioning failure. It +// makes the Session failed with this reason and last activity time. +type EnvironmentFailure struct { + Reason string `json:"reason"` + FailedAt time.Time `json:"failed_at"` + Detail *ProvisioningFailureDetail `json:"-"` +} + +// ProvisioningFailureDetail is private, fixed-category evidence from a confirmed +// initialization receipt. It never contains command text, paths or Runtime output. +type ProvisioningFailureDetail struct { + Step *string `json:"step"` + Index *int `json:"index"` + ExitCode *int `json:"exit_code"` +} + +// ProvisioningFailureReason is the safe reason for an Environment that +// failed without a confirmed failed step: timeouts, unknown effects, missing or +// old receipts, bootstrap rejection and Core restart during initialization. +const ProvisioningFailureReason = "Failed to provision environment: initialization did not complete" + +// Provisioning step kinds for ProvisioningFailure.Step. +const ( + ProvisioningSetupCommand = "setup" + ProvisioningPythonPackages = "python" + ProvisioningNPMPackages = "npm" + ProvisioningInitialFile = "file" + ProvisioningSkill = "skill" + ProvisioningHarness = "harness" +) + +// ProvisioningFailure identifies a confirmed failed initialization step. +// It cannot carry Runtime output: Step selects a fixed label, Index is the setup +// command position and ExitCode is the Runtime-reported status (0 when absent). +type ProvisioningFailure struct { + Step string + Index int + ExitCode int +} + +// Reason renders the public Session error. The setup_commands and Python package +// labels match observed official errors (which append raw pip output for Python; +// Core never does). The npm, file and Skill labels are unverified. +// A script step without a reported exit status keeps the generic reason. +func (f ProvisioningFailure) Reason() string { + label := map[string]string{ + ProvisioningPythonPackages: "Python package installation", + ProvisioningNPMPackages: "npm package installation", + }[f.Step] + if f.Step == ProvisioningSetupCommand && f.Index >= 0 { + label = fmt.Sprintf("setup_commands[%d]", f.Index) + } + switch { + case label != "" && f.ExitCode > 0 && f.ExitCode < 256: + return fmt.Sprintf("Failed to provision environment: script %q failed with exit code %d", label, f.ExitCode) + case f.Step == ProvisioningInitialFile: + return "Failed to provision environment: initial file installation failed" + case f.Step == ProvisioningHarness: + return "Failed to prepare environment: the selected Harness is unavailable. Install the supported Harness version on the Runtime and create a new Session." + case f.Step == ProvisioningSkill: + return "Failed to provision environment: Skill installation failed" + } + return ProvisioningFailureReason +} + +// Detail is the private evidence recorded with the failure. +func (f ProvisioningFailure) Detail() *ProvisioningFailureDetail { + return SanitizedProvisioningDetail(ProvisioningFailureDetail{Step: &f.Step, Index: &f.Index, ExitCode: &f.ExitCode}) +} + +// SanitizedProvisioningDetail keeps only the fields the step's category allows +// and returns nil for a missing or unknown step. +func SanitizedProvisioningDetail(f ProvisioningFailureDetail) *ProvisioningFailureDetail { + if f.Step == nil { + return nil + } + result := &ProvisioningFailureDetail{} + switch *f.Step { + case ProvisioningSetupCommand: + // JSON clients can represent these integer positions exactly. + if f.Index != nil && *f.Index >= 0 && int64(*f.Index) <= 9007199254740991 { + value := *f.Index + result.Index = &value + } + case ProvisioningPythonPackages, ProvisioningNPMPackages: + case ProvisioningInitialFile, ProvisioningSkill, ProvisioningHarness: + value := *f.Step + result.Step = &value + return result + default: + return nil + } + value := *f.Step + result.Step = &value + if f.ExitCode != nil && *f.ExitCode > 0 && *f.ExitCode < 256 { + value := *f.ExitCode + result.ExitCode = &value + } + return result +} diff --git a/services/core/internal/sessions/provisioning_test.go b/services/core/internal/sessions/provisioning_test.go new file mode 100644 index 000000000..90ecbc499 --- /dev/null +++ b/services/core/internal/sessions/provisioning_test.go @@ -0,0 +1,27 @@ +package sessions + +import "testing" + +// Reasons contain only a fixed label and an exit status. Setup and Python labels +// match official samples; npm, system, file and Skill labels are unverified. +func TestProvisioningFailureReasons(t *testing.T) { + for failure, want := range map[ProvisioningFailure]string{ + {Step: ProvisioningSetupCommand, Index: 0, ExitCode: 3}: `Failed to provision environment: script "setup_commands[0]" failed with exit code 3`, + {Step: ProvisioningSetupCommand, Index: 12, ExitCode: 1}: `Failed to provision environment: script "setup_commands[12]" failed with exit code 1`, + {Step: ProvisioningPythonPackages, ExitCode: 1}: `Failed to provision environment: script "Python package installation" failed with exit code 1`, + {Step: ProvisioningNPMPackages, ExitCode: 1}: `Failed to provision environment: script "npm package installation" failed with exit code 1`, + {Step: ProvisioningInitialFile}: "Failed to provision environment: initial file installation failed", + {Step: ProvisioningSkill}: "Failed to provision environment: Skill installation failed", + // Missing or impossible statuses, unknown steps and old receipts stay generic. + {Step: ProvisioningSetupCommand, Index: 0}: ProvisioningFailureReason, + {Step: ProvisioningSetupCommand, Index: -1, ExitCode: 3}: ProvisioningFailureReason, + {Step: ProvisioningPythonPackages, ExitCode: 256}: ProvisioningFailureReason, + {Step: ProvisioningNPMPackages, ExitCode: -9}: ProvisioningFailureReason, + {Step: "configure", ExitCode: 1}: ProvisioningFailureReason, + {}: ProvisioningFailureReason, + } { + if got := failure.Reason(); got != want || len(got) > 256 { + t.Errorf("%+v: %q", failure, got) + } + } +} diff --git a/services/core/internal/sessions/reads.go b/services/core/internal/sessions/reads.go new file mode 100644 index 000000000..522012181 --- /dev/null +++ b/services/core/internal/sessions/reads.go @@ -0,0 +1,50 @@ +package sessions + +import ( + "time" + + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" +) + +type ItemPage struct { + Items []v1.Item + HasMore bool +} + +type Artifact struct { + ID string + SessionID string + TurnID string + EnvironmentID string + Path string + SizeBytes int64 + CreatedAt time.Time +} + +type ArtifactPage struct { + Artifacts []Artifact + NextCursor string +} + +// ItemDiagnosticTiming records Core database receipt and settlement, never native +// execution duration. Historical terminal Items can have unknown settlement. +type ItemDiagnosticTiming struct { + ItemID string + StartedAt time.Time + CompletedAt *time.Time +} + +type TurnDiagnosticsSnapshot struct { + Session Session + Turn Turn + Items []ItemDiagnosticTiming + ItemsTruncated bool +} + +// ManagedArchive reports resource disposal, not archive request provenance +// or Turn settlement. Existing expiry and failed provisioning use the same states. +type ManagedArchive struct { + SessionID string `json:"session_id"` + EnvironmentID string `json:"environment_id"` + State string `json:"state"` +} diff --git a/services/core/internal/sessions/session.go b/services/core/internal/sessions/session.go new file mode 100644 index 000000000..61ded8a8d --- /dev/null +++ b/services/core/internal/sessions/session.go @@ -0,0 +1,75 @@ +package sessions + +import ( + "encoding/json" + "regexp" + "time" + + "github.com/google/uuid" + + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" +) + +var enginePattern = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`) + +// Session is a durable execution context, separate from product conversations +// and from live daemon connections. Engine session IDs will be bound at execution. +type Session struct { + ID string + TenantID string + Creator *identity.Subject + Engine string + Metadata map[string]string + CreatedAt time.Time + Configuration json.RawMessage + LastTurn *Turn + Usage json.RawMessage + RequiredActions []v1.FunctionCallAction + Environment *Environment + EnvironmentInputActivity *EnvironmentInputActivity + // EnvironmentFailure is the recorded provisioning failure of a failed hosted + // Environment. It makes the Session failed and is terminal. + EnvironmentFailure *EnvironmentFailure + // PendingInput reports that the latest input reservation, read once no Turn + // is active or newer, can still start a Turn. It only supports settlement + // checks and is never rendered. + PendingInput bool +} + +type CreateSession struct { + // DeploymentProviderRevision is private creation metadata, never retry identity. + DeploymentProviderRevision uuid.UUID `json:"-"` + ExecutionConfiguration *v1.SessionExecutionConfiguration + ModelProvider *v1.ModelProviderInput + ModelProviderSource string // session, agent or deployment; empty allows only openai_hosted + Initialization environmentconfig.Setup + InitialFiles []environmentconfig.InitialFile + Creator identity.Subject + CreationRequest json.RawMessage + Engine string + Metadata map[string]string + IdempotencyKey string + Configuration json.RawMessage + InitialInputs []Input +} + +type Page struct { + Sessions []Session + NextCursor string +} + +// Creation starts observation at the Session upsert. For a new creation, +// CreateSessionStream returns the committed Session projection that +// CreateSession returns, read after the creation commits; Cursor still precedes +// the initial inputs, so their events remain observable exactly once. Retries +// and FindSessionCreation return only the resource row and its cursor. Only a new creation emits a created snapshot and +// streams from Cursor; a stream retry of an existing creation sends no events. +type Creation struct { + Session Session + Created bool + Cursor int64 +} + +func ValidEngine(engine string) bool { return enginePattern.MatchString(engine) } diff --git a/services/core/internal/sessions/turn.go b/services/core/internal/sessions/turn.go index 3851093a5..0455e4d7b 100644 --- a/services/core/internal/sessions/turn.go +++ b/services/core/internal/sessions/turn.go @@ -36,3 +36,42 @@ type Turn struct { func TerminalStatus(status string) bool { return status == TurnCompleted || status == TurnFailed || status == TurnCancelled } + +type TurnTransition struct { + ExpectedStatus string + Status string + Outcome json.RawMessage +} + +type TurnPage struct { + Turns []Turn + NextCursor string +} + +type TurnEvent struct { + Ordinal int32 + Kind string + Payload json.RawMessage + CreatedAt time.Time +} + +type ExecutionEvent struct { + Kind string `json:"kind"` + Payload json.RawMessage `json:"payload"` +} + +type ExecutionWork struct{ TenantID, SessionID, TurnID, Status string } + +// ExecutionDevice contains safe identity only, never a device credential. +type ExecutionDevice struct { + ID string + Name string + EnvironmentID string +} + +// ExecutionBinding identifies the Runtime and native history selected for one API Session. +type ExecutionBinding struct { + Device ExecutionDevice + NativeSessionID string + HasStartedTurn bool +} diff --git a/services/core/internal/store/admin_delete_audit_test.go b/services/core/internal/store/admin_delete_audit_test.go index 6eeb2f6c1..6aa52cd03 100644 --- a/services/core/internal/store/admin_delete_audit_test.go +++ b/services/core/internal/store/admin_delete_audit_test.go @@ -169,7 +169,7 @@ func prepareAdminHistoryDelete(t *testing.T, s *Store, name string) (string, res if artifact.Path == "/workspace/outputs/other.txt" { want = []byte("second body") } - if err := s.ReadSessionArtifact(t.Context(), tenant, session, artifact.ID, func(_ SessionArtifact, r io.Reader) error { + if err := s.ReadSessionArtifact(t.Context(), tenant, session, artifact.ID, func(_ sessions.Artifact, r io.Reader) error { got, err := io.ReadAll(r) if !bytes.Equal(got, want) { t.Error("artifact large-object bytes were not restored") @@ -200,7 +200,7 @@ func assertAdminDeletedResource(t *testing.T, s *Store, tenant string, mutation default: t.Fatal("unsupported delete fixture") } - if !errors.Is(err, ErrNotFound) { + if !errors.Is(err, sessions.ErrNotFound) { t.Fatal("successful deletion left resource visible", err) } } diff --git a/services/core/internal/store/admin_session_archive.go b/services/core/internal/store/admin_session_archive.go index 7ff87f2a9..6de7cae50 100644 --- a/services/core/internal/store/admin_session_archive.go +++ b/services/core/internal/store/admin_session_archive.go @@ -12,42 +12,35 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -// ManagedSessionArchive reports resource disposal, not archive request provenance -// or Turn settlement. Existing expiry and failed provisioning use the same states. -type ManagedSessionArchive struct { - SessionID string `json:"session_id"` - EnvironmentID string `json:"environment_id"` - State string `json:"state"` -} - // ArchiveManagedSession ends a managed Environment's lifetime while keeping its // public Session, history and persisted files. The lifecycle owner performs the // external cleanup; only its existing confirmation can release an allocation. -func (s *Store) ArchiveManagedSession(ctx context.Context, tenantID, sessionID string, expectedGeneration uint64) (ManagedSessionArchive, error) { +func (s *Store) ArchiveManagedSession(ctx context.Context, tenantID, sessionID string, expectedGeneration uint64) (sessions.ManagedArchive, error) { return s.archiveManagedSession(ctx, tenantID, sessionID, expectedGeneration, nil) } // A reset instance is identified by its persisted request time as well as its // generation, preventing a cancelled clear's candidates from affecting its successor. -func (s *Store) ArchiveSandboxResetSession(ctx context.Context, tenantID, sessionID string, generation uint64, requestedAt time.Time) (ManagedSessionArchive, error) { +func (s *Store) ArchiveSandboxResetSession(ctx context.Context, tenantID, sessionID string, generation uint64, requestedAt time.Time) (sessions.ManagedArchive, error) { return s.archiveManagedSession(ctx, tenantID, sessionID, generation, &requestedAt) } var ErrSandboxResetSessionBusy = errors.New("the hosted Session is busy") -func (s *Store) archiveManagedSession(ctx context.Context, tenantID, sessionID string, expectedGeneration uint64, resetRequestedAt *time.Time) (ManagedSessionArchive, error) { +func (s *Store) archiveManagedSession(ctx context.Context, tenantID, sessionID string, expectedGeneration uint64, resetRequestedAt *time.Time) (sessions.ManagedArchive, error) { if err := s.checkExecutionAuthority(); err != nil { - return ManagedSessionArchive{}, err + return sessions.ManagedArchive{}, err } tenant, err := parseID(tenantID) if err != nil { - return ManagedSessionArchive{}, err + return sessions.ManagedArchive{}, err } - var result ManagedSessionArchive + var result sessions.ManagedArchive err = s.withPublicSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { // Session precedes deployment, matching Turn, allocation and input admission. current, err := q.LockRuntimeDeployment(ctx) @@ -65,14 +58,14 @@ func (s *Store) archiveManagedSession(ctx context.Context, tenantID, sessionID s } environment, err := q.GetSessionEnvironment(ctx, sqlc.GetSessionEnvironmentParams{TenantID: tenant, ID: session}) if errors.Is(err, pgx.ErrNoRows) { - return ErrInvalidInput + return sessions.ErrInvalidInput } if err != nil { return err } kind, err := storedEnvironmentType(environment) if err != nil || kind != "openai_hosted" { - return ErrInvalidInput + return sessions.ErrInvalidInput } if resetRequestedAt != nil { if !current.ResetClear.Valid || !current.ResetRequestedAt.Time.Equal(*resetRequestedAt) { @@ -142,24 +135,24 @@ func (s *Store) archiveManagedSession(ctx context.Context, tenantID, sessionID s } // GetManagedSessionArchive reads one database snapshot and never contacts compute. -func (s *Store) GetManagedSessionArchive(ctx context.Context, tenantID, sessionID string) (ManagedSessionArchive, error) { +func (s *Store) GetManagedSessionArchive(ctx context.Context, tenantID, sessionID string) (sessions.ManagedArchive, error) { tenant, err := parseID(tenantID) if err != nil { - return ManagedSessionArchive{}, err + return sessions.ManagedArchive{}, err } return getManagedSessionArchive(ctx, s.queries, tenant, pgunit.PathID(sessionID)) } -func getManagedSessionArchive(ctx context.Context, q *sqlc.Queries, tenant, session pgtype.UUID) (ManagedSessionArchive, error) { +func getManagedSessionArchive(ctx context.Context, q *sqlc.Queries, tenant, session pgtype.UUID) (sessions.ManagedArchive, error) { row, err := q.GetManagedSessionArchive(ctx, sqlc.GetManagedSessionArchiveParams{TenantID: tenant, ID: session}) if errors.Is(err, pgx.ErrNoRows) { - return ManagedSessionArchive{}, ErrNotFound + return sessions.ManagedArchive{}, sessions.ErrNotFound } if err != nil { - return ManagedSessionArchive{}, err + return sessions.ManagedArchive{}, err } if row.EnvironmentType != "openai_hosted" { - return ManagedSessionArchive{}, ErrInvalidInput + return sessions.ManagedArchive{}, sessions.ErrInvalidInput } - return ManagedSessionArchive{SessionID: runtimeUUID(row.SessionID), EnvironmentID: runtimeUUID(row.EnvironmentID), State: row.State}, nil + return sessions.ManagedArchive{SessionID: runtimeUUID(row.SessionID), EnvironmentID: runtimeUUID(row.EnvironmentID), State: row.State}, nil } diff --git a/services/core/internal/store/admin_session_archive_race_test.go b/services/core/internal/store/admin_session_archive_race_test.go index 029f57bc4..147bad1fe 100644 --- a/services/core/internal/store/admin_session_archive_race_test.go +++ b/services/core/internal/store/admin_session_archive_race_test.go @@ -11,6 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestManagedSessionArchiveReleasesPendingNodePlacement(t *testing.T) { @@ -58,8 +59,8 @@ func TestManagedSessionArchiveOrdersConcurrentInput(t *testing.T) { go func() { defer wg.Done() <-start - _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "racing-input", []Input{{Kind: "message", Payload: json.RawMessage(`{"text":"racing"}`)}}) - if err != nil && !errors.Is(err, ErrEnvironmentUnavailable) { + _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "racing-input", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"racing"}`)}}) + if err != nil && !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Error(err) } }() diff --git a/services/core/internal/store/admin_session_archive_test.go b/services/core/internal/store/admin_session_archive_test.go index 61ad06459..78396305a 100644 --- a/services/core/internal/store/admin_session_archive_test.go +++ b/services/core/internal/store/admin_session_archive_test.go @@ -36,7 +36,7 @@ func managedArchiveFixture(t *testing.T) (*Store, *Store, string) { return s, w, installation } -func managedArchiveSession(t *testing.T, s *Store, input CreateSessionInput) (string, Session) { +func managedArchiveSession(t *testing.T, s *Store, input sessions.CreateSession) (string, sessions.Session) { t.Helper() tenant := uuid.NewString() session, err := s.CreateSession(t.Context(), tenant, input) @@ -52,7 +52,7 @@ func managedArchiveSession(t *testing.T, s *Store, input CreateSessionInput) (st return tenant, session } -func archiveAllocation(t *testing.T, w *Store, tenant string, session Session, installation string) RuntimeAllocation { +func archiveAllocation(t *testing.T, w *Store, tenant string, session sessions.Session, installation string) RuntimeAllocation { t.Helper() owner, err := w.ReserveRuntimeAllocation(t.Context(), tenant, session.Environment.ID, installation, runtimedevice.HashCredential(uuid.NewString())) if err != nil { @@ -64,7 +64,7 @@ func archiveAllocation(t *testing.T, w *Store, tenant string, session Session, i func TestManagedSessionArchiveUnallocatedAndGuards(t *testing.T) { s, w, installation := managedArchiveFixture(t) input := managerSessionInput(uuid.NewString()) - input.InitialInputs = []Input{{Kind: "message", Payload: json.RawMessage(`{"text":"waiting"}`)}} + input.InitialInputs = []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"waiting"}`)}} tenant, session := managedArchiveSession(t, s, input) ctx := adminDeleteContext(t.Context(), tenant, uuid.NewString()) active, err := s.GetManagedSessionArchive(t.Context(), tenant, session.ID) @@ -80,17 +80,17 @@ func TestManagedSessionArchiveUnallocatedAndGuards(t *testing.T) { t.Fatal("unleased archive accepted", err) } for _, other := range []string{uuid.NewString(), "malformed"} { - if _, err := w.ArchiveManagedSession(ctx, tenant, other, 1); !errors.Is(err, ErrNotFound) { + if _, err := w.ArchiveManagedSession(ctx, tenant, other, 1); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unknown archive", err) } - if _, err := s.GetManagedSessionArchive(ctx, tenant, other); !errors.Is(err, ErrNotFound) { + if _, err := s.GetManagedSessionArchive(ctx, tenant, other); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unknown status", err) } } - if _, err := w.ArchiveManagedSession(ctx, uuid.NewString(), session.ID, 1); !errors.Is(err, ErrNotFound) { + if _, err := w.ArchiveManagedSession(ctx, uuid.NewString(), session.ID, 1); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign archive", err) } - if _, err := s.GetManagedSessionArchive(ctx, uuid.NewString(), session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetManagedSessionArchive(ctx, uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign status", err) } result, err := w.ArchiveManagedSession(ctx, tenant, session.ID, 1) @@ -113,10 +113,10 @@ func TestManagedSessionArchiveUnallocatedAndGuards(t *testing.T) { if status, err := s.GetManagedSessionArchive(ctx, tenant, session.ID); err != nil || status != result { t.Fatal("status differs from committed archive", status, err) } - if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, session.Environment.ID, installation, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, ErrInvalidInput) { + if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, session.Environment.ID, installation, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("archived Environment allocated after archive", err) } - if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "later", []Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}); !errors.Is(err, ErrEnvironmentUnavailable) { + if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "later", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}); !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Fatal("archived Environment accepted new input", err) } view, err := deploymentService(t, s).View(t.Context()) @@ -136,7 +136,7 @@ func TestManagedSessionArchiveRetainsHistoryAndSettledResources(t *testing.T) { } input := submitMessage(t, s, tenant, session.ID, "completed") transition(t, w, tenant, session.ID, input.TurnID, sessions.TurnQueued, sessions.TurnInProgress) - if err := w.AppendTurnEvents(t.Context(), tenant, session.ID, input.TurnID, 1, []ExecutionEvent{{Kind: "output_message", Payload: json.RawMessage(`{"id":"answer","status":"completed","text":"retained"}`)}}); err != nil { + if err := w.AppendTurnEvents(t.Context(), tenant, session.ID, input.TurnID, 1, []sessions.ExecutionEvent{{Kind: "output_message", Payload: json.RawMessage(`{"id":"answer","status":"completed","text":"retained"}`)}}); err != nil { t.Fatal(err) } body := []byte("retained artifact") @@ -157,7 +157,7 @@ func TestManagedSessionArchiveRetainsHistoryAndSettledResources(t *testing.T) { if _, ok, err := s.GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { t.Fatal("archive retained runtime authority", err) } - if _, err := w.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, ErrTurnConflict) { + if _, err := w.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("archive discarded unknown Create ownership", err) } replay, err := w.ReserveRuntimeAllocation(t.Context(), tenant, session.Environment.ID, installation, runtimedevice.HashCredential(uuid.NewString())) @@ -184,7 +184,7 @@ func TestManagedSessionArchiveRetainsHistoryAndSettledResources(t *testing.T) { if err != nil || len(page.Artifacts) != 1 { t.Fatal(page, err) } - if err := s.ReadSessionArtifact(t.Context(), tenant, session.ID, page.Artifacts[0].ID, func(_ SessionArtifact, r io.Reader) error { + if err := s.ReadSessionArtifact(t.Context(), tenant, session.ID, page.Artifacts[0].ID, func(_ sessions.Artifact, r io.Reader) error { got, err := io.ReadAll(r) if !bytes.Equal(got, body) { t.Error("archive damaged published artifact bytes") @@ -235,7 +235,7 @@ func TestManagedSessionArchivePreservesFailuresAndRejectsSelfHosted(t *testing.T if _, err := s.pool.Exec(t.Context(), "UPDATE environments SET initialization='running' WHERE id=$1", owner.EnvironmentID); err != nil { t.Fatal(err) } - if err := w.FailEnvironmentInitialization(t.Context(), EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: owner.DeviceID}, ProvisioningFailure{Step: ProvisioningSetupCommand, Index: 0, ExitCode: 2}); err != nil { + if err := w.FailEnvironmentInitialization(t.Context(), sessions.EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: owner.DeviceID}, sessions.ProvisioningFailure{Step: sessions.ProvisioningSetupCommand, Index: 0, ExitCode: 2}); err != nil { t.Fatal(err) } failed, err := s.GetSession(t.Context(), tenant, session.ID) @@ -251,10 +251,10 @@ func TestManagedSessionArchivePreservesFailuresAndRejectsSelfHosted(t *testing.T t.Fatal("archive changed recorded provisioning failure", after, err) } before := adminMutationSnapshot(t, s, "sessions", "environments", "admin_audit_log") - if _, err := w.ArchiveManagedSession(adminDeleteContext(t.Context(), otherTenant, uuid.NewString()), otherTenant, selfHosted.ID, 1); !errors.Is(err, ErrInvalidInput) { + if _, err := w.ArchiveManagedSession(adminDeleteContext(t.Context(), otherTenant, uuid.NewString()), otherTenant, selfHosted.ID, 1); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("self-hosted archive accepted", err) } - if _, err := s.GetManagedSessionArchive(t.Context(), otherTenant, selfHosted.ID); !errors.Is(err, ErrInvalidInput) { + if _, err := s.GetManagedSessionArchive(t.Context(), otherTenant, selfHosted.ID); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("self-hosted cleanup projected", err) } if !reflect.DeepEqual(before, adminMutationSnapshot(t, s, "sessions", "environments", "admin_audit_log")) { diff --git a/services/core/internal/store/admin_session_archive_worker_http_test.go b/services/core/internal/store/admin_session_archive_worker_http_test.go index 788c2aebc..4a3f9d05e 100644 --- a/services/core/internal/store/admin_session_archive_worker_http_test.go +++ b/services/core/internal/store/admin_session_archive_worker_http_test.go @@ -72,9 +72,9 @@ func TestAdminSessionArchiveWorkerHTTPPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - create := func() store.Session { + create := func() sessions.Session { t.Helper() - session, err := s.CreateSession(t.Context(), project.TenantID, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`)}) + session, err := s.CreateSession(t.Context(), project.TenantID, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`)}) if err != nil { t.Fatal(err) } @@ -110,7 +110,7 @@ func TestAdminSessionArchiveWorkerHTTPPostgres(t *testing.T) { return w } w := request(http.MethodPost, active.ID) - var archived store.ManagedSessionArchive + var archived sessions.ManagedArchive if w.Code != http.StatusOK || json.Unmarshal(w.Body.Bytes(), &archived) != nil || archived.State != "cleanup_pending" || archived.SessionID != active.ID { t.Fatalf("archive did not use Worker's leased Store: %d %s", w.Code, w.Body) } diff --git a/services/core/internal/store/admin_summary.go b/services/core/internal/store/admin_summary.go index 5f24af4a7..d9d665b5f 100644 --- a/services/core/internal/store/admin_summary.go +++ b/services/core/internal/store/admin_summary.go @@ -6,6 +6,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -24,14 +25,14 @@ type AdminAssetCounts struct { // ReadAdminSummary visits one space's Sessions from one read-only snapshot. The // visitor reuses the API's Session projection instead of creating another status // or usage model. Paging keeps the stored configurations out of an unbounded slice. -func (s *Store) ReadAdminSummary(ctx context.Context, tenantID string, filter AdminSummaryFilter, visit func(Session, *string) error) (AdminAssetCounts, error) { +func (s *Store) ReadAdminSummary(ctx context.Context, tenantID string, filter AdminSummaryFilter, visit func(sessions.Session, *string) error) (AdminAssetCounts, error) { var counts AdminAssetCounts tenant, err := parseID(tenantID) if err != nil { return counts, err } if visit == nil || filter.CreatedAfter != nil && filter.CreatedBefore != nil && !filter.CreatedAfter.Before(*filter.CreatedBefore) { - return counts, ErrInvalidInput + return counts, sessions.ErrInvalidInput } err = s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { q := s.queries.WithTx(tx) @@ -82,7 +83,7 @@ type AdminRuntimeTargetPage struct { func (s *Store) ListAdminRuntimeTargets(ctx context.Context, tenantIDs []string, after string, limit int, ascending bool) (AdminRuntimeTargetPage, error) { page := AdminRuntimeTargetPage{Data: []AdminRuntimeTarget{}} if limit < 1 || limit > 100 { - return page, ErrInvalidInput + return page, sessions.ErrInvalidInput } tenants := make([]pgtype.UUID, 0, len(tenantIDs)) for _, value := range tenantIDs { @@ -97,11 +98,11 @@ func (s *Store) ListAdminRuntimeTargets(ctx context.Context, tenantIDs []string, var err error params.AfterID, err = parseID(after) if err != nil { - return page, ErrNotFound + return page, sessions.ErrNotFound } params.AfterTime, err = s.queries.AdminRuntimeCursor(ctx, sqlc.AdminRuntimeCursorParams{ID: params.AfterID, TenantIds: tenants}) if errors.Is(err, pgx.ErrNoRows) { - return page, ErrNotFound + return page, sessions.ErrNotFound } if err != nil { return page, err diff --git a/services/core/internal/store/admin_validation.go b/services/core/internal/store/admin_validation.go deleted file mode 100644 index 00e819a7a..000000000 --- a/services/core/internal/store/admin_validation.go +++ /dev/null @@ -1,12 +0,0 @@ -package store - -// AdminValidationError adds field identity without changing the store error text -// or ErrInvalidInput classification. Bounds are fixed validator constants. -type AdminValidationError struct { - Code, Param string - MaxLength int - message string -} - -func (e *AdminValidationError) Error() string { return e.message } -func (e *AdminValidationError) Unwrap() error { return ErrInvalidInput } diff --git a/services/core/internal/store/archive_cancellation_test.go b/services/core/internal/store/archive_cancellation_test.go index f8dcc28f3..3545bd280 100644 --- a/services/core/internal/store/archive_cancellation_test.go +++ b/services/core/internal/store/archive_cancellation_test.go @@ -59,7 +59,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { t.Fatal(err) } configuration := strings.Replace(functionConfiguration, `"type":"none"`, `"type":"openai_hosted","network":{"access":"disabled"}`, 1) - session, err := s.CreateSession(t.Context(), project.TenantID, store.WithFixtureModelProvider(store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(configuration)})) + session, err := s.CreateSession(t.Context(), project.TenantID, store.WithFixtureModelProvider(sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(configuration)})) if err != nil { t.Fatal(err) } @@ -118,7 +118,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { } time.Sleep(time.Millisecond) } - pending, err := s.ReserveEnvironmentInput(t.Context(), h.tenant, session.ID, "pending", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}, {Kind: "message", Payload: json.RawMessage(`{"text":"second"}`)}}) + pending, err := s.ReserveEnvironmentInput(t.Context(), h.tenant, session.ID, "pending", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}, {Kind: "message", Payload: json.RawMessage(`{"text":"second"}`)}}) if err != nil { t.Fatal(err) } @@ -129,7 +129,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { handle := acknowledgePreparation(h, frame.ID) start := readyPreparedDispatch(t, h, frame.ID, handle) h.write(frame.ID, proto.TypePreparationStatus, proto.PreparationStatusPayload{Handle: handle, Revision: 3, State: "started", RunID: start.RunID}) - input := store.InputReceipt{TurnID: start.RunID} + input := sessions.InputReceipt{TurnID: start.RunID} h.write(input.TurnID, proto.TypeFunctionCall, proto.FunctionCallPayload{CallID: "pending", Name: "lookup_ticket", Arguments: json.RawMessage(`{"ticket":"42"}`)}) state := functionState(t, h, 1) if state.LastTurn.Status != sessions.TurnWaiting { diff --git a/services/core/internal/store/artifact_capture.go b/services/core/internal/store/artifact_capture.go index 06b8e88a5..7a449027f 100644 --- a/services/core/internal/store/artifact_capture.go +++ b/services/core/internal/store/artifact_capture.go @@ -28,7 +28,7 @@ func (s *Store) StageTurnArtifacts(ctx context.Context, tenantID, sessionID, tur } environment, err := parseID(environmentID) if err != nil || input == nil { - return ErrInvalidInput + return sessions.ErrInvalidInput } // Authorize before reading caller-controlled bytes or allocating storage. owned, err := s.GetSessionEnvironment(ctx, tenantID, sessionID) @@ -36,7 +36,7 @@ func (s *Store) StageTurnArtifacts(ctx context.Context, tenantID, sessionID, tur return err } if owned.ID != environmentID { - return ErrNotFound + return sessions.ErrNotFound } var configuration struct { Type string `json:"type"` @@ -45,7 +45,7 @@ func (s *Store) StageTurnArtifacts(ctx context.Context, tenantID, sessionID, tur return err } if configuration.Type != "openai_hosted" && configuration.Type != "self_hosted" { - return ErrInvalidInput + return sessions.ErrInvalidInput } return s.pooled.Transaction(ctx, func(ctx context.Context, tx pgx.Tx) error { rows, err := captureArtifactArchive(ctx, tx, input) @@ -55,20 +55,20 @@ func (s *Store) StageTurnArtifacts(ctx context.Context, tenantID, sessionID, tur q := s.queries.WithTx(tx) locked, err := q.LockSession(ctx, sqlc.LockSessionParams{TenantID: lookup.TenantID, ID: lookup.SessionID}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } if locked.DeletedAt.Valid { - return ErrNotFound + return sessions.ErrNotFound } turn, err := q.GetTurn(ctx, lookup) if err != nil { return err } if turn.Status != sessions.TurnInProgress || turn.CancelRequestedAt.Valid { - return ErrTurnConflict + return sessions.ErrTurnConflict } for _, row := range rows { row.SessionID, row.TurnID, row.EnvironmentID = lookup.SessionID, lookup.ID, environment @@ -94,7 +94,7 @@ func captureArtifactArchive(ctx context.Context, tx pgx.Tx, input io.Reader) ([] return nil, err } if header.Typeflag != tar.TypeReg || !strings.HasPrefix(header.Name, "outputs/") || !fs.ValidPath(header.Name) || strings.ContainsAny(header.Name, "\\\x00\r\n") || len(header.Name) > 4096 || header.Size < 0 || header.Size > MaxArtifactBytes || header.Size > MaxArtifactBatchBytes-total || len(rows) >= 4096 || seen[header.Name] { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } seen[header.Name] = true total += header.Size @@ -117,11 +117,11 @@ func captureArtifactArchive(ctx context.Context, tx pgx.Tx, input io.Reader) ([] return nil, err } if len(padding) > 32768 { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } for _, b := range padding { if b != 0 { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } } return rows, nil diff --git a/services/core/internal/store/artifact_lifecycle.go b/services/core/internal/store/artifact_lifecycle.go index e15576957..d0118232d 100644 --- a/services/core/internal/store/artifact_lifecycle.go +++ b/services/core/internal/store/artifact_lifecycle.go @@ -18,29 +18,29 @@ func (s *Store) BeginTurnArtifactCapture(ctx context.Context, tenantID, sessionI return err } if appliedThrough < 0 { - return ErrInvalidInput + return sessions.ErrInvalidInput } return s.withSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { turn, err := q.GetTurn(ctx, lookup) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } if turn.Status != sessions.TurnInProgress || turn.CancelRequestedAt.Valid { - return ErrTurnConflict + return sessions.ErrTurnConflict } pending, err := q.HasUnappliedMessages(ctx, sqlc.HasUnappliedMessagesParams{SessionID: session, TurnID: lookup.ID, Sequence: appliedThrough}) if err != nil { return err } if pending { - return ErrUnappliedInputs + return sessions.ErrUnappliedInputs } count, err := q.BeginTurnArtifactCapture(ctx, sqlc.BeginTurnArtifactCaptureParams{SessionID: session, ID: lookup.ID}) if err == nil && count != 1 { - return ErrTurnConflict + return sessions.ErrTurnConflict } return err }) diff --git a/services/core/internal/store/claude_execution_test.go b/services/core/internal/store/claude_execution_test.go index a28d11fca..95cfaddfa 100644 --- a/services/core/internal/store/claude_execution_test.go +++ b/services/core/internal/store/claude_execution_test.go @@ -18,7 +18,7 @@ import ( func claudeSession(t *testing.T, h *dispatchHarness, configuration string, prebound bool) { t.Helper() var err error - h.session, err = h.s.CreateSession(t.Context(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "claude_sdk", IdempotencyKey: "claude", Configuration: json.RawMessage(configuration)}) + h.session, err = h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "claude_sdk", IdempotencyKey: "claude", Configuration: json.RawMessage(configuration)}) if err != nil { t.Fatal(err) } @@ -73,7 +73,7 @@ func TestClaudeWorkerSelectsStoredEngineAndRestrictiveCapabilities(t *testing.T) t.Fatal(turn, err) } if !prebound { - if _, err := h.s.GetSessionDevice(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSessionDevice(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("bound an incapable device", err) } } @@ -126,22 +126,22 @@ func TestClaudeInvalidImageResultRejectsWholeBatchBeforePersistence(t *testing.T worker := startWorker(t, t.Context(), h.db, h.d) defer func() { ctx, cancel := context.WithCancel(context.Background()); cancel(); _ = worker.Run(ctx) }() input := h.message("start", "Run") - if _, err := h.s.TransitionTurn(t.Context(), h.tenant, h.session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := h.s.TransitionTurn(t.Context(), h.tenant, h.session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - call := store.FunctionCall{CallID: "public-call", ExecutorCallID: "native-call", Name: "lookup_ticket", Arguments: json.RawMessage(`{"ticket":"42"}`)} + call := sessions.FunctionCall{CallID: "public-call", ExecutorCallID: "native-call", Name: "lookup_ticket", Arguments: json.RawMessage(`{"ticket":"42"}`)} if err := h.s.RecordFunctionCall(t.Context(), h.tenant, h.session.ID, input.TurnID, call); err != nil { t.Fatal(err) } - result := func(raw string) store.Input { - payload, err := json.Marshal(store.FunctionResultInput{TurnID: input.TurnID, CallID: call.CallID, Result: json.RawMessage(raw)}) + result := func(raw string) sessions.Input { + payload, err := json.Marshal(sessions.FunctionResultInput{TurnID: input.TurnID, CallID: call.CallID, Result: json.RawMessage(raw)}) if err != nil { t.Fatal(err) } - return store.Input{Kind: "tool_result", Payload: payload} + return sessions.Input{Kind: "tool_result", Payload: payload} } - batch := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"Follow up"}`)}, result(`{"success":true,"output":[{"type":"input_image","image_url":"data:image/png;base64,AA=="}]}`), {Kind: "cancel", Payload: json.RawMessage(`{}`)}} - if _, err := worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "batch", batch); !errors.Is(err, store.ErrInvalidInput) { + batch := []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"Follow up"}`)}, result(`{"success":true,"output":[{"type":"input_image","image_url":"data:image/png;base64,AA=="}]}`), {Kind: "cancel", Payload: json.RawMessage(`{}`)}} + if _, err := worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "batch", batch); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal(err) } saved, err := h.s.GetFunctionCall(t.Context(), h.tenant, h.session.ID, input.TurnID, call.CallID) diff --git a/services/core/internal/store/claude_mcp_test.go b/services/core/internal/store/claude_mcp_test.go index 364ebc55f..92d4b6ea7 100644 --- a/services/core/internal/store/claude_mcp_test.go +++ b/services/core/internal/store/claude_mcp_test.go @@ -11,7 +11,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestClaudeMCPWaitsForCapableRuntime(t *testing.T) { @@ -61,7 +60,7 @@ func TestClaudeMCPWaitsForCapableRuntime(t *testing.T) { t.Fatal("incapable runtime claimed work", turn, err) } if !prebound { - if _, err := h.s.GetSessionDevice(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSessionDevice(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("bound an incapable runtime", err) } } diff --git a/services/core/internal/store/command_output_test.go b/services/core/internal/store/command_output_test.go index 7e4c42781..08c87b0eb 100644 --- a/services/core/internal/store/command_output_test.go +++ b/services/core/internal/store/command_output_test.go @@ -18,7 +18,7 @@ func TestCommandOutputCommitsFragmentsSnapshotsAndRecovery(t *testing.T) { s, pool := store.NewTestStore(t) defer pool.Close() tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "command-output"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "command-output"}) if err != nil { t.Fatal(err) } @@ -26,13 +26,13 @@ func TestCommandOutputCommitsFragmentsSnapshotsAndRecovery(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - event := func(kind, raw string) store.ExecutionEvent { - return store.ExecutionEvent{Kind: kind, Payload: json.RawMessage(raw)} + event := func(kind, raw string) sessions.ExecutionEvent { + return sessions.ExecutionEvent{Kind: kind, Payload: json.RawMessage(raw)} } - batch := []store.ExecutionEvent{ + batch := []sessions.ExecutionEvent{ event("tool_call", `{"id":"cmd","stage":"before","observation":{"kind":"command","command":"run","status":"in_progress"}}`), event("command_output", `{"id":"cmd","delta":"same\n"}`), event("command_output", `{"id":"cmd","delta":"same\n"}`), @@ -44,7 +44,7 @@ func TestCommandOutputCommitsFragmentsSnapshotsAndRecovery(t *testing.T) { } before, _ := s.SessionEventCursor(ctx, tenant, session.ID) // A bad command reference rolls back preceding valid fragments and their events. - if err := s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, []store.ExecutionEvent{ + if err := s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, []sessions.ExecutionEvent{ event("command_output", `{"id":"cmd","delta":"rollback"}`), event("command_output", `{"id":"unknown","delta":"orphan"}`), }); err == nil { @@ -61,7 +61,7 @@ func TestCommandOutputCommitsFragmentsSnapshotsAndRecovery(t *testing.T) { if page.Items[1].Output != "same\nsame\n" { t.Fatal("draft output lost", page.Items[1]) } - final := []store.ExecutionEvent{ + final := []sessions.ExecutionEvent{ event("tool_call", `{"id":"cmd","stage":"after","observation":{"kind":"command","command":"run","status":"completed","output":"authoritative","exit_code":0}}`), event("command_output", `{"id":"cmd","delta":"late"}`), event("tool_call", `{"id":"partial","stage":"before","observation":{"kind":"command","command":"wait","status":"in_progress"}}`), @@ -87,7 +87,7 @@ func TestCommandOutputCommitsFragmentsSnapshotsAndRecovery(t *testing.T) { if before != after { t.Fatal("query replayed events") } - if _, err := reopened.ListSessionEvents(ctx, uuid.NewString(), session.ID, 0); !errors.Is(err, store.ErrNotFound) { + if _, err := reopened.ListSessionEvents(ctx, uuid.NewString(), session.ID, 0); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign event access", err) } var fragments []string diff --git a/services/core/internal/store/core_metrics.go b/services/core/internal/store/core_metrics.go index 6af05ffaf..e6e30dcbb 100644 --- a/services/core/internal/store/core_metrics.go +++ b/services/core/internal/store/core_metrics.go @@ -5,6 +5,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -66,7 +67,7 @@ func (s *Store) ReadCoreExecutionHistory(ctx context.Context, start, end time.Ti if resolution < time.Second || resolution%time.Second != 0 || span <= 0 || span > 7*24*time.Hour || span%resolution != 0 || span/resolution > 1008 || start.Nanosecond() != 0 || end.Nanosecond() != 0 || start.Unix()%int64(resolution/time.Second) != 0 || end.Unix()%int64(resolution/time.Second) != 0 { - return CoreExecutionHistory{}, ErrInvalidInput + return CoreExecutionHistory{}, sessions.ErrInvalidInput } result := CoreExecutionHistory{Buckets: make([]CoreQueueWaitBucket, int(span/resolution))} for i := range result.Buckets { diff --git a/services/core/internal/store/core_metrics_test.go b/services/core/internal/store/core_metrics_test.go index c208e8531..9fb2f6c2a 100644 --- a/services/core/internal/store/core_metrics_test.go +++ b/services/core/internal/store/core_metrics_test.go @@ -10,6 +10,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func coreMetricsSession(t *testing.T, pool *pgxpool.Pool, deleted bool) string { @@ -169,7 +171,7 @@ func TestCoreMetricsHistoryBounds(t *testing.T) { {start, start.Add(time.Hour + time.Second), time.Minute}, {start.Add(time.Nanosecond), start.Add(time.Hour + time.Nanosecond), time.Minute}, } { - if _, err := s.ReadCoreExecutionHistory(t.Context(), tc.start, tc.end, tc.step); !errors.Is(err, ErrInvalidInput) { + if _, err := s.ReadCoreExecutionHistory(t.Context(), tc.start, tc.end, tc.step); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("unbounded or unaligned range accepted: %+v, %v", tc, err) } } diff --git a/services/core/internal/store/creation_stream_settlement_public_test.go b/services/core/internal/store/creation_stream_settlement_public_test.go index 430def91c..1c4c23c19 100644 --- a/services/core/internal/store/creation_stream_settlement_public_test.go +++ b/services/core/internal/store/creation_stream_settlement_public_test.go @@ -12,7 +12,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -170,7 +170,7 @@ func TestCreationStreamPublicLifetimes(t *testing.T) { created.ended(t, 5*time.Second) connect(first.Session.Environment.ID) - if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, first.Session.ID, "later", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}); err != nil { + if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, first.Session.ID, "later", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}); err != nil { t.Fatal(err) } if current, err := s.GetSession(t.Context(), tenant, first.Session.ID); err != nil || !current.PendingInput { @@ -210,7 +210,7 @@ func TestCreationStreamPublicLifetimes(t *testing.T) { if err != nil { t.Fatal(err) } - if settled, err := s.CancelEnvironmentInput(t.Context(), tenant, session, reservation); err != nil || settled.State != store.EnvironmentInputCancelled { + if settled, err := s.CancelEnvironmentInput(t.Context(), tenant, session, reservation); err != nil || settled.State != sessions.EnvironmentInputCancelled { t.Fatal(settled.State, err) } if after, err := s.SessionEventCursor(t.Context(), tenant, session); err != nil || after != cursor { diff --git a/services/core/internal/store/credential_matrix_http_test.go b/services/core/internal/store/credential_matrix_http_test.go index bad59e53e..1078e395a 100644 --- a/services/core/internal/store/credential_matrix_http_test.go +++ b/services/core/internal/store/credential_matrix_http_test.go @@ -20,6 +20,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeenrollment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -75,7 +76,7 @@ func TestCredentialNamespaceMatrix(t *testing.T) { if err != nil { t.Fatal(err) } - session, err := s.CreateSession(ctx, binding.Principal.TenantID, store.CreateSessionInput{Creator: binding.Principal.Subject(), Engine: "codex", IdempotencyKey: uuid.NewString(), + session, err := s.CreateSession(ctx, binding.Principal.TenantID, sessions.CreateSession{Creator: binding.Principal.Subject(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"self_hosted","workspace_directory":"/workspace","capability_directories":[]}}`)}) if err != nil { t.Fatal(err) @@ -84,7 +85,7 @@ func TestCredentialNamespaceMatrix(t *testing.T) { if err != nil { t.Fatal(err) } - var executor store.IssuedExecutorCredential + var executor sessions.IssuedExecutorCredential created("POST", "/core/v1/projects/"+project.ID+"/environments/"+environment.ID+"/executor-credentials", coreKey, `{"key_id":"`+uuid.NewString()+`"}`, &executor) // A node credential: a Docker deployment, an enrollment token issued with the Core key, and an enrolled node. diff --git a/services/core/internal/store/deployment_model_providers_http_test.go b/services/core/internal/store/deployment_model_providers_http_test.go index ae75ac86d..66592c120 100644 --- a/services/core/internal/store/deployment_model_providers_http_test.go +++ b/services/core/internal/store/deployment_model_providers_http_test.go @@ -21,6 +21,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/modelconfigurationpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -235,14 +236,14 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { // reserved before the upgrade fails with that reason instead of waiting. func TestLegacySessionWithoutProviderCannotStartWork(t *testing.T) { h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), true) - legacy, err := h.s.CreateSession(t.Context(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), + legacy, err := h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: []byte(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) if err != nil { t.Fatal(err) } executor := connectFixtureRuntime(t, h, legacy) // Reserved directly, as a pre-upgrade Core did. - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, legacy.ID, "before-upgrade", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"old"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, legacy.ID, "before-upgrade", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"old"}`)}}) if err != nil { t.Fatal(err) } @@ -258,7 +259,7 @@ func TestLegacySessionWithoutProviderCannotStartWork(t *testing.T) { return count } before := reservations() - message := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"start"}`)}} + message := []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"start"}`)}} if _, err := worker.SubmitInputs(t.Context(), h.tenant, legacy.ID, uuid.NewString(), message); !errors.Is(err, execution.ErrModelProviderRequired) { t.Fatal("provider-free Session accepted work", err) } @@ -267,7 +268,7 @@ func TestLegacySessionWithoutProviderCannotStartWork(t *testing.T) { } awaitDaemonRemoteCondition(t, t.Context(), 5*time.Second, "legacy reservation settled", func() bool { got, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, legacy.ID, pending.ID) - return err == nil && got.State == store.EnvironmentInputFailed + return err == nil && got.State == sessions.EnvironmentInputFailed }) session, err := h.s.GetSession(t.Context(), h.tenant, legacy.ID) if err != nil || session.EnvironmentInputActivity == nil || session.EnvironmentInputActivity.Failure != "model_provider_required" { diff --git a/services/core/internal/store/devices.go b/services/core/internal/store/devices.go index 66b780d64..9d0cf9827 100644 --- a/services/core/internal/store/devices.go +++ b/services/core/internal/store/devices.go @@ -14,46 +14,31 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -var ErrDeviceBindingConflict = errors.New("session is already bound to a different device") - -// ExecutionDevice contains safe identity only, never a device credential. -type ExecutionDevice struct { - ID string - Name string - EnvironmentID string -} - -// SessionExecutionBinding identifies the Runtime and native history selected for one API Session. -type SessionExecutionBinding struct { - Device ExecutionDevice - NativeSessionID string - HasStartedTurn bool -} - // CreateDevice is operator provisioning, not a tenant-facing registration API. -func (s *Store) CreateDevice(ctx context.Context, tenantID, name, credentialHash string) (ExecutionDevice, error) { +func (s *Store) CreateDevice(ctx context.Context, tenantID, name, credentialHash string) (sessions.ExecutionDevice, error) { tenant, err := parseID(tenantID) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } params, err := newDeviceParams(tenant, name, credentialHash) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } id, err := s.queries.CreateDevice(ctx, params) if err != nil { - return ExecutionDevice{}, fmt.Errorf("create execution device: %w", err) + return sessions.ExecutionDevice{}, fmt.Errorf("create execution device: %w", err) } - return ExecutionDevice{ID: uuid.UUID(id.Bytes).String(), Name: params.Name}, nil + return sessions.ExecutionDevice{ID: uuid.UUID(id.Bytes).String(), Name: params.Name}, nil } func newDeviceParams(tenant pgtype.UUID, name, credentialHash string) (sqlc.CreateDeviceParams, error) { name = strings.TrimSpace(name) digest, err := hex.DecodeString(credentialHash) if err != nil || len(digest) != 32 || name == "" || len(name) > 256 { - return sqlc.CreateDeviceParams{}, fmt.Errorf("%w: device name and SHA-256 credential digest required", ErrInvalidInput) + return sqlc.CreateDeviceParams{}, fmt.Errorf("%w: device name and SHA-256 credential digest required", sessions.ErrInvalidInput) } return sqlc.CreateDeviceParams{ID: pgtype.UUID{Bytes: uuid.New(), Valid: true}, TenantID: tenant, Name: name, CredentialHash: pgtype.Text{String: hex.EncodeToString(digest), Valid: true}}, nil @@ -84,7 +69,7 @@ func (s *Store) RevokeDevice(ctx context.Context, tenantID, deviceID string) err } n, err := s.queries.RevokeDevice(ctx, sqlc.RevokeDeviceParams(params)) if err == nil && n == 0 { - return ErrNotFound + return sessions.ErrNotFound } return err } @@ -98,70 +83,70 @@ func (s *Store) BindSessionDevice(ctx context.Context, tenantID, sessionID, devi } return s.withSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { if _, err := q.GetDevice(ctx, params); errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } else if err != nil { return err } _, err := q.BindSessionDevice(ctx, sqlc.BindSessionDeviceParams{TenantID: params.TenantID, ID: session, ID_2: params.ID}) if errors.Is(err, pgx.ErrNoRows) { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } return err }) } -func (s *Store) GetSessionDevice(ctx context.Context, tenantID, sessionID string) (ExecutionDevice, error) { +func (s *Store) GetSessionDevice(ctx context.Context, tenantID, sessionID string) (sessions.ExecutionDevice, error) { params, err := deviceLookup(tenantID, sessionID) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } if err := s.requireInitializedEnvironment(ctx, params.TenantID, params.ID); err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } return s.GetSessionRuntimeDevice(ctx, tenantID, sessionID) } // GetSessionRuntimeDevice reports an authorized connection binding. It does not // admit native execution or file access before Environment preparation completes. -func (s *Store) GetSessionRuntimeDevice(ctx context.Context, tenantID, sessionID string) (ExecutionDevice, error) { +func (s *Store) GetSessionRuntimeDevice(ctx context.Context, tenantID, sessionID string) (sessions.ExecutionDevice, error) { params, err := deviceLookup(tenantID, sessionID) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } row, err := s.queries.GetSessionDevice(ctx, sqlc.GetSessionDeviceParams(params)) if errors.Is(err, pgx.ErrNoRows) { - return ExecutionDevice{}, ErrNotFound + return sessions.ExecutionDevice{}, sessions.ErrNotFound } if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } return executionDevice(row.ID, row.Name, row.EnvironmentID), nil } -func (s *Store) GetSessionExecutionBinding(ctx context.Context, tenantID, sessionID string) (SessionExecutionBinding, error) { +func (s *Store) GetSessionExecutionBinding(ctx context.Context, tenantID, sessionID string) (sessions.ExecutionBinding, error) { params, err := deviceLookup(tenantID, sessionID) if err != nil { - return SessionExecutionBinding{}, err + return sessions.ExecutionBinding{}, err } if err := s.requireInitializedEnvironment(ctx, params.TenantID, params.ID); err != nil { - return SessionExecutionBinding{}, err + return sessions.ExecutionBinding{}, err } row, err := s.queries.GetSessionExecutionBinding(ctx, sqlc.GetSessionExecutionBindingParams(params)) if errors.Is(err, pgx.ErrNoRows) { - return SessionExecutionBinding{}, ErrNotFound + return sessions.ExecutionBinding{}, sessions.ErrNotFound } if err != nil { - return SessionExecutionBinding{}, err + return sessions.ExecutionBinding{}, err } - return SessionExecutionBinding{ + return sessions.ExecutionBinding{ Device: executionDevice(row.ID, row.Name, row.EnvironmentID), NativeSessionID: row.NativeSessionID, HasStartedTurn: row.HasStartedTurn, }, nil } -func executionDevice(id pgtype.UUID, name string, environmentID pgtype.UUID) ExecutionDevice { - value := ExecutionDevice{ID: uuid.UUID(id.Bytes).String(), Name: name} +func executionDevice(id pgtype.UUID, name string, environmentID pgtype.UUID) sessions.ExecutionDevice { + value := sessions.ExecutionDevice{ID: uuid.UUID(id.Bytes).String(), Name: name} if environmentID.Valid { value.EnvironmentID = uuid.UUID(environmentID.Bytes).String() } diff --git a/services/core/internal/store/devices_test.go b/services/core/internal/store/devices_test.go index 45df558bf..c30960cc3 100644 --- a/services/core/internal/store/devices_test.go +++ b/services/core/internal/store/devices_test.go @@ -19,9 +19,10 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -func registerTestDevice(t *testing.T, s *Store, tenant string) (ExecutionDevice, string) { +func registerTestDevice(t *testing.T, s *Store, tenant string) (sessions.ExecutionDevice, string) { t.Helper() secret := uuid.NewString() + uuid.NewString() d, err := s.CreateDevice(context.Background(), tenant, "isolated executor", runtimedevice.HashCredential(secret)) @@ -41,7 +42,7 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { b, _ := registerTestDevice(t, s, tenant) foreign, _ := registerTestDevice(t, s, otherTenant) for _, args := range [][3]string{{tenant, session.ID, foreign.ID}, {otherTenant, session.ID, foreign.ID}, {tenant, otherSession.ID, a.ID}} { - if err := s.BindSessionDevice(ctx, args[0], args[1], args[2]); !errors.Is(err, ErrNotFound) { + if err := s.BindSessionDevice(ctx, args[0], args[1], args[2]); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign binding: %v", err) } } @@ -65,7 +66,7 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { switch { case err == nil: success++ - case errors.Is(err, ErrDeviceBindingConflict): + case errors.Is(err, sessions.ErrDeviceBindingConflict): conflicts++ default: t.Fatal(err) @@ -78,7 +79,7 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := s.GetSessionDevice(ctx, otherTenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionDevice(ctx, otherTenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign lookup: %v", err) } pool.Close() @@ -87,7 +88,7 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { if err != nil || got != winner { t.Fatalf("binding after restart: %+v %v", got, err) } - if err := restarted.RevokeDevice(ctx, otherTenant, winner.ID); !errors.Is(err, ErrNotFound) { + if err := restarted.RevokeDevice(ctx, otherTenant, winner.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign revocation: %v", err) } for range 2 { @@ -95,7 +96,7 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { t.Fatal(err) } } - if _, err := restarted.GetSessionDevice(ctx, tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := restarted.GetSessionDevice(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("revoked device remains dispatchable: %v", err) } } diff --git a/services/core/internal/store/dispatch_test.go b/services/core/internal/store/dispatch_test.go index a1ad37777..5596580c1 100644 --- a/services/core/internal/store/dispatch_test.go +++ b/services/core/internal/store/dispatch_test.go @@ -34,8 +34,8 @@ type dispatchHarness struct { d *execution.Dispatcher lease execution.Ownership // held by tests that run execution operations without a Worker tenant string - session store.Session - device store.ExecutionDevice + session sessions.Session + device sessions.ExecutionDevice conn *websocket.Conn registry *runtimegateway.Registry url string @@ -54,7 +54,7 @@ func newDispatchHarnessForSession(t *testing.T, configuration []byte, local bool h := &dispatchHarness{t: t, s: s, db: db, tenant: uuid.NewString(), environments: map[string]*dispatchHarness{}} ctx := context.Background() var err error - h.session, err = s.CreateSession(ctx, h.tenant, store.WithFixtureModelProvider(store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "session", Configuration: configuration})) + h.session, err = s.CreateSession(ctx, h.tenant, store.WithFixtureModelProvider(sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "session", Configuration: configuration})) if err != nil { t.Fatal(err) } @@ -118,7 +118,7 @@ func newDispatchHarnessForSession(t *testing.T, configuration []byte, local bool return h } -func (h *dispatchHarness) message(key, text string) store.InputReceipt { +func (h *dispatchHarness) message(key, text string) sessions.InputReceipt { h.t.Helper() body, _ := json.Marshal(map[string]string{"text": text}) r, err := h.s.SubmitMessage(context.Background(), h.tenant, h.session.ID, key, body) @@ -202,10 +202,10 @@ func TestExecutionDispatchSteeringAndNativeContinuity(t *testing.T) { if inputTextForTest(t, prompt.Input) != "Initial input" || prompt.ConversationID != h.session.ID || prompt.AgentOptions["model"] != "test-model" || prompt.AgentOptions["system_prompt"] != "Keep this instruction." { t.Fatalf("wrong resolved request: %+v", prompt) } - if _, err := h.d.Run(ctx, uuid.NewString(), h.session.ID, first.TurnID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.d.Run(ctx, uuid.NewString(), h.session.ID, first.TurnID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign execution: %v", err) } - if _, err := h.d.Run(ctx, h.tenant, h.session.ID, first.TurnID); !errors.Is(err, store.ErrTurnConflict) { + if _, err := h.d.Run(ctx, h.tenant, h.session.ID, first.TurnID); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("duplicate execution: %v", err) } second := h.message("second", "Follow-up input") @@ -331,7 +331,7 @@ func TestExecutionFailureDoesNotBecomeSuccessOrReplay(t *testing.T) { } } if kind != "disconnect" { - if _, err := h.d.Run(context.Background(), h.tenant, h.session.ID, first.TurnID); !errors.Is(err, store.ErrTurnConflict) { + if _, err := h.d.Run(context.Background(), h.tenant, h.session.ID, first.TurnID); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("terminal replay: %v", err) } } @@ -343,12 +343,12 @@ func TestExecutionOutcomeAndNativeBindingCommitTogether(t *testing.T) { h := newDispatchHarness(t) first := h.message("first", "Run") ctx := context.Background() - _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, first.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, first.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } late := h.message("second", "Late") - if _, err := h.s.CompleteExecution(ctx, h.tenant, h.session.ID, first.TurnID, sessions.TurnCompleted, []byte(`{}`), "native-one", first.Sequence); !errors.Is(err, store.ErrUnappliedInputs) { + if _, err := h.s.CompleteExecution(ctx, h.tenant, h.session.ID, first.TurnID, sessions.TurnCompleted, []byte(`{}`), "native-one", first.Sequence); !errors.Is(err, sessions.ErrUnappliedInputs) { t.Fatalf("unapplied completion: %v", err) } bound, _ := h.s.GetSessionExecutionBinding(ctx, h.tenant, h.session.ID) @@ -371,7 +371,7 @@ func TestExecutionOutcomeAndNativeBindingCommitTogether(t *testing.T) { for err := range errs { if err == nil { success++ - } else if !errors.Is(err, store.ErrTurnConflict) { + } else if !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } } diff --git a/services/core/internal/store/environment_admission_test.go b/services/core/internal/store/environment_admission_test.go index 1161b7711..5aae0496f 100644 --- a/services/core/internal/store/environment_admission_test.go +++ b/services/core/internal/store/environment_admission_test.go @@ -16,7 +16,7 @@ import ( ) type environmentAdmissionResult struct { - receipts []store.InputReceipt + receipts []sessions.InputReceipt err error } @@ -30,7 +30,7 @@ func newEnvironmentAdmission(t *testing.T) (*dispatchHarness, *execution.Worker) cancel() _ = worker.Run(ctx) }) - session, err := worker.CreateSession(t.Context(), h.tenant, store.WithFixtureModelProvider(store.CreateSessionInput{ + session, err := worker.CreateSession(t.Context(), h.tenant, store.WithFixtureModelProvider(sessions.CreateSession{ Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), })) @@ -51,8 +51,8 @@ func submitEnvironmentAdmission(ctx context.Context, h *dispatchHarness, worker return result } -func environmentAdmissionInputs() []store.Input { - return []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}, {Kind: "message", Payload: json.RawMessage(`{"text":"second"}`)}} +func environmentAdmissionInputs() []sessions.Input { + return []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}, {Kind: "message", Payload: json.RawMessage(`{"text":"second"}`)}} } func awaitEnvironmentAdmission(t *testing.T, result <-chan environmentAdmissionResult) environmentAdmissionResult { @@ -66,7 +66,7 @@ func awaitEnvironmentAdmission(t *testing.T, result <-chan environmentAdmissionR } } -func environmentAdmissionPending(t *testing.T, h *dispatchHarness, key string) store.EnvironmentInputReservation { +func environmentAdmissionPending(t *testing.T, h *dispatchHarness, key string) sessions.EnvironmentInputReservation { t.Helper() _, pool := store.NewTestStore(t) var id string @@ -97,17 +97,17 @@ func TestEnvironmentAdmissionWaitsForPreparedClaimAndRetainsRetry(t *testing.T) t.Fatal("waiting activity", session, err) } inputs := environmentAdmissionInputs() - if _, err = worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "other", inputs); !errors.Is(err, store.ErrTurnConflict) { + if _, err = worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "other", inputs); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("competing batch", err) } - if _, err = worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "wait", inputs[:1]); !errors.Is(err, store.ErrIdempotencyConflict) { + if _, err = worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "wait", inputs[:1]); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed retry", err) } - if _, err = worker.SubmitInputs(t.Context(), uuid.NewString(), h.session.ID, "wait", inputs); !errors.Is(err, store.ErrNotFound) { + if _, err = worker.SubmitInputs(t.Context(), uuid.NewString(), h.session.ID, "wait", inputs); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign tenant", err) } - mixed := append(inputs, store.Input{Kind: "cancel", Payload: json.RawMessage(`{}`)}) - if _, err = worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "mixed", mixed); !errors.Is(err, store.ErrInvalidInput) { + mixed := append(inputs, sessions.Input{Kind: "cancel", Payload: json.RawMessage(`{}`)}) + if _, err = worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "mixed", mixed); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("mixed batch accepted", err) } ctx, cancel := context.WithCancel(t.Context()) @@ -185,8 +185,8 @@ func TestEnvironmentAdmissionSettlementDoesNotCreateTurn(t *testing.T) { t.Fatal(err) } case "deleted": - expected = store.ErrNotFound - if err := h.s.DeleteSession(t.Context(), h.tenant, h.session.ID); !errors.Is(err, store.ErrSessionNotIdle) { + expected = sessions.ErrNotFound + if err := h.s.DeleteSession(t.Context(), h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("pending input deleted", err) } if err := h.s.CommitLegacyDeletion(t.Context(), h.tenant, h.session.ID); err != nil { @@ -225,7 +225,7 @@ func TestEnvironmentAdmissionSettlementDoesNotCreateTurn(t *testing.T) { t.Fatal(err) } if name == "disconnected" || name == "ownership_lost" { - if retained.State != store.EnvironmentInputPending || !retained.Deadline.Equal(pending.Deadline) { + if retained.State != sessions.EnvironmentInputPending || !retained.Deadline.Equal(pending.Deadline) { t.Fatal("observer changed durable outcome", retained) } } else { diff --git a/services/core/internal/store/environment_claim_worker_test.go b/services/core/internal/store/environment_claim_worker_test.go index dbde88971..c4dd8249c 100644 --- a/services/core/internal/store/environment_claim_worker_test.go +++ b/services/core/internal/store/environment_claim_worker_test.go @@ -10,7 +10,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestWorkerReconcilesEnvironmentPromotionBeforeStart(t *testing.T) { @@ -26,7 +25,7 @@ func TestWorkerReconcilesEnvironmentPromotionBeforeStart(t *testing.T) { } turnID := got.Receipts[0].TurnID if deleted { - if err := s.DeleteSession(t.Context(), tenant, pending.SessionID); !errors.Is(err, store.ErrSessionNotIdle) { + if err := s.DeleteSession(t.Context(), tenant, pending.SessionID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("claimed Session deleted", err) } if err := s.CommitLegacyDeletion(t.Context(), tenant, pending.SessionID); err != nil { @@ -69,7 +68,7 @@ func TestWorkerReconcilesEnvironmentPromotionBeforeStart(t *testing.T) { successor := executionOwner(t, db, s).Store retry, err := successor.PromoteEnvironmentInput(t.Context(), tenant, pending.SessionID, pending.ID) if deleted { - if !errors.Is(err, store.ErrNotFound) { + if !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted reservation was exposed", err) } } else if err != nil || len(retry.Receipts) != 1 || !retry.Receipts[0].Replayed || retry.Receipts[0].Sequence != got.Receipts[0].Sequence || retry.Receipts[0].TurnID != turnID { diff --git a/services/core/internal/store/environment_connection_recovery.go b/services/core/internal/store/environment_connection_recovery.go index c32fa6467..21f90dc98 100644 --- a/services/core/internal/store/environment_connection_recovery.go +++ b/services/core/internal/store/environment_connection_recovery.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -40,7 +41,7 @@ func (s *Store) ReconcileEnvironmentConnections(ctx context.Context) error { } return nil }) - if err != nil && !errors.Is(err, ErrNotFound) { + if err != nil && !errors.Is(err, sessions.ErrNotFound) { return err } after = row.ID diff --git a/services/core/internal/store/environment_connection_recovery_test.go b/services/core/internal/store/environment_connection_recovery_test.go index d6caab3f8..f5ead182d 100644 --- a/services/core/internal/store/environment_connection_recovery_test.go +++ b/services/core/internal/store/environment_connection_recovery_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestEnvironmentConnectionRecoveryFencesLostOwnerAcrossPages(t *testing.T) { @@ -11,8 +13,8 @@ func TestEnvironmentConnectionRecoveryFencesLostOwnerAcrossPages(t *testing.T) { old := executionWriter(t, s) type target struct { tenant string - session Session - environment Environment + session sessions.Session + environment sessions.Environment generation string } var targets []target diff --git a/services/core/internal/store/environment_connection_worker_test.go b/services/core/internal/store/environment_connection_worker_test.go index df47a830e..4313ba793 100644 --- a/services/core/internal/store/environment_connection_worker_test.go +++ b/services/core/internal/store/environment_connection_worker_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -16,7 +17,7 @@ import ( func TestEnvironmentConnectionWorkerReconcilesAndReleasesLease(t *testing.T) { s, db := newTestStoreDB(t) tenant := uuid.NewString() - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "connection-worker", Configuration: []byte(`{"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "connection-worker", Configuration: []byte(`{"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/environment_connections.go b/services/core/internal/store/environment_connections.go index c22933b9a..941b53085 100644 --- a/services/core/internal/store/environment_connections.go +++ b/services/core/internal/store/environment_connections.go @@ -24,7 +24,7 @@ func (s *Store) ReplaceEnvironmentConnection(ctx context.Context, tenant, enviro } return s.withEnvironmentConnection(ctx, tenant, environment, func(ctx context.Context, q *sqlc.Queries, row sqlc.GetSessionEnvironmentRow) error { if row.Environment.Status == "failed" || row.Environment.Status == "expired" { - return ErrInvalidInput + return sessions.ErrInvalidInput } old, err := q.GetEnvironmentConnection(ctx, row.Environment.ID) if err == nil && old.Generation == gen { @@ -48,7 +48,7 @@ func (s *Store) ReplaceEnvironmentConnection(ctx context.Context, tenant, enviro func (s *Store) ObserveEnvironmentConnection(ctx context.Context, tenant, environment, generation string, revision int64, connected bool) error { gen, err := parseConnectionGeneration(generation) if err != nil || revision <= 0 { - return ErrInvalidInput + return sessions.ErrInvalidInput } return s.withEnvironmentConnection(ctx, tenant, environment, func(ctx context.Context, q *sqlc.Queries, row sqlc.GetSessionEnvironmentRow) error { current, err := q.GetEnvironmentConnection(ctx, row.Environment.ID) @@ -62,7 +62,7 @@ func (s *Store) ObserveEnvironmentConnection(ctx context.Context, tenant, enviro return nil } if row.Environment.Status == "failed" || row.Environment.Status == "expired" { - return ErrInvalidInput + return sessions.ErrInvalidInput } if err := q.AdvanceEnvironmentConnection(ctx, sqlc.AdvanceEnvironmentConnectionParams{EnvironmentID: row.Environment.ID, Revision: revision}); err != nil { return err @@ -95,7 +95,7 @@ func (s *Store) withEnvironmentConnection(ctx context.Context, tenant, environme return s.withPublicSession(ctx, tenant, owned.SessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { row, err := q.GetSessionEnvironment(ctx, sqlc.GetSessionEnvironmentParams{TenantID: tenantID, ID: session}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -109,7 +109,7 @@ func recordEnvironmentConnection(ctx context.Context, q *sqlc.Queries, row sqlc. return err } if status != "connected" && status != "disconnected" { - return ErrInvalidInput + return sessions.ErrInvalidInput } if err := q.SetEnvironmentConnectionStatus(ctx, sqlc.SetEnvironmentConnectionStatusParams{ID: row.Environment.ID, Status: status}); err != nil { return err @@ -148,7 +148,7 @@ func storedEnvironmentType(row sqlc.GetSessionEnvironmentRow) (string, error) { func parseConnectionGeneration(value string) (pgtype.UUID, error) { id, err := parseID(value) if err != nil || id.Bytes == [16]byte{} { - return pgtype.UUID{}, ErrInvalidInput + return pgtype.UUID{}, sessions.ErrInvalidInput } return id, nil } diff --git a/services/core/internal/store/environment_connections_test.go b/services/core/internal/store/environment_connections_test.go index b5d448477..369b740e9 100644 --- a/services/core/internal/store/environment_connections_test.go +++ b/services/core/internal/store/environment_connections_test.go @@ -14,7 +14,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -func connectionFixture(t *testing.T, s *Store) (string, Session, Environment) { +func connectionFixture(t *testing.T, s *Store) (string, sessions.Session, sessions.Environment) { t.Helper() tenant, session := environmentInputSession(t, s) environment, err := s.GetSessionEnvironment(t.Context(), tenant, session.ID) @@ -140,10 +140,10 @@ func TestEnvironmentConnectionRequiresOwnerAndRollsBackWithEvent(t *testing.T) { if err := writer.ReplaceEnvironmentConnection(t.Context(), tenant, environment.ID, generation); err != nil { t.Fatal(err) } - if err := writer.ReplaceEnvironmentConnection(t.Context(), uuid.NewString(), environment.ID, generation); !errors.Is(err, ErrNotFound) { + if err := writer.ReplaceEnvironmentConnection(t.Context(), uuid.NewString(), environment.ID, generation); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign generation accepted", err) } - if err := writer.ObserveEnvironmentConnection(t.Context(), tenant, session.ID, generation, 1, true); !errors.Is(err, ErrNotFound) { + if err := writer.ObserveEnvironmentConnection(t.Context(), tenant, session.ID, generation, 1, true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("Session ID used as Environment", err) } before := connectionSnapshot(t, pool, environment.ID) diff --git a/services/core/internal/store/environment_device_test.go b/services/core/internal/store/environment_device_test.go index 9ffa02aa0..9a7a116ee 100644 --- a/services/core/internal/store/environment_device_test.go +++ b/services/core/internal/store/environment_device_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -33,7 +34,7 @@ func TestWorkerEnvironmentSelectsCapableDeviceWithoutMovingBinding(t *testing.T) t.Fatal("incapable enrolled device received work", frame.Type) case <-time.After(time.Second): } - if _, err := h.s.GetSessionDevice(t.Context(), h.tenant, pending.SessionID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSessionDevice(t.Context(), h.tenant, pending.SessionID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unregistered Runtime was assigned general compute", err) } session, err := h.s.GetSession(t.Context(), h.tenant, pending.SessionID) @@ -59,9 +60,9 @@ func TestWorkerEnvironmentSelectsCapableDeviceWithoutMovingBinding(t *testing.T) t.Fatal("preparation owner was not released") } stop() - for _, value := range []store.EnvironmentInputReservation{pending, bound} { + for _, value := range []sessions.EnvironmentInputReservation{pending, bound} { stored, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, value.SessionID, value.ID) - if err != nil || stored.State != store.EnvironmentInputPending || !stored.Deadline.Equal(value.Deadline) { + if err != nil || stored.State != sessions.EnvironmentInputPending || !stored.Deadline.Equal(value.Deadline) { t.Fatal("device readiness changed pending input", err) } assertEnvironmentExpiryHasNoHistory(t, pool, value.SessionID) diff --git a/services/core/internal/store/environment_directory_active_test.go b/services/core/internal/store/environment_directory_active_test.go index fbfbf2100..161e59146 100644 --- a/services/core/internal/store/environment_directory_active_test.go +++ b/services/core/internal/store/environment_directory_active_test.go @@ -5,13 +5,12 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestEnvironmentDirectoryActiveRunUsesExistingOwner(t *testing.T) { h, w, environment := directoryWorker(t) awaitFixtureCapabilities(t, h, workerEnvironmentCapabilities()) - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "execute", []store.Input{{Kind: "message", Payload: []byte(`{"text":"work"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "execute", []sessions.Input{{Kind: "message", Payload: []byte(`{"text":"work"}`)}}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/environment_directory_test.go b/services/core/internal/store/environment_directory_test.go index fa49fc0b4..537b5be6e 100644 --- a/services/core/internal/store/environment_directory_test.go +++ b/services/core/internal/store/environment_directory_test.go @@ -9,7 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -18,7 +18,7 @@ type directoryResult struct { err error } -func directoryWorker(t *testing.T) (*dispatchHarness, *execution.Worker, store.Environment) { +func directoryWorker(t *testing.T) (*dispatchHarness, *execution.Worker, sessions.Environment) { t.Helper() h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"unavailable-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), true) environment, err := h.s.GetSessionEnvironment(t.Context(), h.tenant, h.session.ID) @@ -49,7 +49,7 @@ func directoryWorker(t *testing.T) (*dispatchHarness, *execution.Worker, store.E return h, w, environment } -func startDirectoryRead(ctx context.Context, w *execution.Worker, environment store.Environment) <-chan directoryResult { +func startDirectoryRead(ctx context.Context, w *execution.Worker, environment sessions.Environment) <-chan directoryResult { ch := make(chan directoryResult, 1) go func() { value, err := w.ReadEnvironmentDirectory(ctx, environment, "reports") @@ -69,7 +69,7 @@ func awaitDirectoryResult(t *testing.T, ch <-chan directoryResult) directoryResu } } -func prepareDirectoryRead(t *testing.T, h *dispatchHarness, environment store.Environment) (string, string) { +func prepareDirectoryRead(t *testing.T, h *dispatchHarness, environment sessions.Environment) (string, string) { t.Helper() frame := h.read(proto.TypeExecutionPrepare) var request proto.ExecutionPreparePayload @@ -106,12 +106,12 @@ func TestEnvironmentDirectoryWorkerReadsWithoutExecutionPrerequisites(t *testing h, w, environment := directoryWorker(t) foreign := environment foreign.TenantID = uuid.NewString() - if _, err := w.ReadEnvironmentDirectory(t.Context(), foreign, "reports"); !errors.Is(err, store.ErrNotFound) { + if _, err := w.ReadEnvironmentDirectory(t.Context(), foreign, "reports"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign reader admitted", err) } wrong := environment wrong.SessionID = uuid.NewString() - if _, err := w.ReadEnvironmentDirectory(t.Context(), wrong, "reports"); !errors.Is(err, store.ErrNotFound) { + if _, err := w.ReadEnvironmentDirectory(t.Context(), wrong, "reports"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("wrong Session admitted", err) } result := startDirectoryRead(t.Context(), w, environment) @@ -179,7 +179,7 @@ func TestEnvironmentDirectoryNotDirectoryIsAnEmptyListing(t *testing.T) { }{ {proto.WorkspaceReadNotDirectory, false, nil}, {proto.WorkspaceReadNotDirectory, true, execution.ErrExecutionUnavailable}, - {"not_found", false, store.ErrNotFound}, + {"not_found", false, sessions.ErrNotFound}, {"invalid_request", false, execution.ErrExecutionUnavailable}, {"permission_denied", false, execution.ErrExecutionUnavailable}, {"resource_unavailable", false, execution.ErrExecutionUnavailable}, @@ -188,7 +188,7 @@ func TestEnvironmentDirectoryNotDirectoryIsAnEmptyListing(t *testing.T) { h, w, environment := directoryWorker(t) foreign := environment foreign.TenantID = uuid.NewString() - if _, err := w.ReadEnvironmentDirectory(t.Context(), foreign, "reports"); !errors.Is(err, store.ErrNotFound) { + if _, err := w.ReadEnvironmentDirectory(t.Context(), foreign, "reports"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign reader admitted", err) } result := startDirectoryRead(t.Context(), w, environment) diff --git a/services/core/internal/store/environment_executor_command_test.go b/services/core/internal/store/environment_executor_command_test.go index 1669ad1bf..98b0228cf 100644 --- a/services/core/internal/store/environment_executor_command_test.go +++ b/services/core/internal/store/environment_executor_command_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -48,7 +49,7 @@ func TestEnvironmentExecutorOperatorCommand(t *testing.T) { } command(uuid.NewString(), false) first := command(tenant, true) - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "operator-key", Configuration: json.RawMessage(`{"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "operator-key", Configuration: json.RawMessage(`{"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) if err != nil { t.Fatal(err) } @@ -64,14 +65,14 @@ func TestEnvironmentExecutorOperatorCommand(t *testing.T) { if next == first { t.Fatal("rotation returned the same key") } - if _, err := s.AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(first)); !errors.Is(err, store.ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(first)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("old command credential retained authority", err) } if owner, err := s.AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(next)); err != nil || owner != tenant { t.Fatal("rotated command credential failed", err) } command(tenant, true, "--revoke") - if _, err := s.AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(next)); !errors.Is(err, store.ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(next)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("revoked command credential retained authority", err) } t.Log("built operator command issued before Session creation, rejected duplicate/foreign requests, rotated and revoked durable credentials") diff --git a/services/core/internal/store/environment_executor_credentials.go b/services/core/internal/store/environment_executor_credentials.go index 3ef8f74c8..890d2a045 100644 --- a/services/core/internal/store/environment_executor_credentials.go +++ b/services/core/internal/store/environment_executor_credentials.go @@ -9,50 +9,43 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -var ErrExecutorCredentialExists = errors.New("executor key ID already exists; rotate explicitly") - -type IssuedExecutorCredential struct { - KeyID string `json:"key_id"` - EnvironmentID string `json:"environment_id,omitempty"` - Token string `json:"executor_token"` -} - // IssueExecutorCredential returns a new connect-only secret once, without replacing an existing ID. -func (s *Store) IssueExecutorCredential(ctx context.Context, principal identity.Principal, keyID, environment string) (IssuedExecutorCredential, error) { +func (s *Store) IssueExecutorCredential(ctx context.Context, principal identity.Principal, keyID, environment string) (sessions.IssuedExecutorCredential, error) { return s.issueExecutorCredential(ctx, principal, keyID, environment, nil) } // issueExecutorCredential runs before, when given, first in the issuing // transaction; its error aborts the issuance. -func (s *Store) issueExecutorCredential(ctx context.Context, principal identity.Principal, keyID, environment string, before func(context.Context, *sqlc.Queries) error) (IssuedExecutorCredential, error) { +func (s *Store) issueExecutorCredential(ctx context.Context, principal identity.Principal, keyID, environment string, before func(context.Context, *sqlc.Queries) error) (sessions.IssuedExecutorCredential, error) { tenant, id, err := executorCredentialIdentity(principal, keyID) if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } exists, err := s.queries.ExecutorProjectScopeExists(ctx, sqlc.ExecutorProjectScopeExistsParams{TenantID: tenant, OrganizationID: principal.OrganizationID, ProjectID: principal.ProjectID}) if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } if !exists { - return IssuedExecutorCredential{}, ErrNotFound + return sessions.IssuedExecutorCredential{}, sessions.ErrNotFound } var restriction pgtype.UUID if environment != "" { restriction, err = parseID(environment) if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } } token, digest, err := newExecutorSecret() if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } - var result IssuedExecutorCredential + var result sessions.IssuedExecutorCredential err = s.withExecutorCredentialTarget(ctx, principal, restriction, func(ctx context.Context, q *sqlc.Queries) error { if before != nil { if err := before(ctx, q); err != nil { @@ -64,7 +57,7 @@ func (s *Store) issueExecutorCredential(ctx context.Context, principal identity. OrganizationID: principal.OrganizationID, ProjectID: principal.ProjectID, EnvironmentID: restriction, TokenSha256: digest, }) if errors.Is(err, pgx.ErrNoRows) { - return ErrExecutorCredentialExists + return sessions.ErrExecutorCredentialExists } if err != nil { return err @@ -73,31 +66,31 @@ func (s *Store) issueExecutorCredential(ctx context.Context, principal identity. return nil }) if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } return result, nil } -func (s *Store) RotateExecutorCredential(ctx context.Context, principal identity.Principal, keyID string) (IssuedExecutorCredential, error) { +func (s *Store) RotateExecutorCredential(ctx context.Context, principal identity.Principal, keyID string) (sessions.IssuedExecutorCredential, error) { return s.rotateExecutorCredential(ctx, principal, keyID, nil) } // rotateExecutorCredential runs before, when given, first in the rotating // transaction; its error aborts the rotation. -func (s *Store) rotateExecutorCredential(ctx context.Context, principal identity.Principal, keyID string, before func(context.Context, *sqlc.Queries) error) (IssuedExecutorCredential, error) { +func (s *Store) rotateExecutorCredential(ctx context.Context, principal identity.Principal, keyID string, before func(context.Context, *sqlc.Queries) error) (sessions.IssuedExecutorCredential, error) { tenant, id, err := executorCredentialIdentity(principal, keyID) if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } restriction, err := s.executorCredentialRestriction(ctx, principal, tenant, id) if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } token, digest, err := newExecutorSecret() if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } - var result IssuedExecutorCredential + var result sessions.IssuedExecutorCredential err = s.withExecutorCredentialTarget(ctx, principal, restriction, func(ctx context.Context, q *sqlc.Queries) error { if before != nil { if err := before(ctx, q); err != nil { @@ -106,7 +99,7 @@ func (s *Store) rotateExecutorCredential(ctx context.Context, principal identity } row, err := q.RotateExecutorCredential(ctx, sqlc.RotateExecutorCredentialParams{KeyID: id, TenantID: tenant, SubjectKind: pgtype.Text{String: principal.SubjectKind, Valid: true}, SubjectID: pgtype.Text{String: principal.SubjectID, Valid: true}, TokenSha256: digest}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -115,7 +108,7 @@ func (s *Store) rotateExecutorCredential(ctx context.Context, principal identity return nil }) if err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } return result, nil } @@ -130,7 +123,7 @@ func (s *Store) RevokeExecutorCredential(ctx context.Context, principal identity } n, err := s.queries.RevokeExecutorCredential(ctx, sqlc.RevokeExecutorCredentialParams{KeyID: id, TenantID: tenant, SubjectKind: pgtype.Text{String: principal.SubjectKind, Valid: true}, SubjectID: pgtype.Text{String: principal.SubjectID, Valid: true}}) if err == nil && n == 0 { - return ErrNotFound + return sessions.ErrNotFound } return err } @@ -141,7 +134,7 @@ func (s *Store) executorCredentialRestriction(ctx context.Context, principal ide OrganizationID: principal.OrganizationID, ProjectID: principal.ProjectID, }) if errors.Is(err, pgx.ErrNoRows) { - return pgtype.UUID{}, ErrNotFound + return pgtype.UUID{}, sessions.ErrNotFound } return restriction, err } @@ -150,15 +143,15 @@ func (s *Store) executorCredentialRestriction(ctx context.Context, principal ide func (s *Store) AuthenticateEnvironmentExecutor(ctx context.Context, environment, digest string) (string, error) { id, err := parseID(environment) if err != nil { - return "", ErrNotFound + return "", sessions.ErrNotFound } hash, err := hex.DecodeString(digest) if err != nil || len(hash) != sha256.Size { - return "", ErrNotFound + return "", sessions.ErrNotFound } tenant, err := s.queries.AuthenticateEnvironmentExecutor(ctx, sqlc.AuthenticateEnvironmentExecutorParams{EnvironmentID: id, TokenSha256: hex.EncodeToString(hash)}) if errors.Is(err, pgx.ErrNoRows) { - return "", ErrNotFound + return "", sessions.ErrNotFound } if err != nil { return "", fmt.Errorf("authenticate environment executor: %w", err) diff --git a/services/core/internal/store/environment_executor_credentials_test.go b/services/core/internal/store/environment_executor_credentials_test.go index 19f61aa12..6f9563354 100644 --- a/services/core/internal/store/environment_executor_credentials_test.go +++ b/services/core/internal/store/environment_executor_credentials_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func executorDigest(token string) string { @@ -28,10 +30,10 @@ func TestEnvironmentExecutorCredentialLifecycle(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := s.IssueExecutorCredential(ctx, FixtureExecutorPrincipal(t, s, foreign), environment.ID, environment.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueExecutorCredential(ctx, FixtureExecutorPrincipal(t, s, foreign), environment.ID, environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign issue", err) } - if _, err := s.RotateExecutorCredential(ctx, principal, environment.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.RotateExecutorCredential(ctx, principal, environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("rotate manufactured a credential", err) } issued, err := s.IssueExecutorCredential(ctx, principal, environment.ID, environment.ID) @@ -46,22 +48,22 @@ func TestEnvironmentExecutorCredentialLifecycle(t *testing.T) { if err != nil || owner != tenant { t.Fatal("credential not accepted for owner", err) } - } else if !errors.Is(err, ErrNotFound) { + } else if !errors.Is(err, sessions.ErrNotFound) { t.Fatal("invalid credential accepted", err) } } check(s, token, true) check(s, "caller/device/harness/grant", false) - if _, err := s.AuthenticateEnvironmentExecutor(ctx, uuid.NewString(), executorDigest(token)); !errors.Is(err, ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(ctx, uuid.NewString(), executorDigest(token)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign Environment accepted", err) } - if _, err := s.IssueExecutorCredential(ctx, principal, environment.ID, environment.ID); !errors.Is(err, ErrExecutorCredentialExists) { + if _, err := s.IssueExecutorCredential(ctx, principal, environment.ID, environment.ID); !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal("issue silently replaced credential", err) } - if err := s.RevokeExecutorCredential(ctx, FixtureExecutorPrincipal(t, s, foreign), environment.ID); !errors.Is(err, ErrNotFound) { + if err := s.RevokeExecutorCredential(ctx, FixtureExecutorPrincipal(t, s, foreign), environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign revoke", err) } - if _, err := s.RotateExecutorCredential(ctx, FixtureExecutorPrincipal(t, s, foreign), environment.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.RotateExecutorCredential(ctx, FixtureExecutorPrincipal(t, s, foreign), environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign rotate", err) } var stored string @@ -86,7 +88,7 @@ func TestEnvironmentExecutorCredentialLifecycle(t *testing.T) { } } check(restarted, next.Token, false) - if _, err := s.IssueExecutorCredential(ctx, principal, environment.ID, environment.ID); !errors.Is(err, ErrExecutorCredentialExists) { + if _, err := s.IssueExecutorCredential(ctx, principal, environment.ID, environment.ID); !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal("ordinary issue resurrected revoked authority", err) } restored, err := s.RotateExecutorCredential(ctx, principal, environment.ID) @@ -99,7 +101,7 @@ func TestEnvironmentExecutorCredentialLifecycle(t *testing.T) { t.Fatal(err) } check(restarted, restored.Token, false) - if _, err := s.RotateExecutorCredential(ctx, principal, environment.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.RotateExecutorCredential(ctx, principal, environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted Session authority resurrected", err) } if err := pool.QueryRow(ctx, "SELECT token_sha256 FROM environment_executor_credentials WHERE environment_id=$1", environment.ID).Scan(&stored); err != nil || stored != executorDigest(restored.Token) { @@ -137,7 +139,7 @@ func TestEnvironmentExecutorConcurrentIssueAndDeletion(t *testing.T) { token, err := st.IssueExecutorCredential(ctx, principal, environment.ID, environment.ID) if err == nil { tokens <- token.Token - } else if !errors.Is(err, ErrExecutorCredentialExists) { + } else if !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Error(err) } }() @@ -155,7 +157,7 @@ func TestEnvironmentExecutorConcurrentIssueAndDeletion(t *testing.T) { token, err := other.RotateExecutorCredential(ctx, principal, environment.ID) if err == nil { rotated <- token.Token - } else if !errors.Is(err, ErrNotFound) { + } else if !errors.Is(err, sessions.ErrNotFound) { t.Error(err) } }() @@ -165,11 +167,11 @@ func TestEnvironmentExecutorConcurrentIssueAndDeletion(t *testing.T) { wg.Wait() close(rotated) for token := range rotated { - if _, err := s.AuthenticateEnvironmentExecutor(ctx, environment.ID, executorDigest(token)); !errors.Is(err, ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(ctx, environment.ID, executorDigest(token)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("racing rotation authorized deleted Environment", err) } } - if _, err := s.AuthenticateEnvironmentExecutor(ctx, environment.ID, executorDigest(original)); !errors.Is(err, ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(ctx, environment.ID, executorDigest(original)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("original key survived deletion", err) } } diff --git a/services/core/internal/store/environment_executor_management.go b/services/core/internal/store/environment_executor_management.go index 2fa7b1c5b..6a3be82a9 100644 --- a/services/core/internal/store/environment_executor_management.go +++ b/services/core/internal/store/environment_executor_management.go @@ -4,70 +4,45 @@ import ( "context" "encoding/json" "errors" - "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -// ExecutorCredential is the metadata of one Environment executor credential. -// Its secret is returned only when issued or rotated. -type ExecutorCredential struct { - KeyID string `json:"key_id" format:"uuid"` - CreatedAt time.Time `json:"created_at"` - RevokedAt *time.Time `json:"revoked_at" extensions:"x-nullable"` -} - // The Project* methods serve Core-key executor credential management. project // is the Project's own principal, which becomes the credential's execution // principal; the target must be a self_hosted Environment of that Project whose // Session is not deleted, and only credentials restricted to it are managed. -// ExecutorConnectionState is an internal durable observation, never a wire payload. -// In particular the current credential digest must not be serialized. -type ExecutorConnectionState struct { - DeviceID string `json:"-"` - BoundKeyID *string `json:"-"` - EnrolledAt *time.Time `json:"-"` - LastSeenAt *time.Time `json:"-"` - CredentialHash string `json:"-"` - EnvironmentStatus string `json:"-"` -} - -type ExecutorCredentialState struct { - EnvironmentID string `json:"-"` - Credentials []ExecutorCredential - Connection ExecutorConnectionState -} - -func (s *Store) ListProjectExecutorCredentials(ctx context.Context, project identity.Principal, environment string) ([]ExecutorCredential, error) { +func (s *Store) ListProjectExecutorCredentials(ctx context.Context, project identity.Principal, environment string) ([]sessions.ExecutorCredential, error) { state, err := s.ProjectExecutorCredentialState(ctx, project, environment) return state.Credentials, err } // ProjectExecutorCredentialState reads list metadata and binding facts in one // read-only snapshot. The snapshot ends before any live peer/authority observation. -func (s *Store) ProjectExecutorCredentialState(ctx context.Context, project identity.Principal, environment string) (ExecutorCredentialState, error) { +func (s *Store) ProjectExecutorCredentialState(ctx context.Context, project identity.Principal, environment string) (sessions.ExecutorCredentialState, error) { if err := project.Validate(); err != nil { - return ExecutorCredentialState{}, ErrInvalidInput + return sessions.ExecutorCredentialState{}, sessions.ErrInvalidInput } tenant, err := parseID(project.TenantID) if err != nil { - return ExecutorCredentialState{}, err + return sessions.ExecutorCredentialState{}, err } environmentID := pgunit.PathID(environment) - result := ExecutorCredentialState{Credentials: []ExecutorCredential{}} + result := sessions.ExecutorCredentialState{Credentials: []sessions.ExecutorCredential{}} err = s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { q := s.queries.WithTx(tx) row, err := q.GetEnvironmentExecutorConnection(ctx, sqlc.GetEnvironmentExecutorConnectionParams{EnvironmentID: environmentID, TenantID: tenant}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -100,7 +75,7 @@ func (s *Store) ProjectExecutorCredentialState(ctx context.Context, project iden return err } for _, row := range rows { - credential := ExecutorCredential{KeyID: uuid.UUID(row.KeyID.Bytes).String(), CreatedAt: row.CreatedAt.Time} + credential := sessions.ExecutorCredential{KeyID: uuid.UUID(row.KeyID.Bytes).String(), CreatedAt: row.CreatedAt.Time} if row.RevokedAt.Valid { revoked := row.RevokedAt.Time credential.RevokedAt = &revoked @@ -116,19 +91,19 @@ func (s *Store) ProjectExecutorCredentialState(ctx context.Context, project iden // secret of an existing key restricted to the Environment. An archived Project // gets neither (projects.ErrArchived). The administrator audit entry commits in // the same transaction and never contains the secret. -func (s *Store) IssueProjectExecutorCredential(ctx context.Context, project identity.Principal, environment, keyID string, rotate bool) (IssuedExecutorCredential, error) { +func (s *Store) IssueProjectExecutorCredential(ctx context.Context, project identity.Principal, environment, keyID string, rotate bool) (sessions.IssuedExecutorCredential, error) { // The target is checked first, then the archived Project, then the key. if err := s.selfHostedExecutorTarget(ctx, project, environment); err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } if err := activeProject(ctx, s.queries, project); err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } if !rotate { return s.issueExecutorCredential(ctx, project, keyID, environment, activeProjectAudit(project, "issue", keyID)) } if err := s.exactExecutorRestriction(ctx, project, environment, keyID); err != nil { - return IssuedExecutorCredential{}, err + return sessions.IssuedExecutorCredential{}, err } return s.rotateExecutorCredential(ctx, project, keyID, activeProjectAudit(project, "rotate", keyID)) } @@ -154,7 +129,7 @@ func (s *Store) RevokeProjectExecutorCredential(ctx context.Context, project ide return err } if n == 0 { - return ErrNotFound + return sessions.ErrNotFound } return record(ctx, q) }) @@ -187,7 +162,7 @@ func activeProject(ctx context.Context, q *sqlc.Queries, project identity.Princi } row, err := q.LockProjectByTenant(ctx, tenant) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -200,7 +175,7 @@ func activeProject(ctx context.Context, q *sqlc.Queries, project identity.Princi func (s *Store) selfHostedExecutorTarget(ctx context.Context, principal identity.Principal, environment string) error { if err := principal.Validate(); err != nil { - return ErrInvalidInput + return sessions.ErrInvalidInput } owned, err := s.GetEnvironment(ctx, principal.TenantID, environment) if err != nil { @@ -210,7 +185,7 @@ func (s *Store) selfHostedExecutorTarget(ctx context.Context, principal identity Type string `json:"type"` } if json.Unmarshal(owned.Configuration, &configuration) != nil || configuration.Type != "self_hosted" { - return ErrNotFound + return sessions.ErrNotFound } return nil } @@ -228,7 +203,7 @@ func (s *Store) exactExecutorRestriction(ctx context.Context, principal identity // Restrictions and principals are immutable, so checking before the // rotation or revocation transaction cannot authorize a different target. if !actual.Valid || actual != want { - return ErrNotFound + return sessions.ErrNotFound } return nil } diff --git a/services/core/internal/store/environment_executor_management_test.go b/services/core/internal/store/environment_executor_management_test.go index ebbf38ded..df31ee905 100644 --- a/services/core/internal/store/environment_executor_management_test.go +++ b/services/core/internal/store/environment_executor_management_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -21,7 +22,7 @@ func TestProjectEnvironmentExecutorManagement(t *testing.T) { foreignProject := foreign.Project // Each administrator request has its own request ID. admin := func() context.Context { return keyAdminContext(ctx, project.ID) } - create := func(kind string) (Session, Environment) { + create := func(kind string) (sessions.Session, sessions.Environment) { t.Helper() input := environmentInput(uuid.NewString(), kind, "/workspace") input.Creator = p.Subject() @@ -52,7 +53,7 @@ func TestProjectEnvironmentExecutorManagement(t *testing.T) { t.Fatal("issued credential does not authenticate", err) } // A lost issuance response must not cause a new key or replace the old secret. - if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, keyID, false); !errors.Is(err, ErrExecutorCredentialExists) { + if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, keyID, false); !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal("uncertain retry", err) } listed, err := s.ListProjectExecutorCredentials(ctx, p, one.ID) @@ -64,24 +65,24 @@ func TestProjectEnvironmentExecutorManagement(t *testing.T) { } // Another Project, a hosted or unknown Environment and a key restricted elsewhere are not found. - if _, err := s.ListProjectExecutorCredentials(ctx, foreign.Principal, one.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.ListProjectExecutorCredentials(ctx, foreign.Principal, one.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign list", err) } - if _, err := s.IssueProjectExecutorCredential(keyAdminContext(ctx, foreignProject.ID), foreign.Principal, one.ID, uuid.NewString(), false); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueProjectExecutorCredential(keyAdminContext(ctx, foreignProject.ID), foreign.Principal, one.ID, uuid.NewString(), false); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign issue", err) } for _, environment := range []string{two.ID, hosted.ID, uuid.NewString()} { - if _, err := s.IssueProjectExecutorCredential(admin(), p, environment, keyID, true); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueProjectExecutorCredential(admin(), p, environment, keyID, true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("wrong target rotate", err) } - if err := s.RevokeProjectExecutorCredential(admin(), p, environment, keyID); !errors.Is(err, ErrNotFound) { + if err := s.RevokeProjectExecutorCredential(admin(), p, environment, keyID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("wrong target revoke", err) } } - if _, err := s.IssueProjectExecutorCredential(admin(), p, hosted.ID, uuid.NewString(), false); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueProjectExecutorCredential(admin(), p, hosted.ID, uuid.NewString(), false); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("hosted issuance", err) } - if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, uuid.NewString(), true); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, uuid.NewString(), true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unknown key rotation", err) } @@ -89,7 +90,7 @@ func TestProjectEnvironmentExecutorManagement(t *testing.T) { if err != nil || rotated.Token == issued.Token { t.Fatal("rotation", err) } - if _, err := s.AuthenticateEnvironmentExecutor(ctx, one.ID, executorDigest(issued.Token)); !errors.Is(err, ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(ctx, one.ID, executorDigest(issued.Token)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("old secret", err) } for range 2 { @@ -97,13 +98,13 @@ func TestProjectEnvironmentExecutorManagement(t *testing.T) { t.Fatal(err) } } - if _, err := s.AuthenticateEnvironmentExecutor(ctx, one.ID, executorDigest(rotated.Token)); !errors.Is(err, ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(ctx, one.ID, executorDigest(rotated.Token)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("revoked secret", err) } if listed, err := s.ListProjectExecutorCredentials(ctx, p, one.ID); err != nil || len(listed) != 1 || listed[0].RevokedAt == nil { t.Fatal("revoked list", listed, err) } - if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, keyID, false); !errors.Is(err, ErrExecutorCredentialExists) { + if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, keyID, false); !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal("resurrected secret", err) } @@ -129,13 +130,13 @@ func TestProjectEnvironmentExecutorManagement(t *testing.T) { if err := s.DeleteSession(ctx, p.TenantID, session.ID); err != nil { t.Fatal(err) } - if _, err := s.ListProjectExecutorCredentials(ctx, p, one.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.ListProjectExecutorCredentials(ctx, p, one.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted target list", err) } - if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, keyID, true); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueProjectExecutorCredential(admin(), p, one.ID, keyID, true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted target rotate", err) } - if err := s.RevokeProjectExecutorCredential(admin(), p, one.ID, keyID); !errors.Is(err, ErrNotFound) { + if err := s.RevokeProjectExecutorCredential(admin(), p, one.ID, keyID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted target revoke", err) } } @@ -170,7 +171,7 @@ func TestArchivedProjectExecutorCredentials(t *testing.T) { rotate bool want error }{ - {uuid.NewString(), uuid.NewString(), false, ErrNotFound}, + {uuid.NewString(), uuid.NewString(), false, sessions.ErrNotFound}, {environment.ID, uuid.NewString(), false, projects.ErrArchived}, {environment.ID, keyID, false, projects.ErrArchived}, {environment.ID, keyID, true, projects.ErrArchived}, @@ -205,7 +206,7 @@ func TestProjectExecutorConnectionState(t *testing.T) { if err != nil { t.Fatal(err) } - check := func() ExecutorCredentialState { + check := func() sessions.ExecutorCredentialState { t.Helper() v, e := s.ProjectExecutorCredentialState(ctx, binding.Principal, env.ID) if e != nil { @@ -281,17 +282,17 @@ func TestProjectExecutorConnectionState(t *testing.T) { } foreign := createTestProject(t, pool) for _, id := range []string{uuid.NewString(), "malformed"} { - if _, err = s.ProjectExecutorCredentialState(ctx, binding.Principal, id); !errors.Is(err, ErrNotFound) { + if _, err = s.ProjectExecutorCredentialState(ctx, binding.Principal, id); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("missing target", err) } } - if _, err = s.ProjectExecutorCredentialState(ctx, foreign.Principal, env.ID); !errors.Is(err, ErrNotFound) { + if _, err = s.ProjectExecutorCredentialState(ctx, foreign.Principal, env.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign target", err) } if _, err = pool.Exec(ctx, "UPDATE sessions SET deleted_at=clock_timestamp() WHERE id=$1", session.ID); err != nil { t.Fatal(err) } - if _, err = s.ProjectExecutorCredentialState(ctx, binding.Principal, env.ID); !errors.Is(err, ErrNotFound) { + if _, err = s.ProjectExecutorCredentialState(ctx, binding.Principal, env.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted target", err) } } diff --git a/services/core/internal/store/environment_expiry_dispatch_test.go b/services/core/internal/store/environment_expiry_dispatch_test.go index 5a788ce6c..9482a8dea 100644 --- a/services/core/internal/store/environment_expiry_dispatch_test.go +++ b/services/core/internal/store/environment_expiry_dispatch_test.go @@ -22,10 +22,10 @@ func TestWorkerEnvironmentExpiryAtFullExecutionCapacity(t *testing.T) { enableEnvironmentExpiryDispatch(h) worker, stop := startEnvironmentExpiryWorker(t, h.db, h.d) var requests []proto.Envelope - var active []store.Session + var active []sessions.Session for _, key := range []string{"one", "two", "three", "four"} { session := publicSession(t, h, key) - if _, err := worker.SubmitInputs(t.Context(), h.tenant, session.ID, key, []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"remain active"}`)}}); err != nil { + if _, err := worker.SubmitInputs(t.Context(), h.tenant, session.ID, key, []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"remain active"}`)}}); err != nil { t.Fatal(err) } requests = append(requests, h.read(testExecutionRequest)) @@ -67,7 +67,7 @@ func TestWorkerEnvironmentExpirySkipsBusySessionAndAllowsDispatch(t *testing.T) } worker, stop := startEnvironmentExpiryWorker(t, h.db, h.d) h.session = publicSession(t, h, "unrelated") - receipt, err := worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "work", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"make normal progress"}`)}}) + receipt, err := worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "work", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"make normal progress"}`)}}) if err != nil { t.Fatal(err) } @@ -79,7 +79,7 @@ func TestWorkerEnvironmentExpirySkipsBusySessionAndAllowsDispatch(t *testing.T) h.write(request.ID, proto.TypeDone, proto.DonePayload{Content: "finished"}) waitTurn(t, h, request.ID, sessions.TurnCompleted) var state string - if err := pool.QueryRow(t.Context(), "SELECT state FROM environment_input_reservations WHERE id=$1", locked.ID).Scan(&state); err != nil || state != store.EnvironmentInputPending { + if err := pool.QueryRow(t.Context(), "SELECT state FROM environment_input_reservations WHERE id=$1", locked.ID).Scan(&state); err != nil || state != sessions.EnvironmentInputPending { t.Fatal("sweep did not honor Session lock", state, err) } if err := tx.Commit(t.Context()); err != nil { diff --git a/services/core/internal/store/environment_expiry_worker_test.go b/services/core/internal/store/environment_expiry_worker_test.go index 485040579..9ce19dd15 100644 --- a/services/core/internal/store/environment_expiry_worker_test.go +++ b/services/core/internal/store/environment_expiry_worker_test.go @@ -10,29 +10,30 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) -func newEnvironmentExpiryReservation(t *testing.T, s *store.Store) (string, store.EnvironmentInputReservation) { +func newEnvironmentExpiryReservation(t *testing.T, s *store.Store) (string, sessions.EnvironmentInputReservation) { t.Helper() tenant := uuid.NewString() - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "environment", Configuration: json.RawMessage(`{"agent":{"model":"fixture-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), }) if err != nil { t.Fatal(err) } - pending, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "pending", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"wait for the environment"}`)}}) + pending, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "pending", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"wait for the environment"}`)}}) if err != nil { t.Fatal(err) } return tenant, pending } -func makeEnvironmentExpiryDue(t *testing.T, pool *pgxpool.Pool, pending *store.EnvironmentInputReservation) { +func makeEnvironmentExpiryDue(t *testing.T, pool *pgxpool.Pool, pending *sessions.EnvironmentInputReservation) { t.Helper() if err := pool.QueryRow(t.Context(), "UPDATE environment_input_reservations SET deadline=clock_timestamp()-interval '1 second' WHERE id=$1 RETURNING deadline", pending.ID).Scan(&pending.Deadline); err != nil { t.Fatal(err) @@ -63,7 +64,7 @@ func startEnvironmentExpiryWorker(t *testing.T, db fixtureDB, d *execution.Dispa return worker, stop } -func waitEnvironmentExpiry(t *testing.T, s *store.Store, tenant string, pending store.EnvironmentInputReservation) { +func waitEnvironmentExpiry(t *testing.T, s *store.Store, tenant string, pending sessions.EnvironmentInputReservation) { t.Helper() deadline := time.Now().Add(5 * time.Second) for { @@ -71,7 +72,7 @@ func waitEnvironmentExpiry(t *testing.T, s *store.Store, tenant string, pending if err != nil { t.Fatal(err) } - if got.State == store.EnvironmentInputExpired { + if got.State == sessions.EnvironmentInputExpired { if got.SettledAt == nil || !got.Deadline.Equal(pending.Deadline) || len(got.Receipts) != 0 { t.Fatal("expiry changed identity or created receipts", got) } @@ -107,7 +108,7 @@ func TestWorkerEnvironmentExpiryWithoutDevicesAndAfterRestart(t *testing.T) { _, stop := startEnvironmentExpiryWorker(t, db, d) waitEnvironmentExpiry(t, s, dueTenant, due) got, err := s.GetEnvironmentInputReservation(t.Context(), futureTenant, future.SessionID, future.ID) - if err != nil || got.State != store.EnvironmentInputPending || !got.Deadline.Equal(future.Deadline) { + if err != nil || got.State != sessions.EnvironmentInputPending || !got.Deadline.Equal(future.Deadline) { t.Fatal("future input changed", got, err) } stop() diff --git a/services/core/internal/store/environment_file_writes.go b/services/core/internal/store/environment_file_writes.go index ded255fa0..14ec762bf 100644 --- a/services/core/internal/store/environment_file_writes.go +++ b/services/core/internal/store/environment_file_writes.go @@ -2,10 +2,8 @@ package store import ( "context" - "encoding/hex" "encoding/json" "errors" - "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -13,71 +11,45 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" ) -// FileWriteIdentity binds a private mutation to one dedicated local Runtime. RequestSHA256 covers the canonical destination, byte count and data digest. -// The caller must qualify that binding and validate native receipts independently; -// persistence alone is neither placement authority nor permission to send bytes. -type FileWriteIdentity struct { - ID, DeviceID, RequestSHA256 string -} - -type EnvironmentFileWrite struct { - Identity FileWriteIdentity - EnvironmentID, SessionID string - State string - CreatedAt time.Time - SettledAt *time.Time - Replayed bool -} - -func (k FileWriteIdentity) valid() bool { - for _, value := range []string{k.ID, k.DeviceID} { - id, err := uuid.Parse(value) - if err != nil || id == uuid.Nil || id.String() != value { - return false - } - } - digest, err := hex.DecodeString(k.RequestSHA256) - return err == nil && len(digest) == 32 && hex.EncodeToString(digest) == k.RequestSHA256 -} - // ReserveEnvironmentFileWrite persists intent before external dispatch. A retry // observes the earlier operation and never authorizes resending an unknown write. -func (s *Store) ReserveEnvironmentFileWrite(ctx context.Context, tenant, environment string, key FileWriteIdentity) (EnvironmentFileWrite, error) { +func (s *Store) ReserveEnvironmentFileWrite(ctx context.Context, tenant, environment string, key sessions.FileWriteIdentity) (sessions.EnvironmentFileWrite, error) { if err := s.checkExecutionAuthority(); err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } - if !key.valid() { - return EnvironmentFileWrite{}, ErrInvalidInput + if !key.Valid() { + return sessions.EnvironmentFileWrite{}, sessions.ErrInvalidInput } owned, err := s.GetEnvironment(ctx, tenant, environment) if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } lookup, err := fileWriteLookup(tenant, environment, key.ID) if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } var origin []byte if source, ok := writeaudit.FromContext(ctx); ok { if err := source.Validate(tenant); err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } origin, err = json.Marshal(source) if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } } - var result EnvironmentFileWrite + var result sessions.EnvironmentFileWrite err = s.withPublicSession(ctx, tenant, owned.SessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { previous, err := q.GetEnvironmentFileWrite(ctx, lookup) if err == nil { result = fileWriteFromRow(previous.EnvironmentFileWrite, session) result.Replayed = true if result.Identity != key { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } return nil } @@ -89,17 +61,17 @@ func (s *Store) ReserveEnvironmentFileWrite(ctx context.Context, tenant, environ return err } if current.Environment.ID != lookup.EnvironmentID || current.Environment.Status == "failed" || current.Environment.Status == "expired" { - return ErrInvalidInput + return sessions.ErrInvalidInput } device, err := q.GetSessionDevice(ctx, sqlc.GetSessionDeviceParams{TenantID: lookup.TenantID, ID: session}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } if uuid.UUID(device.ID.Bytes).String() != key.DeviceID || device.EnvironmentID != lookup.EnvironmentID { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } if err := checkRuntimeComputeAdmission(ctx, q, session); err != nil { return err @@ -112,7 +84,7 @@ func (s *Store) ReserveEnvironmentFileWrite(ctx context.Context, tenant, environ return err } if pending { - return ErrTurnConflict + return sessions.ErrTurnConflict } deviceID, _ := parseID(key.DeviceID) row, err := q.CreateEnvironmentFileWrite(ctx, sqlc.CreateEnvironmentFileWriteParams{ @@ -124,24 +96,24 @@ func (s *Store) ReserveEnvironmentFileWrite(ctx context.Context, tenant, environ return err }) if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } return result, nil } // GetEnvironmentFileWrite also retains deleted-Session intents for internal // cleanup. It is not a public resource query and never authorizes dispatch. -func (s *Store) GetEnvironmentFileWrite(ctx context.Context, tenant, environment, id string) (EnvironmentFileWrite, error) { +func (s *Store) GetEnvironmentFileWrite(ctx context.Context, tenant, environment, id string) (sessions.EnvironmentFileWrite, error) { lookup, err := fileWriteLookup(tenant, environment, id) if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } row, err := s.queries.GetEnvironmentFileWrite(ctx, lookup) if errors.Is(err, pgx.ErrNoRows) { - return EnvironmentFileWrite{}, ErrNotFound + return sessions.EnvironmentFileWrite{}, sessions.ErrNotFound } if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } result := fileWriteFromRow(row.EnvironmentFileWrite, row.SessionID) result.Replayed = true @@ -150,19 +122,19 @@ func (s *Store) GetEnvironmentFileWrite(ctx context.Context, tenant, environment // SettleEnvironmentFileWrite requires an independently validated exact receipt. // A missing receipt, cancellation or owner retirement is not a rejected upload. -func (s *Store) SettleEnvironmentFileWrite(ctx context.Context, tenant, environment string, key FileWriteIdentity, state string) (EnvironmentFileWrite, error) { +func (s *Store) SettleEnvironmentFileWrite(ctx context.Context, tenant, environment string, key sessions.FileWriteIdentity, state string) (sessions.EnvironmentFileWrite, error) { if err := s.checkExecutionAuthority(); err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } - if !key.valid() || (state != "committed" && state != "rejected") { - return EnvironmentFileWrite{}, ErrInvalidInput + if !key.Valid() || (state != "committed" && state != "rejected") { + return sessions.EnvironmentFileWrite{}, sessions.ErrInvalidInput } previous, err := s.GetEnvironmentFileWrite(ctx, tenant, environment, key.ID) if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } lookup, _ := fileWriteLookup(tenant, environment, key.ID) - var result EnvironmentFileWrite + var result sessions.EnvironmentFileWrite err = s.withSession(ctx, tenant, previous.SessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { current, err := q.GetEnvironmentFileWrite(ctx, lookup) if err != nil { @@ -170,7 +142,7 @@ func (s *Store) SettleEnvironmentFileWrite(ctx context.Context, tenant, environm } result = fileWriteFromRow(current.EnvironmentFileWrite, session) if result.Identity != key || (result.State != "pending" && result.State != state) { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } if result.State == state { result.Replayed = true @@ -192,7 +164,7 @@ func (s *Store) SettleEnvironmentFileWrite(ctx context.Context, tenant, environm return nil }) if err != nil { - return EnvironmentFileWrite{}, err + return sessions.EnvironmentFileWrite{}, err } return result, nil } @@ -210,8 +182,8 @@ func fileWriteLookup(tenant, environment, id string) (sqlc.GetEnvironmentFileWri return result, err } -func fileWriteFromRow(row sqlc.EnvironmentFileWrite, session pgtype.UUID) EnvironmentFileWrite { - result := EnvironmentFileWrite{Identity: FileWriteIdentity{ID: uuid.UUID(row.ID.Bytes).String(), DeviceID: uuid.UUID(row.DeviceID.Bytes).String(), RequestSHA256: row.RequestSha256}, +func fileWriteFromRow(row sqlc.EnvironmentFileWrite, session pgtype.UUID) sessions.EnvironmentFileWrite { + result := sessions.EnvironmentFileWrite{Identity: sessions.FileWriteIdentity{ID: uuid.UUID(row.ID.Bytes).String(), DeviceID: uuid.UUID(row.DeviceID.Bytes).String(), RequestSHA256: row.RequestSha256}, EnvironmentID: uuid.UUID(row.EnvironmentID.Bytes).String(), SessionID: uuid.UUID(session.Bytes).String(), State: row.State, CreatedAt: row.CreatedAt.Time} if row.SettledAt.Valid { result.SettledAt = &row.SettledAt.Time @@ -222,7 +194,7 @@ func fileWriteFromRow(row sqlc.EnvironmentFileWrite, session pgtype.UUID) Enviro func checkEnvironmentFileWriteGate(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { blocked, err := q.EnvironmentFileWriteBlocksSession(ctx, session) if err == nil && blocked { - return ErrTurnConflict + return sessions.ErrTurnConflict } return err } diff --git a/services/core/internal/store/environment_file_writes_test.go b/services/core/internal/store/environment_file_writes_test.go index b7bc2d353..96021d168 100644 --- a/services/core/internal/store/environment_file_writes_test.go +++ b/services/core/internal/store/environment_file_writes_test.go @@ -7,15 +7,16 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) type fileWriteFixture struct { s, writer *Store tenant string - session Session - env Environment - key FileWriteIdentity + session sessions.Session + env sessions.Environment + key sessions.FileWriteIdentity } func newFileWriteFixture(t *testing.T) fileWriteFixture { @@ -29,7 +30,7 @@ func newFileWriteFixture(t *testing.T) fileWriteFixture { t.Fatal(err) } return fileWriteFixture{s: s, writer: writer, tenant: tenant, session: session, env: env, - key: FileWriteIdentity{ID: uuid.NewString(), DeviceID: host.ID, RequestSHA256: strings.Repeat("a", 64)}} + key: sessions.FileWriteIdentity{ID: uuid.NewString(), DeviceID: host.ID, RequestSHA256: strings.Repeat("a", 64)}} } func TestEnvironmentFileWriteRetainsUnknownAcrossLeaseLoss(t *testing.T) { @@ -54,13 +55,13 @@ func TestEnvironmentFileWriteRetainsUnknownAcrossLeaseLoss(t *testing.T) { } another := f.key another.ID = uuid.NewString() - if _, err := next.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, another); !errors.Is(err, ErrTurnConflict) { + if _, err := next.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, another); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("restart admitted successor", err) } - if _, err := reopened.ReserveEnvironmentInput(ctx, f.tenant, f.session.ID, "new-input", []Input{messageInput("new")}); !errors.Is(err, ErrTurnConflict) { + if _, err := reopened.ReserveEnvironmentInput(ctx, f.tenant, f.session.ID, "new-input", []sessions.Input{messageInput("new")}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("unknown write admitted input", err) } - if _, err := reopened.SubmitMessage(ctx, f.tenant, f.session.ID, "direct", messageInput("new").Payload); !errors.Is(err, ErrTurnConflict) { + if _, err := reopened.SubmitMessage(ctx, f.tenant, f.session.ID, "direct", messageInput("new").Payload); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("direct admission bypassed write", err) } if _, err := reopened.GetEnvironment(ctx, f.tenant, f.env.ID); err != nil { @@ -91,27 +92,27 @@ func TestEnvironmentFileWriteMatchesReceiptAndRetainsDeletedOwner(t *testing.T) if _, err := f.writer.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key); err != nil { t.Fatal(err) } - for _, change := range []func(*FileWriteIdentity){ - func(k *FileWriteIdentity) { k.DeviceID = uuid.NewString() }, - func(k *FileWriteIdentity) { k.RequestSHA256 = strings.Repeat("b", 64) }, + for _, change := range []func(*sessions.FileWriteIdentity){ + func(k *sessions.FileWriteIdentity) { k.DeviceID = uuid.NewString() }, + func(k *sessions.FileWriteIdentity) { k.RequestSHA256 = strings.Repeat("b", 64) }, } { wrong := f.key change(&wrong) - if _, err := f.writer.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, wrong); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := f.writer.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, wrong); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed retry accepted", err) } - if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, wrong, "rejected"); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, wrong, "rejected"); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("mismatched receipt settled", err) } } - if _, err := f.writer.SettleEnvironmentFileWrite(ctx, uuid.NewString(), f.env.ID, f.key, "committed"); !errors.Is(err, ErrNotFound) { + if _, err := f.writer.SettleEnvironmentFileWrite(ctx, uuid.NewString(), f.env.ID, f.key, "committed"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("cross-tenant settlement", err) } - if _, err := f.s.GetEnvironmentFileWrite(ctx, uuid.NewString(), f.env.ID, f.key.ID); !errors.Is(err, ErrNotFound) { + if _, err := f.s.GetEnvironmentFileWrite(ctx, uuid.NewString(), f.env.ID, f.key.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("cross-tenant read", err) } for _, state := range []string{"pending", "unknown", "cancelled", "retired"} { - if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key, state); !errors.Is(err, ErrInvalidInput) { + if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key, state); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("non-receipt settled write", state, err) } } @@ -121,7 +122,7 @@ func TestEnvironmentFileWriteMatchesReceiptAndRetainsDeletedOwner(t *testing.T) if got, err := f.s.GetEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key.ID); err != nil || got.State != "pending" { t.Fatal("deletion discarded unresolved write", got, err) } - if _, err := f.writer.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key); !errors.Is(err, ErrNotFound) { + if _, err := f.writer.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted Session reopened write", err) } if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key, "rejected"); err != nil { @@ -130,7 +131,7 @@ func TestEnvironmentFileWriteMatchesReceiptAndRetainsDeletedOwner(t *testing.T) if got, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key, "rejected"); err != nil || !got.Replayed { t.Fatal("receipt retry lost identity", got, err) } - if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key, "committed"); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key, "committed"); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("terminal outcome changed", err) } } @@ -140,7 +141,7 @@ func TestEnvironmentFileWriteSerializesWithInputAndRetry(t *testing.T) { original := f.key ctx := t.Context() var group sync.WaitGroup - results := make(chan EnvironmentFileWrite, 8) + results := make(chan sessions.EnvironmentFileWrite, 8) for range 8 { group.Go(func() { got, err := f.writer.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key) @@ -170,7 +171,7 @@ func TestEnvironmentFileWriteSerializesWithInputAndRetry(t *testing.T) { f.key.ID = uuid.NewString() start := make(chan struct{}) writes, inputs := make(chan error, 1), make(chan error, 1) - var pending EnvironmentInputReservation + var pending sessions.EnvironmentInputReservation group.Go(func() { <-start _, err := f.writer.ReserveEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key) @@ -180,17 +181,17 @@ func TestEnvironmentFileWriteSerializesWithInputAndRetry(t *testing.T) { group.Go(func() { <-start var err error - pending, err = f.s.ReserveEnvironmentInput(ctx, f.tenant, f.session.ID, inputKey, []Input{messageInput("race")}) + pending, err = f.s.ReserveEnvironmentInput(ctx, f.tenant, f.session.ID, inputKey, []sessions.Input{messageInput("race")}) inputs <- err }) close(start) group.Wait() writeErr, inputErr := <-writes, <-inputs - if writeErr == nil && errors.Is(inputErr, ErrTurnConflict) { + if writeErr == nil && errors.Is(inputErr, sessions.ErrTurnConflict) { if _, err := f.writer.SettleEnvironmentFileWrite(ctx, f.tenant, f.env.ID, f.key, "rejected"); err != nil { t.Fatal(err) } - } else if inputErr == nil && errors.Is(writeErr, ErrTurnConflict) { + } else if inputErr == nil && errors.Is(writeErr, sessions.ErrTurnConflict) { if _, err := f.s.CancelEnvironmentInput(ctx, f.tenant, f.session.ID, pending.ID); err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/environment_initial_input_test.go b/services/core/internal/store/environment_initial_input_test.go index 830a59385..1762ff5f3 100644 --- a/services/core/internal/store/environment_initial_input_test.go +++ b/services/core/internal/store/environment_initial_input_test.go @@ -15,7 +15,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -func initialEnvironmentReservation(t *testing.T, s *Store, pool *pgxpool.Pool, tenant, session string) EnvironmentInputReservation { +func initialEnvironmentReservation(t *testing.T, s *Store, pool *pgxpool.Pool, tenant, session string) sessions.EnvironmentInputReservation { t.Helper() var id string if err := pool.QueryRow(t.Context(), "SELECT id FROM environment_input_reservations WHERE session_id=$1 AND is_initial", session).Scan(&id); err != nil { @@ -32,7 +32,7 @@ func TestEnvironmentInitialExpiryRollsBackWithFailureEventAndSerializesPromotion s, pool := testStore(t) tenant := uuid.NewString() input := environmentInput("initial-failure-rollback", "self_hosted", "/workspace") - input.InitialInputs = []Input{messageInput("initial")} + input.InitialInputs = []sessions.Input{messageInput("initial")} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -53,7 +53,7 @@ func TestEnvironmentInitialExpiryRollsBackWithFailureEventAndSerializesPromotion t.Fatal("expiry committed without its failure event") } retained := initialEnvironmentReservation(t, s, pool, tenant, session.ID) - if retained.State != EnvironmentInputPending || retained.SettledAt != nil { + if retained.State != sessions.EnvironmentInputPending || retained.SettledAt != nil { t.Fatal("failure event rollback lost reservation", retained) } requireEnvironmentInputActivity(t, s, tenant, session.ID, "requires_action", session.Environment.ID) @@ -61,16 +61,16 @@ func TestEnvironmentInitialExpiryRollsBackWithFailureEventAndSerializesPromotion t.Fatal(err) } type result struct { - reservation EnvironmentInputReservation + reservation sessions.EnvironmentInputReservation err error } results := make(chan result, 2) - for _, settle := range []func(context.Context, string, string, string) (EnvironmentInputReservation, error){writer.PromoteEnvironmentInput, s.ExpireEnvironmentInput} { + for _, settle := range []func(context.Context, string, string, string) (sessions.EnvironmentInputReservation, error){writer.PromoteEnvironmentInput, s.ExpireEnvironmentInput} { go func() { r, err := settle(t.Context(), tenant, session.ID, reservation.ID); results <- result{r, err} }() } for i := 0; i < 2; i++ { got := <-results - if got.err != nil || got.reservation.State != EnvironmentInputExpired || len(got.reservation.Receipts) != 0 { + if got.err != nil || got.reservation.State != sessions.EnvironmentInputExpired || len(got.reservation.Receipts) != 0 { t.Fatal("expiry/promotion race started work", got) } } @@ -87,7 +87,7 @@ func TestEnvironmentInitialInputCreationRetainsCursorIdentityAndPromotion(t *tes s, pool := testStore(t) tenant := uuid.NewString() input := environmentInput("initial", kind, "/workspace") - input.InitialInputs = []Input{messageInput("first"), messageInput("second")} + input.InitialInputs = []sessions.Input{messageInput("first"), messageInput("second")} creation, err := s.CreateSessionStream(t.Context(), tenant, input) if err != nil || !creation.Created || creation.Cursor != 0 || creation.Session.LastTurn != nil { t.Fatal("creation started initial work before native readiness", creation, err) @@ -95,7 +95,7 @@ func TestEnvironmentInitialInputCreationRetainsCursorIdentityAndPromotion(t *tes session := creation.Session // The snapshot is the committed JSON projection: self-hosted input requests // its connection, while hosted initial provisioning has no caller action. - activityStatus := func(value Session) string { + activityStatus := func(value sessions.Session) string { if value.EnvironmentInputActivity == nil { return "" } @@ -107,7 +107,7 @@ func TestEnvironmentInitialInputCreationRetainsCursorIdentityAndPromotion(t *tes reservation := initialEnvironmentReservation(t, s, pool, tenant, session.ID) storedBatch, marshalErr := json.Marshal(reservation.Inputs) originalBatch, _ := json.Marshal(input.InitialInputs) - if marshalErr != nil || reservation.State != EnvironmentInputPending || reservation.Deadline.Sub(reservation.CreatedAt) != 5*time.Minute || string(storedBatch) != string(originalBatch) { + if marshalErr != nil || reservation.State != sessions.EnvironmentInputPending || reservation.Deadline.Sub(reservation.CreatedAt) != 5*time.Minute || string(storedBatch) != string(originalBatch) { t.Fatal("initial batch/deadline changed", reservation) } environmentInputHistory(t, pool, session.ID, 0, 0) @@ -133,13 +133,13 @@ func TestEnvironmentInitialInputCreationRetainsCursorIdentityAndPromotion(t *tes t.Fatal("retry changed initial reservation") } changed := input - changed.InitialInputs = []Input{messageInput("different")} - if _, err := other.CreateSession(t.Context(), tenant, changed); !errors.Is(err, ErrIdempotencyConflict) { + changed.InitialInputs = []sessions.Input{messageInput("different")} + if _, err := other.CreateSession(t.Context(), tenant, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed initial batch accepted", err) } changed = input changed.Creator.ID = "different-creator" - if _, err := other.CreateSession(t.Context(), tenant, changed); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := other.CreateSession(t.Context(), tenant, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed creator accepted", err) } writer := executionWriter(t, s) @@ -157,7 +157,7 @@ func TestEnvironmentInitialInputCreationRetainsCursorIdentityAndPromotion(t *tes requireEnvironmentInputActivity(t, s, tenant, session.ID, connectedStatus, "") environmentInputHistory(t, pool, session.ID, 0, 0) promoted, err := writer.PromoteEnvironmentInput(t.Context(), tenant, session.ID, reservation.ID) - if err != nil || promoted.State != EnvironmentInputAdmitted || !promoted.IsInitial || len(promoted.Receipts) != 2 { + if err != nil || promoted.State != sessions.EnvironmentInputAdmitted || !promoted.IsInitial || len(promoted.Receipts) != 2 { t.Fatal("initial batch did not promote", promoted, err) } active := requireEnvironmentInputActivity(t, s, tenant, session.ID, "", "") @@ -200,7 +200,7 @@ func TestEnvironmentInitialInputExpiryHasNoTurnAndCannotReplay(t *testing.T) { s, pool := testStore(t) tenant := uuid.NewString() input := environmentInput("initial-expiry", kind, "/workspace") - input.InitialInputs = []Input{messageInput("private initial text")} + input.InitialInputs = []sessions.Input{messageInput("private initial text")} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -210,7 +210,7 @@ func TestEnvironmentInitialInputExpiryHasNoTurnAndCannotReplay(t *testing.T) { t.Fatal(err) } writer := executionWriter(t, s) - for reservation.State == EnvironmentInputPending { + for reservation.State == sessions.EnvironmentInputPending { count, err := writer.ExpireEnvironmentInputs(t.Context()) if err != nil || count < 1 || count > 32 { t.Fatal("expiry made no bounded progress", count, err) @@ -251,7 +251,7 @@ func TestEnvironmentInitialInputExpiryHasNoTurnAndCannotReplay(t *testing.T) { t.Fatal(err) } late, err := writer.PromoteEnvironmentInput(t.Context(), tenant, session.ID, reservation.ID) - if err != nil || late.State != EnvironmentInputExpired || len(late.Receipts) != 0 { + if err != nil || late.State != sessions.EnvironmentInputExpired || len(late.Receipts) != 0 { t.Fatal("late connection resurrected initial input", late, err) } requireEnvironmentInputActivity(t, reopened, tenant, session.ID, "failed", "") @@ -266,7 +266,7 @@ func TestEnvironmentInitialInputExpiryHasNoTurnAndCannotReplay(t *testing.T) { t.Fatal("later work changed historical failure", err) } // The later input is still pending, so deletion waits for it to settle. - if err := reopened.DeleteSession(t.Context(), tenant, session.ID); !errors.Is(err, ErrSessionNotIdle) { + if err := reopened.DeleteSession(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("pending later input deleted", err) } if _, err := reopened.CancelEnvironmentInput(t.Context(), tenant, session.ID, later.ID); err != nil { @@ -275,7 +275,7 @@ func TestEnvironmentInitialInputExpiryHasNoTurnAndCannotReplay(t *testing.T) { if err := reopened.DeleteSession(t.Context(), tenant, session.ID); err != nil { t.Fatal(err) } - if _, err := reopened.GetSession(t.Context(), tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := reopened.GetSession(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted initial Session remained visible", err) } }) diff --git a/services/core/internal/store/environment_initial_public_test.go b/services/core/internal/store/environment_initial_public_test.go index 6147b8db6..82deb5708 100644 --- a/services/core/internal/store/environment_initial_public_test.go +++ b/services/core/internal/store/environment_initial_public_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -29,9 +30,9 @@ func TestEnvironmentInitialFailureOfficialClient(t *testing.T) { }) // Private setup isolates the persistence prerequisite from public creation admission. configuration := json.RawMessage(`{"agent":{"id":"agent_initial_failure","model":"fixture","tools":[],"multi_agent":{"enabled":false,"max_concurrent_subagents":null},"reasoning":{},"service_tier":"auto","text":{"format":{"type":"text"},"verbosity":"medium"}},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`) - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{ + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{ Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "initial", Configuration: configuration, - InitialInputs: []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"private-input-marker"}`)}}, + InitialInputs: []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"private-input-marker"}`)}}, }) if err != nil { t.Fatal(err) @@ -93,7 +94,7 @@ func TestEnvironmentInitialFailureOfficialClient(t *testing.T) { if err := db.pool.QueryRow(t.Context(), "UPDATE environment_input_reservations SET deadline=clock_timestamp()-interval '1 second' WHERE session_id=$1 AND is_initial RETURNING id", session.ID).Scan(&reservation); err != nil { t.Fatal(err) } - if result, err := writer.ExpireEnvironmentInput(t.Context(), tenant, session.ID, reservation); err != nil || result.State != store.EnvironmentInputExpired { + if result, err := writer.ExpireEnvironmentInput(t.Context(), tenant, session.ID, reservation); err != nil || result.State != sessions.EnvironmentInputExpired { t.Fatal("initial reservation did not expire", result, err) } observed := <-done diff --git a/services/core/internal/store/environment_initialization.go b/services/core/internal/store/environment_initialization.go index 5aeed5e61..b5cb9bf6b 100644 --- a/services/core/internal/store/environment_initialization.go +++ b/services/core/internal/store/environment_initialization.go @@ -5,17 +5,12 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -// EnvironmentInitialization owns preparation independently of compute ownership. -// A running record without its process-local owner is unknown, never replayable. -type EnvironmentInitialization struct { - EnvironmentID, SessionID, TenantID, DeviceID, State, Engine string -} - -func (s *Store) ListEnvironmentInitializations(ctx context.Context, after string) ([]EnvironmentInitialization, error) { +func (s *Store) ListEnvironmentInitializations(ctx context.Context, after string) ([]sessions.EnvironmentInitialization, error) { if err := s.checkExecutionOwnership(ctx); err != nil { return nil, err } @@ -31,14 +26,14 @@ func (s *Store) ListEnvironmentInitializations(ctx context.Context, after string if err != nil { return nil, err } - result := make([]EnvironmentInitialization, 0, len(rows)) + result := make([]sessions.EnvironmentInitialization, 0, len(rows)) for _, row := range rows { - result = append(result, EnvironmentInitialization{EnvironmentID: runtimeUUID(row.ID), SessionID: runtimeUUID(row.SessionID), TenantID: runtimeUUID(row.TenantID), DeviceID: runtimeUUID(row.DeviceID), State: row.Initialization, Engine: row.Engine}) + result = append(result, sessions.EnvironmentInitialization{EnvironmentID: runtimeUUID(row.ID), SessionID: runtimeUUID(row.SessionID), TenantID: runtimeUUID(row.TenantID), DeviceID: runtimeUUID(row.DeviceID), State: row.Initialization, Engine: row.Engine}) } return result, nil } -func (s *Store) mutateEnvironmentInitialization(ctx context.Context, owner EnvironmentInitialization, apply func(*sqlc.Queries, sqlc.GetSessionEnvironmentRow) error) error { +func (s *Store) mutateEnvironmentInitialization(ctx context.Context, owner sessions.EnvironmentInitialization, apply func(*sqlc.Queries, sqlc.GetSessionEnvironmentRow) error) error { if err := s.checkExecutionAuthority(); err != nil { return err } @@ -52,65 +47,65 @@ func (s *Store) mutateEnvironmentInitialization(ctx context.Context, owner Envir return err } if runtimeUUID(row.Environment.ID) != owner.EnvironmentID { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } if row.Environment.Status == "failed" || row.Environment.Status == "expired" { - return ErrNotFound + return sessions.ErrNotFound } return apply(q, row) }) } -func (s *Store) ClaimEnvironmentInitialization(ctx context.Context, owner EnvironmentInitialization) error { +func (s *Store) ClaimEnvironmentInitialization(ctx context.Context, owner sessions.EnvironmentInitialization) error { return s.mutateEnvironmentInitialization(ctx, owner, func(q *sqlc.Queries, row sqlc.GetSessionEnvironmentRow) error { if err := checkInitializationDevice(ctx, q, owner, row); err != nil { return err } count, err := q.ClaimEnvironmentInitialization(ctx, row.Environment.ID) if err == nil && count != 1 { - return ErrTurnConflict + return sessions.ErrTurnConflict } return err }) } -func checkInitializationDevice(ctx context.Context, q *sqlc.Queries, owner EnvironmentInitialization, row sqlc.GetSessionEnvironmentRow) error { +func checkInitializationDevice(ctx context.Context, q *sqlc.Queries, owner sessions.EnvironmentInitialization, row sqlc.GetSessionEnvironmentRow) error { tenant, _ := parseID(owner.TenantID) bound, err := q.GetSessionDevice(ctx, sqlc.GetSessionDeviceParams{TenantID: tenant, ID: row.Environment.SessionID}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } if runtimeUUID(bound.ID) != owner.DeviceID || bound.EnvironmentID != row.Environment.ID { - return ErrTurnConflict + return sessions.ErrTurnConflict } return nil } -func (s *Store) CompleteEnvironmentInitialization(ctx context.Context, owner EnvironmentInitialization) error { +func (s *Store) CompleteEnvironmentInitialization(ctx context.Context, owner sessions.EnvironmentInitialization) error { return s.mutateEnvironmentInitialization(ctx, owner, func(q *sqlc.Queries, row sqlc.GetSessionEnvironmentRow) error { if err := checkInitializationDevice(ctx, q, owner, row); err != nil { return err } count, err := q.CompleteEnvironmentInitialization(ctx, row.Environment.ID) if err == nil && count != 1 { - return ErrTurnConflict + return sessions.ErrTurnConflict } return err }) } // Failure records state and settles work, never destroys compute or a workspace. -func (s *Store) FailEnvironmentInitialization(ctx context.Context, owner EnvironmentInitialization, failure ProvisioningFailure) error { +func (s *Store) FailEnvironmentInitialization(ctx context.Context, owner sessions.EnvironmentInitialization, failure sessions.ProvisioningFailure) error { return s.mutateEnvironmentInitialization(ctx, owner, func(q *sqlc.Queries, row sqlc.GetSessionEnvironmentRow) error { if row.Environment.Initialization == "complete" { - return ErrTurnConflict + return sessions.ErrTurnConflict } if err := q.FailEnvironmentInitialization(ctx, row.Environment.ID); err != nil { return err } - return failEnvironment(ctx, q, row, row.Environment.SessionID, failure.reason(), failure.detail(), func() error { return cancelSessionWork(ctx, q, row.Environment.SessionID) }) + return failEnvironment(ctx, q, row, row.Environment.SessionID, failure.Reason(), failure.Detail(), func() error { return cancelSessionWork(ctx, q, row.Environment.SessionID) }) }) } diff --git a/services/core/internal/store/environment_initialization_test.go b/services/core/internal/store/environment_initialization_test.go index 9f5aba92a..ffd7e8d9f 100644 --- a/services/core/internal/store/environment_initialization_test.go +++ b/services/core/internal/store/environment_initialization_test.go @@ -20,6 +20,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -54,7 +55,7 @@ func TestUserManagedPreparationUsesAuthenticatedRuntimeWithoutAllocation(t *test } s, db := store.NewWithCredentialCipher(pool, cipher), fixtureDB{pool: pool, cipher: cipher} principal := store.FixtureExecutorPrincipal(t, s, uuid.NewString()) - session, err := s.CreateSession(t.Context(), principal.TenantID, store.CreateSessionInput{ + session, err := s.CreateSession(t.Context(), principal.TenantID, sessions.CreateSession{ Creator: principal.Subject(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"self_hosted","workspace_directory":"/home/user/work"}}`), InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/input", Data: []byte("frozen")}}, @@ -131,7 +132,7 @@ func TestUserManagedPreparationUsesAuthenticatedRuntimeWithoutAllocation(t *test want = "failed" } awaitInitialization(t, s, principal.TenantID, environment.ID, want) - if _, err := s.GetRuntimeAllocation(t.Context(), principal.TenantID, environment.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := s.GetRuntimeAllocation(t.Context(), principal.TenantID, environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("self-hosted preparation fabricated allocation", err) } if outcome == "completed" { @@ -170,9 +171,9 @@ func TestUserManagedPreparationUsesAuthenticatedRuntimeWithoutAllocation(t *test func TestEnvironmentInitializationRevocationBeforeClaim(t *testing.T) { s, db := newManagedTestStoreDB(t) principal := store.FixtureExecutorPrincipal(t, s, uuid.NewString()) - create := func() store.EnvironmentInitialization { + create := func() sessions.EnvironmentInitialization { t.Helper() - session, err := s.CreateSession(t.Context(), principal.TenantID, store.CreateSessionInput{ + session, err := s.CreateSession(t.Context(), principal.TenantID, sessions.CreateSession{ Creator: principal.Subject(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"self_hosted","workspace_directory":"/home/user/work"}}`), InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/input", Data: []byte("frozen")}}, @@ -192,19 +193,19 @@ func TestEnvironmentInitializationRevocationBeforeClaim(t *testing.T) { if err != nil { t.Fatal(err) } - return store.EnvironmentInitialization{EnvironmentID: environment.ID, SessionID: session.ID, TenantID: principal.TenantID, DeviceID: enrolled.DeviceID, State: "pending", Engine: "codex"} + return sessions.EnvironmentInitialization{EnvironmentID: environment.ID, SessionID: session.ID, TenantID: principal.TenantID, DeviceID: enrolled.DeviceID, State: "pending", Engine: "codex"} } revoked, other := create(), create() owned := executionOwner(t, db, s).Store if err := s.RevokeDevice(t.Context(), principal.TenantID, revoked.DeviceID); err != nil { t.Fatal(err) } - if err := owned.ClaimEnvironmentInitialization(t.Context(), revoked); !errors.Is(err, store.ErrNotFound) { + if err := owned.ClaimEnvironmentInitialization(t.Context(), revoked); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("revocation escaped normal admission handling: %v", err) } stale := other stale.DeviceID = uuid.NewString() - if err := owned.ClaimEnvironmentInitialization(t.Context(), stale); !errors.Is(err, store.ErrTurnConflict) { + if err := owned.ClaimEnvironmentInitialization(t.Context(), stale); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("stale binding escaped normal admission handling: %v", err) } if initializationState(t, s, other.TenantID, other.EnvironmentID) != "pending" { diff --git a/services/core/internal/store/environment_input_activity.go b/services/core/internal/store/environment_input_activity.go index 6b0b07763..a16e516e9 100644 --- a/services/core/internal/store/environment_input_activity.go +++ b/services/core/internal/store/environment_input_activity.go @@ -30,22 +30,22 @@ func environmentInputState(ctx context.Context, q *sqlc.Queries, session pgtype. if err != nil { return nil, false, err } - pending := row.State == EnvironmentInputPending + pending := row.State == sessions.EnvironmentInputPending activity := &sessions.EnvironmentInputActivity{Status: "idle", LastActiveAt: row.CreatedAt.Time} if row.SettledAt.Valid { activity.LastActiveAt = row.SettledAt.Time } - if row.State == EnvironmentInputFailed { + if row.State == sessions.EnvironmentInputFailed { activity.Status, activity.Failure = "failed", "environment_unavailable" if row.FailureCode.Valid { activity.Failure = row.FailureCode.String } } - if row.IsInitial && row.State == EnvironmentInputExpired { + if row.IsInitial && row.State == sessions.EnvironmentInputExpired { activity.Status = "failed" } if row.EnvironmentType == "openai_hosted" && row.IsInitial && - (row.State == EnvironmentInputPending || row.State == EnvironmentInputCancelled) { + (row.State == sessions.EnvironmentInputPending || row.State == sessions.EnvironmentInputCancelled) { // No Turn has started. The pinned Session contract permits idle while a // hosted Environment provisions; neither a caller action nor an invented // in-progress/idle transition is appropriate here. diff --git a/services/core/internal/store/environment_input_activity_test.go b/services/core/internal/store/environment_input_activity_test.go index e0a414608..21937bdd6 100644 --- a/services/core/internal/store/environment_input_activity_test.go +++ b/services/core/internal/store/environment_input_activity_test.go @@ -12,7 +12,7 @@ import ( "github.com/jackc/pgx/v5" ) -func requireEnvironmentInputActivity(t *testing.T, s *Store, tenant, session, status, environment string) Session { +func requireEnvironmentInputActivity(t *testing.T, s *Store, tenant, session, status, environment string) sessions.Session { t.Helper() value, err := s.GetSession(t.Context(), tenant, session) if err != nil { @@ -92,22 +92,22 @@ func TestEnvironmentInputActivityWaitsBeforeTurnAndClearsOnConnection(t *testing if active.LastTurn == nil || active.LastTurn.Status != sessions.TurnInProgress { t.Fatal("normal Turn did not take ownership") } - if _, err := s.GetSession(t.Context(), uuid.NewString(), session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSession(t.Context(), uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign Session activity visible", err) } } func TestEnvironmentInputActivitySettlementAndNewerWork(t *testing.T) { - for _, state := range []string{EnvironmentInputCancelled, EnvironmentInputExpired} { + for _, state := range []string{sessions.EnvironmentInputCancelled, sessions.EnvironmentInputExpired} { t.Run(state, func(t *testing.T) { s, pool := testStore(t) tenant, session := environmentInputSession(t, s) writer := executionWriter(t, s) - prior, err := s.SubmitInputs(t.Context(), tenant, session.ID, "prior", []Input{messageInput("prior")}) + prior, err := s.SubmitInputs(t.Context(), tenant, session.ID, "prior", []sessions.Input{messageInput("prior")}) if err != nil { t.Fatal(err) } - if _, err := writer.TransitionTurn(t.Context(), tenant, session.ID, prior[0].TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed, Outcome: []byte(`{}`)}); err != nil { + if _, err := writer.TransitionTurn(t.Context(), tenant, session.ID, prior[0].TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed, Outcome: []byte(`{}`)}); err != nil { t.Fatal(err) } reservation := reserveEnvironmentInput(t, s, tenant, session.ID, "waiting") @@ -119,14 +119,14 @@ func TestEnvironmentInputActivitySettlementAndNewerWork(t *testing.T) { if err != nil { t.Fatal(err) } - if state == EnvironmentInputCancelled { + if state == sessions.EnvironmentInputCancelled { _, err = s.CancelEnvironmentInput(t.Context(), tenant, session.ID, reservation.ID) } else { if _, err := pool.Exec(t.Context(), "UPDATE environment_input_reservations SET deadline=clock_timestamp()-interval '1 second' WHERE id=$1", reservation.ID); err != nil { t.Fatal(err) } // Other retained test rows may precede this reservation in bounded batches. - for reservation.State == EnvironmentInputPending { + for reservation.State == sessions.EnvironmentInputPending { count, sweepErr := writer.ExpireEnvironmentInputs(t.Context()) if sweepErr != nil || count < 1 || count > 32 { t.Fatal("expiry made no bounded progress", count, sweepErr) @@ -155,7 +155,7 @@ func TestEnvironmentInputActivitySettlementAndNewerWork(t *testing.T) { if after, err := s.SessionEventCursor(t.Context(), tenant, session.ID); err != nil || after != cursor { t.Fatal("settled retry repeated activity", after, cursor, err) } - if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "newer", []Input{messageInput("newer")}); err != nil { + if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "newer", []sessions.Input{messageInput("newer")}); err != nil { t.Fatal(err) } requireEnvironmentInputActivity(t, s, tenant, session.ID, "", "") @@ -173,7 +173,7 @@ func TestEnvironmentInputActivityRollsBackReservationAndConnection(t *testing.T) t.Cleanup(func() { _, _ = pool.Exec(context.Background(), "ALTER TABLE session_events DROP CONSTRAINT IF EXISTS "+constraint) }) - if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "rollback", []Input{messageInput("pending")}); err == nil { + if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "rollback", []sessions.Input{messageInput("pending")}); err == nil { t.Fatal("activity failure retained reservation") } var count int @@ -228,7 +228,7 @@ func TestEnvironmentInputActivityRecoversWaitingActionAndHidesDeletion(t *testin } requireEnvironmentInputActivity(t, s, tenant, session.ID, "requires_action", environment) got, err := s.GetEnvironmentInputReservation(t.Context(), tenant, session.ID, reservation.ID) - if err != nil || got.State != EnvironmentInputPending || !got.Deadline.Equal(reservation.Deadline) { + if err != nil || got.State != sessions.EnvironmentInputPending || !got.Deadline.Equal(reservation.Deadline) { t.Fatal("recovery changed waiting input or its deadline", got, err) } cursor, err := s.SessionEventCursor(t.Context(), tenant, session.ID) @@ -242,16 +242,16 @@ func TestEnvironmentInputActivityRecoversWaitingActionAndHidesDeletion(t *testin t.Fatal("retired generation changed activity", after, cursor, err) } environmentInputHistory(t, pool, session.ID, 0, 0) - if err := s.DeleteSession(t.Context(), tenant, session.ID); !errors.Is(err, ErrSessionNotIdle) { + if err := s.DeleteSession(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("waiting input deleted", err) } if err := s.commitLegacyDeletion(t.Context(), tenant, session.ID); err != nil { t.Fatal(err) } - if _, err := s.GetSession(t.Context(), tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSession(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted activity remained visible", err) } - if _, err := s.ListSessionEvents(t.Context(), tenant, session.ID, 0); !errors.Is(err, ErrNotFound) { + if _, err := s.ListSessionEvents(t.Context(), tenant, session.ID, 0); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted activity events remained visible", err) } } @@ -267,13 +267,13 @@ func TestPreparationFailurePreservesCancelledAndNewerInput(t *testing.T) { if err := s.FailEnvironmentInput(t.Context(), tenant, session.ID, first.ID, "runtime_preparation_failed"); err != nil { t.Fatal(err) } - for id, state := range map[string]string{first.ID: EnvironmentInputCancelled, next.ID: EnvironmentInputPending} { + for id, state := range map[string]string{first.ID: sessions.EnvironmentInputCancelled, next.ID: sessions.EnvironmentInputPending} { current, err := s.GetEnvironmentInputReservation(t.Context(), tenant, session.ID, id) if err != nil || current.State != state { t.Fatal("late failure changed another outcome", err) } } - if err := s.FailEnvironmentInput(t.Context(), tenant, session.ID, next.ID, "secret-canary"); !errors.Is(err, ErrInvalidInput) { + if err := s.FailEnvironmentInput(t.Context(), tenant, session.ID, next.ID, "secret-canary"); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("unclassified diagnostic accepted", err) } } diff --git a/services/core/internal/store/environment_input_claim_test.go b/services/core/internal/store/environment_input_claim_test.go index 7c4cc8a7f..5c27d3cd6 100644 --- a/services/core/internal/store/environment_input_claim_test.go +++ b/services/core/internal/store/environment_input_claim_test.go @@ -16,7 +16,7 @@ func TestEnvironmentInputConcurrentPromotionClaimsOnce(t *testing.T) { t.Fatal(err) } const count = 8 - results := make(chan EnvironmentInputReservation, count) + results := make(chan sessions.EnvironmentInputReservation, count) var group sync.WaitGroup for range count { group.Go(func() { @@ -34,7 +34,7 @@ func TestEnvironmentInputConcurrentPromotionClaimsOnce(t *testing.T) { fresh, received := 0, 0 for got := range results { received++ - if got.State != EnvironmentInputAdmitted || len(got.Receipts) != 2 { + if got.State != sessions.EnvironmentInputAdmitted || len(got.Receipts) != 2 { t.Fatal("promotion lost the original batch", got) } if turnID == "" { @@ -89,7 +89,7 @@ func TestEnvironmentInputConcurrentPromotionClaimsOnce(t *testing.T) { t.Fatal("terminal retry published events", after, cursor, err) } retained, err := s.GetEnvironmentInputReservation(t.Context(), tenant, session.ID, later.ID) - if err != nil || retained.State != EnvironmentInputPending || !retained.Deadline.Equal(later.Deadline) { + if err != nil || retained.State != sessions.EnvironmentInputPending || !retained.Deadline.Equal(later.Deadline) { t.Fatal("old promotion affected new preparation", retained, err) } } diff --git a/services/core/internal/store/environment_input_expiry_test.go b/services/core/internal/store/environment_input_expiry_test.go index 2704dbf2a..bcc6ecd97 100644 --- a/services/core/internal/store/environment_input_expiry_test.go +++ b/services/core/internal/store/environment_input_expiry_test.go @@ -6,12 +6,14 @@ import ( "testing" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestEnvironmentExpiryBoundsBatchAndRequiresExecutionWriter(t *testing.T) { s, pool := testStore(t) ctx := t.Context() - var reservations []EnvironmentInputReservation + var reservations []sessions.EnvironmentInputReservation for range 33 { tenant, session := environmentInputSession(t, s) pending := reserveEnvironmentInput(t, s, tenant, session.ID, "pending") @@ -45,7 +47,7 @@ func TestEnvironmentExpiryBoundsBatchAndRequiresExecutionWriter(t *testing.T) { } for _, pending := range reservations { var state string - if err := pool.QueryRow(ctx, "SELECT state FROM environment_input_reservations WHERE id=$1", pending.ID).Scan(&state); err != nil || state != EnvironmentInputExpired { + if err := pool.QueryRow(ctx, "SELECT state FROM environment_input_reservations WHERE id=$1", pending.ID).Scan(&state); err != nil || state != sessions.EnvironmentInputExpired { t.Fatal(state, err) } environmentInputHistory(t, pool, pending.SessionID, 0, 0) @@ -69,10 +71,10 @@ func TestEnvironmentExpiryFencesLostExecutionOwner(t *testing.T) { t.Fatal("lost owner expired input", n, err) } got, err := s.GetEnvironmentInputReservation(t.Context(), tenant, session.ID, pending.ID) - if err != nil || got.State != EnvironmentInputPending { + if err != nil || got.State != sessions.EnvironmentInputPending { t.Fatal("lost owner wrote through the pool", got, err) } - for got.State == EnvironmentInputPending { + for got.State == sessions.EnvironmentInputPending { n, err := successor.ExpireEnvironmentInputs(t.Context()) if err != nil || n == 0 { t.Fatal("successor could not expire input", n, err) @@ -82,7 +84,7 @@ func TestEnvironmentExpiryFencesLostExecutionOwner(t *testing.T) { t.Fatal(err) } } - if got.State != EnvironmentInputExpired { + if got.State != sessions.EnvironmentInputExpired { t.Fatal(got) } environmentInputHistory(t, pool, session.ID, 0, 0) @@ -124,18 +126,18 @@ func TestEnvironmentExpirySerializesWithTargetedSettlement(t *testing.T) { if err := pool.QueryRow(t.Context(), "SELECT state FROM environment_input_reservations WHERE id=$1", pending.ID).Scan(&state); err != nil { t.Fatal(err) } - if state != EnvironmentInputExpired && (action != "delete" || state != EnvironmentInputCancelled) { + if state != sessions.EnvironmentInputExpired && (action != "delete" || state != sessions.EnvironmentInputCancelled) { t.Fatal("invalid competing settlement", state) } environmentInputHistory(t, pool, session.ID, 0, 0) if action == "delete" { - if _, err := writer.PromoteEnvironmentInput(t.Context(), tenant, session.ID, pending.ID); !errors.Is(err, ErrNotFound) { + if _, err := writer.PromoteEnvironmentInput(t.Context(), tenant, session.ID, pending.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted input resurrected", err) } return } later := reserveEnvironmentInput(t, s, tenant, session.ID, uuid.NewString()) - for _, settle := range []func(context.Context, string, string, string) (EnvironmentInputReservation, error){writer.PromoteEnvironmentInput, s.CancelEnvironmentInput, s.ExpireEnvironmentInput} { + for _, settle := range []func(context.Context, string, string, string) (sessions.EnvironmentInputReservation, error){writer.PromoteEnvironmentInput, s.CancelEnvironmentInput, s.ExpireEnvironmentInput} { old, err := settle(t.Context(), tenant, session.ID, pending.ID) if err != nil || old.State != state { t.Fatal("old reservation changed", old, err) @@ -145,7 +147,7 @@ func TestEnvironmentExpirySerializesWithTargetedSettlement(t *testing.T) { t.Fatal(err) } got, err := s.GetEnvironmentInputReservation(t.Context(), tenant, session.ID, later.ID) - if err != nil || got.State != EnvironmentInputPending || !got.Deadline.Equal(later.Deadline) { + if err != nil || got.State != sessions.EnvironmentInputPending || !got.Deadline.Equal(later.Deadline) { t.Fatal("old settlement affected successor", got, err) } }) diff --git a/services/core/internal/store/environment_input_migration_test.go b/services/core/internal/store/environment_input_migration_test.go index 6b8815c80..c04de8f7a 100644 --- a/services/core/internal/store/environment_input_migration_test.go +++ b/services/core/internal/store/environment_input_migration_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/stdlib" @@ -120,7 +121,7 @@ func TestEnvironmentInputPromotionUsesCurrentExecutionWriter(t *testing.T) { } environmentInputHistory(t, pool, session.ID, 0, 0) got, err := successor.PromoteEnvironmentInput(t.Context(), tenant, session.ID, pending.ID) - if err != nil || got.State != EnvironmentInputAdmitted { + if err != nil || got.State != sessions.EnvironmentInputAdmitted { t.Fatal("successor could not promote", got, err) } environmentInputHistory(t, pool, session.ID, 1, 2) diff --git a/services/core/internal/store/environment_input_settlement_test.go b/services/core/internal/store/environment_input_settlement_test.go index 26adc66a6..82039e2d7 100644 --- a/services/core/internal/store/environment_input_settlement_test.go +++ b/services/core/internal/store/environment_input_settlement_test.go @@ -8,10 +8,12 @@ import ( "time" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestEnvironmentInputTerminalReservationsCannotRestart(t *testing.T) { - for _, terminal := range []string{EnvironmentInputCancelled, EnvironmentInputExpired} { + for _, terminal := range []string{sessions.EnvironmentInputCancelled, sessions.EnvironmentInputExpired} { t.Run(terminal, func(t *testing.T) { s, pool := testStore(t) writer := executionWriter(t, s) @@ -19,11 +21,11 @@ func TestEnvironmentInputTerminalReservationsCannotRestart(t *testing.T) { ctx := context.Background() pending := reserveEnvironmentInput(t, s, tenant, session.ID, "pending") early, err := s.ExpireEnvironmentInput(ctx, tenant, session.ID, pending.ID) - if err != nil || early.State != EnvironmentInputPending || early.SettledAt != nil || !early.Deadline.Equal(pending.Deadline) { + if err != nil || early.State != sessions.EnvironmentInputPending || early.SettledAt != nil || !early.Deadline.Equal(pending.Deadline) { t.Fatal("early expiry", early, err) } - var settled EnvironmentInputReservation - if terminal == EnvironmentInputExpired { + var settled sessions.EnvironmentInputReservation + if terminal == sessions.EnvironmentInputExpired { if _, err := pool.Exec(ctx, "UPDATE environment_input_reservations SET deadline=clock_timestamp()-interval '1 second' WHERE id=$1", pending.ID); err != nil { t.Fatal(err) } @@ -38,14 +40,14 @@ func TestEnvironmentInputTerminalReservationsCannotRestart(t *testing.T) { if err != nil || retry.State != terminal || retry.ID != pending.ID || !retry.Deadline.Equal(settled.Deadline) || !retry.SettledAt.Equal(*settled.SettledAt) { t.Fatal("terminal retry changed outcome", retry, err) } - if _, err := s.SubmitInputs(ctx, tenant, session.ID, "pending", pending.Inputs); !errors.Is(err, ErrTurnConflict) { + if _, err := s.SubmitInputs(ctx, tenant, session.ID, "pending", pending.Inputs); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("terminal request reopened through direct path", err) } - if _, err := s.SubmitInputs(ctx, tenant, session.ID, "pending", []Input{messageInput("changed")}); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.SubmitInputs(ctx, tenant, session.ID, "pending", []sessions.Input{messageInput("changed")}); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("terminal identity changed", err) } later := reserveEnvironmentInput(t, s, tenant, session.ID, "later") - for _, finish := range []func(context.Context, string, string, string) (EnvironmentInputReservation, error){ + for _, finish := range []func(context.Context, string, string, string) (sessions.EnvironmentInputReservation, error){ writer.PromoteEnvironmentInput, s.CancelEnvironmentInput, s.ExpireEnvironmentInput, } { got, err := finish(ctx, tenant, session.ID, pending.ID) @@ -54,7 +56,7 @@ func TestEnvironmentInputTerminalReservationsCannotRestart(t *testing.T) { } } got, err := s.GetEnvironmentInputReservation(ctx, tenant, session.ID, later.ID) - if err != nil || got.State != EnvironmentInputPending || !got.Deadline.Equal(later.Deadline) { + if err != nil || got.State != sessions.EnvironmentInputPending || !got.Deadline.Equal(later.Deadline) { t.Fatal("old settlement touched successor", got, err) } environmentInputHistory(t, pool, session.ID, 0, 0) @@ -94,14 +96,14 @@ func TestEnvironmentInputPromotionRollsBackHistoryAndSettlement(t *testing.T) { } environmentInputHistory(t, pool, session.ID, 0, 0) got, err := s.GetEnvironmentInputReservation(ctx, tenant, session.ID, pending.ID) - if err != nil || got.State != EnvironmentInputPending || got.SettledAt != nil || !got.Deadline.Equal(pending.Deadline) { + if err != nil || got.State != sessions.EnvironmentInputPending || got.SettledAt != nil || !got.Deadline.Equal(pending.Deadline) { t.Fatal("partial settlement survived", got, err) } if _, err := pool.Exec(ctx, "ALTER TABLE "+table+" DROP CONSTRAINT "+name); err != nil { t.Fatal(err) } got, err = writer.PromoteEnvironmentInput(ctx, tenant, session.ID, pending.ID) - if err != nil || got.State != EnvironmentInputAdmitted { + if err != nil || got.State != sessions.EnvironmentInputAdmitted { t.Fatal(got, err) } environmentInputHistory(t, pool, session.ID, 1, 2) @@ -128,12 +130,12 @@ func TestEnvironmentInputDeadlineIsCheckedAfterSessionLock(t *testing.T) { t.Fatal(err) } type outcome struct { - value EnvironmentInputReservation + value sessions.EnvironmentInputReservation err error } done := make(chan outcome, 1) go func() { - var got EnvironmentInputReservation + var got sessions.EnvironmentInputReservation var err error if action == "promote" { got, err = writer.PromoteEnvironmentInput(ctx, tenant, session.ID, pending.ID) @@ -169,12 +171,12 @@ func TestEnvironmentInputDeadlineIsCheckedAfterSessionLock(t *testing.T) { t.Fatal(err) } result := <-done - if result.err != nil || result.value.State != EnvironmentInputExpired || result.value.SettledAt == nil { + if result.err != nil || result.value.State != sessions.EnvironmentInputExpired || result.value.SettledAt == nil { t.Fatal("lock wait extended input lifetime", result) } environmentInputHistory(t, pool, session.ID, 0, 0) stored, err := s.GetEnvironmentInputReservation(ctx, tenant, session.ID, pending.ID) - if err != nil || stored.State != EnvironmentInputExpired { + if err != nil || stored.State != sessions.EnvironmentInputExpired { t.Fatal("expiry was rolled back", stored, err) } }) @@ -189,9 +191,9 @@ func TestEnvironmentInputCancelAndPromotionShareOneOutcome(t *testing.T) { ctx := context.Background() pending := reserveEnvironmentInput(t, s, tenant, session.ID, "pending") start := make(chan struct{}) - results := make(chan EnvironmentInputReservation, 2) + results := make(chan sessions.EnvironmentInputReservation, 2) errs := make(chan error, 2) - for _, finish := range []func(context.Context, string, string, string) (EnvironmentInputReservation, error){ + for _, finish := range []func(context.Context, string, string, string) (sessions.EnvironmentInputReservation, error){ writer.PromoteEnvironmentInput, other.CancelEnvironmentInput, } { go func() { @@ -208,11 +210,11 @@ func TestEnvironmentInputCancelAndPromotionShareOneOutcome(t *testing.T) { t.Fatal(err) } } - if first.State != second.State || (first.State != EnvironmentInputAdmitted && first.State != EnvironmentInputCancelled) { + if first.State != second.State || (first.State != sessions.EnvironmentInputAdmitted && first.State != sessions.EnvironmentInputCancelled) { t.Fatal("competing settlements diverged", first.State, second.State) } turns, inputs := 0, 0 - if first.State == EnvironmentInputAdmitted { + if first.State == sessions.EnvironmentInputAdmitted { turns, inputs = 1, 2 } environmentInputHistory(t, pool, session.ID, turns, inputs) @@ -234,7 +236,7 @@ func TestEnvironmentInputDeletionSettlesPendingAndFencesPromotion(t *testing.T) }() } if !concurrent { - if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, ErrSessionNotIdle) { + if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("pending input deleted", err) } } @@ -243,18 +245,18 @@ func TestEnvironmentInputDeletionSettlesPendingAndFencesPromotion(t *testing.T) t.Fatal(err) } if concurrent { - if err := <-done; err != nil && !errors.Is(err, ErrNotFound) { + if err := <-done; err != nil && !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } - for _, action := range []func(context.Context, string, string, string) (EnvironmentInputReservation, error){ + for _, action := range []func(context.Context, string, string, string) (sessions.EnvironmentInputReservation, error){ s.GetEnvironmentInputReservation, writer.PromoteEnvironmentInput, s.CancelEnvironmentInput, } { - if _, err := action(ctx, tenant, session.ID, pending.ID); !errors.Is(err, ErrNotFound) { + if _, err := action(ctx, tenant, session.ID, pending.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted reservation remained accessible", err) } } - if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "late", pending.Inputs); !errors.Is(err, ErrNotFound) { + if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "late", pending.Inputs); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted Session accepted reservation", err) } var state string @@ -262,7 +264,7 @@ func TestEnvironmentInputDeletionSettlesPendingAndFencesPromotion(t *testing.T) if err := pool.QueryRow(ctx, "SELECT state FROM environment_input_reservations WHERE id=$1", pending.ID).Scan(&state); err != nil { t.Fatal(err) } - if state != EnvironmentInputCancelled && (!concurrent || state != EnvironmentInputAdmitted) { + if state != sessions.EnvironmentInputCancelled && (!concurrent || state != sessions.EnvironmentInputAdmitted) { t.Fatal("deletion lost pending settlement", state) } if err := pool.QueryRow(ctx, "SELECT count(*) FROM turns WHERE session_id=$1 AND (status='queued' OR (status IN ('in_progress','waiting') AND cancel_requested_at IS NULL))", session.ID).Scan(&active); err != nil || active != 0 { diff --git a/services/core/internal/store/environment_inputs.go b/services/core/internal/store/environment_inputs.go index 26ffab496..c41a2cf56 100644 --- a/services/core/internal/store/environment_inputs.go +++ b/services/core/internal/store/environment_inputs.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -16,51 +15,21 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -const ( - EnvironmentInputPending = "pending" - EnvironmentInputAdmitted = "admitted" - EnvironmentInputExpired = "expired" - EnvironmentInputCancelled = "cancelled" - EnvironmentInputFailed = "failed" -) - -// ErrSessionInputPending rejects a new input batch while earlier Session input -// still waits for admission. It remains a Turn conflict for internal callers. -var ErrSessionInputPending = fmt.Errorf("%w: session input is still pending", ErrTurnConflict) - -// ErrHostedEnvironmentFailed rejects new input on a Session whose hosted -// Environment failed to provision. It remains ErrEnvironmentUnavailable for -// internal callers; an expired Environment keeps that plain error. -var ErrHostedEnvironmentFailed = fmt.Errorf("%w: the hosted environment failed to provision", ErrEnvironmentUnavailable) - -// EnvironmentInputReservation is private admission state, not a public Session projection. -type EnvironmentInputReservation struct { - ID string - SessionID string - State string - IsInitial bool - Inputs []Input - CreatedAt time.Time - Deadline time.Time - SettledAt *time.Time - Receipts []InputReceipt -} - // ReserveEnvironmentInput appends to active work or reserves an idle message batch. // The Session lock decides both paths; only promotion can create a new Turn. -func (s *Store) ReserveEnvironmentInput(ctx context.Context, tenantID, sessionID, key string, inputs []Input) (EnvironmentInputReservation, error) { - if err := ValidateInputKey(key); err != nil { - return EnvironmentInputReservation{}, err +func (s *Store) ReserveEnvironmentInput(ctx context.Context, tenantID, sessionID, key string, inputs []sessions.Input) (sessions.EnvironmentInputReservation, error) { + if err := sessions.ValidateInputKey(key); err != nil { + return sessions.EnvironmentInputReservation{}, err } batch, encoded, err := validateInitialInputs(inputs) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } tenant, err := parseID(tenantID) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } - var result EnvironmentInputReservation + var result sessions.EnvironmentInputReservation err = s.withEnvironmentInputSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { audit := func() error { return auditpg.RecordWriteAudit(ctx, q, tenantID, "send_events", "session", uuid.UUID(session.Bytes).String(), "") @@ -70,10 +39,10 @@ func (s *Store) ReserveEnvironmentInput(ctx context.Context, tenantID, sessionID }) if err == nil { if !previous.Matches { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } - result, err = settleEnvironmentInput(ctx, q, tenantID, previous.EnvironmentInputReservation, EnvironmentInputExpired) - if err == nil && (result.State == EnvironmentInputPending || result.State == EnvironmentInputAdmitted) { + result, err = settleEnvironmentInput(ctx, q, tenantID, previous.EnvironmentInputReservation, sessions.EnvironmentInputExpired) + if err == nil && (result.State == sessions.EnvironmentInputPending || result.State == sessions.EnvironmentInputAdmitted) { return audit() } return err @@ -87,7 +56,7 @@ func (s *Store) ReserveEnvironmentInput(ctx context.Context, tenantID, sessionID } if len(receipts) > 0 { // Earlier direct admission has receipts, but never had a reservation or deadline. - result = EnvironmentInputReservation{SessionID: sessionID, State: EnvironmentInputAdmitted, Receipts: receipts} + result = sessions.EnvironmentInputReservation{SessionID: sessionID, State: sessions.EnvironmentInputAdmitted, Receipts: receipts} return audit() } if err := checkEnvironmentFileWriteGate(ctx, q, session); err != nil { @@ -95,23 +64,23 @@ func (s *Store) ReserveEnvironmentInput(ctx context.Context, tenantID, sessionID } environment, err := q.GetSessionEnvironment(ctx, sqlc.GetSessionEnvironmentParams{TenantID: tenant, ID: session}) if errors.Is(err, pgx.ErrNoRows) { - return ErrInvalidInput + return sessions.ErrInvalidInput } else if err != nil { return err } if environment.Environment.Status == "failed" { if kind, err := storedEnvironmentType(environment); err == nil && kind == "openai_hosted" { - return ErrHostedEnvironmentFailed + return sessions.ErrHostedEnvironmentFailed } } if environment.Environment.Status == "failed" || environment.Environment.Status == "expired" { - return ErrEnvironmentUnavailable + return sessions.ErrEnvironmentUnavailable } if err := checkEnvironmentInputGate(ctx, q, session, key, encoded); err != nil { return err } if active, err := q.GetActiveTurn(ctx, session); err == nil && !active.ArtifactCaptureStarted { - result = EnvironmentInputReservation{SessionID: sessionID, State: EnvironmentInputAdmitted} + result = sessions.EnvironmentInputReservation{SessionID: sessionID, State: sessions.EnvironmentInputAdmitted} for position, input := range batch { receipt, err := admitInput(ctx, q, tenantID, session, key, int32(position), input) if err != nil { @@ -136,21 +105,21 @@ func (s *Store) ReserveEnvironmentInput(ctx context.Context, tenantID, sessionID return audit() }) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } return result, nil } -func (s *Store) GetEnvironmentInputReservation(ctx context.Context, tenantID, sessionID, reservationID string) (EnvironmentInputReservation, error) { +func (s *Store) GetEnvironmentInputReservation(ctx context.Context, tenantID, sessionID, reservationID string) (sessions.EnvironmentInputReservation, error) { id, err := parseID(reservationID) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } - var result EnvironmentInputReservation + var result sessions.EnvironmentInputReservation err = s.withPublicSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { row, err := q.GetEnvironmentInputReservation(ctx, sqlc.GetEnvironmentInputReservationParams{SessionID: session, ID: id}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -159,28 +128,28 @@ func (s *Store) GetEnvironmentInputReservation(ctx context.Context, tenantID, se return err }) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } return result, nil } // PromoteEnvironmentInput admits and claims work for the retained native preparation. -func (s *Store) PromoteEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID string) (EnvironmentInputReservation, error) { +func (s *Store) PromoteEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID string) (sessions.EnvironmentInputReservation, error) { if err := s.checkExecutionAuthority(); err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } - return s.settleEnvironmentInput(ctx, tenantID, sessionID, reservationID, EnvironmentInputAdmitted) + return s.settleEnvironmentInput(ctx, tenantID, sessionID, reservationID, sessions.EnvironmentInputAdmitted) } -func (s *Store) CancelEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID string) (EnvironmentInputReservation, error) { - return s.settleEnvironmentInput(ctx, tenantID, sessionID, reservationID, EnvironmentInputCancelled) +func (s *Store) CancelEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID string) (sessions.EnvironmentInputReservation, error) { + return s.settleEnvironmentInput(ctx, tenantID, sessionID, reservationID, sessions.EnvironmentInputCancelled) } // FailEnvironmentInput settles a confirmed pre-admission failure. The Session // lock and pending-state predicate preserve cancellation and newer input. func (s *Store) FailEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID, code string) error { if code != "model_provider_required" && code != "runtime_preparation_failed" { - return ErrInvalidInput + return sessions.ErrInvalidInput } id, err := parseID(reservationID) if err != nil { @@ -195,20 +164,20 @@ func (s *Store) FailEnvironmentInput(ctx context.Context, tenantID, sessionID, r }) } -func (s *Store) ExpireEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID string) (EnvironmentInputReservation, error) { - return s.settleEnvironmentInput(ctx, tenantID, sessionID, reservationID, EnvironmentInputExpired) +func (s *Store) ExpireEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID string) (sessions.EnvironmentInputReservation, error) { + return s.settleEnvironmentInput(ctx, tenantID, sessionID, reservationID, sessions.EnvironmentInputExpired) } -func (s *Store) settleEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID, state string) (EnvironmentInputReservation, error) { +func (s *Store) settleEnvironmentInput(ctx context.Context, tenantID, sessionID, reservationID, state string) (sessions.EnvironmentInputReservation, error) { id, err := parseID(reservationID) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } - var result EnvironmentInputReservation + var result sessions.EnvironmentInputReservation err = s.withEnvironmentInputSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { row, err := q.GetEnvironmentInputReservation(ctx, sqlc.GetEnvironmentInputReservationParams{SessionID: session, ID: id}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -217,55 +186,55 @@ func (s *Store) settleEnvironmentInput(ctx context.Context, tenantID, sessionID, return err }) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } return result, nil } -func settleEnvironmentInput(ctx context.Context, q *sqlc.Queries, tenantID string, row sqlc.EnvironmentInputReservation, state string) (EnvironmentInputReservation, error) { - if row.State != EnvironmentInputPending { +func settleEnvironmentInput(ctx context.Context, q *sqlc.Queries, tenantID string, row sqlc.EnvironmentInputReservation, state string) (sessions.EnvironmentInputReservation, error) { + if row.State != sessions.EnvironmentInputPending { return environmentInputOutcome(ctx, q, row) } if err := q.ExpireEnvironmentInputReservation(ctx, sqlc.ExpireEnvironmentInputReservationParams{SessionID: row.SessionID, ID: row.ID}); err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } row, err := q.GetEnvironmentInputReservation(ctx, sqlc.GetEnvironmentInputReservationParams{SessionID: row.SessionID, ID: row.ID}) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } // Terminal outcomes are successful storage results so settlement is not rolled back. - if row.State != EnvironmentInputPending || state == EnvironmentInputExpired { + if row.State != sessions.EnvironmentInputPending || state == sessions.EnvironmentInputExpired { return environmentInputOutcome(ctx, q, row) } result, err := environmentInputFromRow(row) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } - if state == EnvironmentInputAdmitted { + if state == sessions.EnvironmentInputAdmitted { if err := environmentInputMayStart(ctx, q, row.SessionID); err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } for position, input := range result.Inputs { receipt, err := admitInput(ctx, q, tenantID, row.SessionID, row.IdempotencyKey, int32(position), input) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } result.Receipts = append(result.Receipts, receipt) } } row, err = q.SettleEnvironmentInputReservation(ctx, sqlc.SettleEnvironmentInputReservationParams{SessionID: row.SessionID, ID: row.ID, State: state}) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } - if state == EnvironmentInputAdmitted { + if state == sessions.EnvironmentInputAdmitted { params, err := turnLookup(tenantID, result.SessionID, result.Receipts[0].TurnID) if err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } - if _, err := transitionTurn(ctx, q, params, TurnTransition{ + if _, err := transitionTurn(ctx, q, params, sessions.TurnTransition{ ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress, Outcome: json.RawMessage(`{}`), }); err != nil { - return EnvironmentInputReservation{}, err + return sessions.EnvironmentInputReservation{}, err } } result.State = row.State @@ -282,14 +251,14 @@ func environmentInputMayStart(ctx context.Context, q *sqlc.Queries, session pgty return nil } if err == nil { - return ErrTurnConflict + return sessions.ErrTurnConflict } return err } -func environmentInputOutcome(ctx context.Context, q *sqlc.Queries, row sqlc.EnvironmentInputReservation) (EnvironmentInputReservation, error) { +func environmentInputOutcome(ctx context.Context, q *sqlc.Queries, row sqlc.EnvironmentInputReservation) (sessions.EnvironmentInputReservation, error) { result, err := environmentInputFromRow(row) - if err != nil || row.State != EnvironmentInputAdmitted { + if err != nil || row.State != sessions.EnvironmentInputAdmitted { return result, err } result.Receipts, err = inputBatchReceipts(ctx, q, row.SessionID, row.IdempotencyKey, row.Batch) @@ -299,8 +268,8 @@ func environmentInputOutcome(ctx context.Context, q *sqlc.Queries, row sqlc.Envi return result, err } -func environmentInputFromRow(row sqlc.EnvironmentInputReservation) (EnvironmentInputReservation, error) { - result := EnvironmentInputReservation{ +func environmentInputFromRow(row sqlc.EnvironmentInputReservation) (sessions.EnvironmentInputReservation, error) { + result := sessions.EnvironmentInputReservation{ ID: uuid.UUID(row.ID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), State: row.State, IsInitial: row.IsInitial, CreatedAt: row.CreatedAt.Time, Deadline: row.Deadline.Time, } @@ -308,7 +277,7 @@ func environmentInputFromRow(row sqlc.EnvironmentInputReservation) (EnvironmentI result.SettledAt = &row.SettledAt.Time } if err := json.Unmarshal(row.Batch, &result.Inputs); err != nil { - return EnvironmentInputReservation{}, fmt.Errorf("decode Environment input: %w", err) + return sessions.EnvironmentInputReservation{}, fmt.Errorf("decode Environment input: %w", err) } return result, nil } @@ -319,10 +288,10 @@ func checkEnvironmentInputGate(ctx context.Context, q *sqlc.Queries, session pgt return err } if !gate.Matches { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } if gate.Blocked { - return ErrSessionInputPending + return sessions.ErrInputPending } return nil } diff --git a/services/core/internal/store/environment_inputs_test.go b/services/core/internal/store/environment_inputs_test.go index 9b31a4f6e..2b2074e70 100644 --- a/services/core/internal/store/environment_inputs_test.go +++ b/services/core/internal/store/environment_inputs_test.go @@ -15,7 +15,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -func environmentInputSession(t *testing.T, s *Store) (string, Session) { +func environmentInputSession(t *testing.T, s *Store) (string, sessions.Session) { t.Helper() tenant := uuid.NewString() session, err := s.CreateSession(context.Background(), tenant, environmentInput("session", "self_hosted", "/workspace")) @@ -25,9 +25,9 @@ func environmentInputSession(t *testing.T, s *Store) (string, Session) { return tenant, session } -func reserveEnvironmentInput(t *testing.T, s *Store, tenant, session, key string) EnvironmentInputReservation { +func reserveEnvironmentInput(t *testing.T, s *Store, tenant, session, key string) sessions.EnvironmentInputReservation { t.Helper() - got, err := s.ReserveEnvironmentInput(context.Background(), tenant, session, key, []Input{messageInput("first"), messageInput("second")}) + got, err := s.ReserveEnvironmentInput(context.Background(), tenant, session, key, []sessions.Input{messageInput("first"), messageInput("second")}) if err != nil { t.Fatal(err) } @@ -53,19 +53,19 @@ func TestEnvironmentInputReservationConcurrentIdentity(t *testing.T) { other, _ := testStore(t) tenant, session := environmentInputSession(t, s) ctx := context.Background() - batch := []Input{ + batch := []sessions.Input{ {Kind: "message", Payload: json.RawMessage(`{"text":"first","detail":{"a":1,"b":2}}`)}, messageInput("second"), } const count = 8 - results := make(chan EnvironmentInputReservation, count) + results := make(chan sessions.EnvironmentInputReservation, count) var wg sync.WaitGroup for i := range count { wg.Add(1) go func() { defer wg.Done() st := s - inputs := append([]Input(nil), batch...) + inputs := append([]sessions.Input(nil), batch...) if i%2 == 0 { st = other inputs[0].Payload = json.RawMessage(` { "detail": {"b": 2, "a": 1}, "text": "first" } `) @@ -80,7 +80,7 @@ func TestEnvironmentInputReservationConcurrentIdentity(t *testing.T) { } wg.Wait() close(results) - var first EnvironmentInputReservation + var first sessions.EnvironmentInputReservation received := 0 for result := range results { received++ @@ -91,16 +91,16 @@ func TestEnvironmentInputReservationConcurrentIdentity(t *testing.T) { t.Fatal("reservation identity changed", first, result) } } - if received != count || first.State != EnvironmentInputPending || first.ID == "" || first.Deadline.Sub(first.CreatedAt) != 5*time.Minute || first.SettledAt != nil || len(first.Receipts) != 0 { + if received != count || first.State != sessions.EnvironmentInputPending || first.ID == "" || first.Deadline.Sub(first.CreatedAt) != 5*time.Minute || first.SettledAt != nil || len(first.Receipts) != 0 { t.Fatal("invalid pending result", received, first) } environmentInputHistory(t, pool, session.ID, 0, 0) - for _, changed := range [][]Input{batch[:1], {batch[1], batch[0]}, {messageInput("changed"), batch[1]}} { - if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "request", changed); !errors.Is(err, ErrIdempotencyConflict) { + for _, changed := range [][]sessions.Input{batch[:1], {batch[1], batch[0]}, {messageInput("changed"), batch[1]}} { + if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "request", changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed request accepted", err) } } - if _, err := other.ReserveEnvironmentInput(ctx, tenant, session.ID, "other", batch); !errors.Is(err, ErrTurnConflict) { + if _, err := other.ReserveEnvironmentInput(ctx, tenant, session.ID, "other", batch); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("second pending request accepted", err) } pool.Close() @@ -119,13 +119,13 @@ func TestEnvironmentInputReservationPromotionAndDirectRetries(t *testing.T) { first := reserveEnvironmentInput(t, s, tenant, session.ID, "pending") for _, request := range []struct { key string - inputs []Input + inputs []sessions.Input want error }{ - {"pending", first.Inputs, ErrTurnConflict}, - {"pending", []Input{messageInput("changed")}, ErrIdempotencyConflict}, - {"later", []Input{messageInput("later")}, ErrTurnConflict}, - {"cancel", []Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}}, ErrTurnConflict}, + {"pending", first.Inputs, sessions.ErrTurnConflict}, + {"pending", []sessions.Input{messageInput("changed")}, sessions.ErrIdempotencyConflict}, + {"later", []sessions.Input{messageInput("later")}, sessions.ErrTurnConflict}, + {"cancel", []sessions.Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}}, sessions.ErrTurnConflict}, } { if _, err := s.SubmitInputs(ctx, tenant, session.ID, request.key, request.inputs); !errors.Is(err, request.want) { t.Fatal("direct path bypassed reservation", request.key, err) @@ -133,7 +133,7 @@ func TestEnvironmentInputReservationPromotionAndDirectRetries(t *testing.T) { } environmentInputHistory(t, pool, session.ID, 0, 0) promoted, err := writer.PromoteEnvironmentInput(ctx, tenant, session.ID, first.ID) - if err != nil || promoted.State != EnvironmentInputAdmitted || promoted.SettledAt == nil || len(promoted.Receipts) != 2 || !promoted.Deadline.Equal(first.Deadline) { + if err != nil || promoted.State != sessions.EnvironmentInputAdmitted || promoted.SettledAt == nil || len(promoted.Receipts) != 2 || !promoted.Deadline.Equal(first.Deadline) { t.Fatal(promoted, err) } for i, receipt := range promoted.Receipts { @@ -142,19 +142,19 @@ func TestEnvironmentInputReservationPromotionAndDirectRetries(t *testing.T) { } } environmentInputHistory(t, pool, session.ID, 1, 2) - for _, read := range []func() (EnvironmentInputReservation, error){ - func() (EnvironmentInputReservation, error) { + for _, read := range []func() (sessions.EnvironmentInputReservation, error){ + func() (sessions.EnvironmentInputReservation, error) { return writer.PromoteEnvironmentInput(ctx, tenant, session.ID, first.ID) }, - func() (EnvironmentInputReservation, error) { + func() (sessions.EnvironmentInputReservation, error) { return s.GetEnvironmentInputReservation(ctx, tenant, session.ID, first.ID) }, - func() (EnvironmentInputReservation, error) { + func() (sessions.EnvironmentInputReservation, error) { return s.ReserveEnvironmentInput(ctx, tenant, session.ID, "pending", first.Inputs) }, } { retry, err := read() - if err != nil || retry.ID != first.ID || !retry.Deadline.Equal(first.Deadline) || retry.State != EnvironmentInputAdmitted || len(retry.Receipts) != 2 { + if err != nil || retry.ID != first.ID || !retry.Deadline.Equal(first.Deadline) || retry.State != sessions.EnvironmentInputAdmitted || len(retry.Receipts) != 2 { t.Fatal(retry, err) } for i, receipt := range retry.Receipts { @@ -175,7 +175,7 @@ func TestEnvironmentInputReservationPromotionAndDirectRetries(t *testing.T) { pool.Close() restarted, pool := testStore(t) after, err := executionWriter(t, restarted).PromoteEnvironmentInput(ctx, tenant, session.ID, first.ID) - if err != nil || after.State != EnvironmentInputAdmitted || after.Receipts[0].Sequence != promoted.Receipts[0].Sequence { + if err != nil || after.State != sessions.EnvironmentInputAdmitted || after.Receipts[0].Sequence != promoted.Receipts[0].Sequence { t.Fatal("restart repeated promotion", after, err) } environmentInputHistory(t, pool, session.ID, 1, 2) @@ -186,21 +186,21 @@ func TestEnvironmentInputReservationKeepsEarlierDirectIdentity(t *testing.T) { tenant, session := environmentInputSession(t, s) ctx := context.Background() input := messageInput("already admitted") - receipts, err := s.SubmitInputs(ctx, tenant, session.ID, "direct", []Input{input}) + receipts, err := s.SubmitInputs(ctx, tenant, session.ID, "direct", []sessions.Input{input}) if err != nil { t.Fatal(err) } transition(t, s, tenant, session.ID, receipts[0].TurnID, sessions.TurnQueued, sessions.TurnInProgress) transition(t, s, tenant, session.ID, receipts[0].TurnID, sessions.TurnInProgress, sessions.TurnCompleted) pending := reserveEnvironmentInput(t, s, tenant, session.ID, "new") - got, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "direct", []Input{input}) - if err != nil || got.State != EnvironmentInputAdmitted || got.ID != "" || !got.Deadline.IsZero() || len(got.Receipts) != 1 || got.Receipts[0].Sequence != receipts[0].Sequence { + got, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "direct", []sessions.Input{input}) + if err != nil || got.State != sessions.EnvironmentInputAdmitted || got.ID != "" || !got.Deadline.IsZero() || len(got.Receipts) != 1 || got.Receipts[0].Sequence != receipts[0].Sequence { t.Fatal("direct admission gained a reservation", got, err) } - if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "direct", []Input{messageInput("changed")}); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "direct", []sessions.Input{messageInput("changed")}); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal(err) } - retry, err := s.SubmitInputs(ctx, tenant, session.ID, "direct", []Input{input}) + retry, err := s.SubmitInputs(ctx, tenant, session.ID, "direct", []sessions.Input{input}) if err != nil || len(retry) != 1 || !retry[0].Replayed { t.Fatal(retry, err) } @@ -234,7 +234,7 @@ func TestEnvironmentInputReservationRejectsUnsupportedOrForeignState(t *testing. return err }, } { - if err := read(); !errors.Is(err, ErrNotFound) { + if err := read(); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign access", err) } } @@ -242,13 +242,13 @@ func TestEnvironmentInputReservationRejectsUnsupportedOrForeignState(t *testing. if err != nil { t.Fatal(err) } - for _, action := range []func(context.Context, string, string, string) (EnvironmentInputReservation, error){ + for _, action := range []func(context.Context, string, string, string) (sessions.EnvironmentInputReservation, error){ s.GetEnvironmentInputReservation, writer.PromoteEnvironmentInput, s.CancelEnvironmentInput, s.ExpireEnvironmentInput, } { - if _, err := action(ctx, tenant, other.ID, pending.ID); !errors.Is(err, ErrNotFound) { + if _, err := action(ctx, tenant, other.ID, pending.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("reservation crossed Session ownership", err) } - if _, err := action(ctx, uuid.NewString(), session.ID, pending.ID); !errors.Is(err, ErrNotFound) { + if _, err := action(ctx, uuid.NewString(), session.ID, pending.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("reservation crossed tenant ownership", err) } } @@ -256,19 +256,19 @@ func TestEnvironmentInputReservationRejectsUnsupportedOrForeignState(t *testing. if err != nil || !reflect.DeepEqual(retained, pending) { t.Fatal("foreign operations changed reservation", retained, err) } - for _, invalid := range [][]Input{nil, {{Kind: "cancel", Payload: json.RawMessage(`{}`)}}, {{Kind: "tool_result", Payload: json.RawMessage(`{}`)}}} { - if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "invalid", invalid); !errors.Is(err, ErrInvalidInput) { + for _, invalid := range [][]sessions.Input{nil, {{Kind: "cancel", Payload: json.RawMessage(`{}`)}}, {{Kind: "tool_result", Payload: json.RawMessage(`{}`)}}} { + if _, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "invalid", invalid); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("unsupported reservation", err) } } noneTenant, none := newTurnSession(t, s) - if _, err := s.ReserveEnvironmentInput(ctx, noneTenant, none.ID, "none", pending.Inputs); !errors.Is(err, ErrInvalidInput) { + if _, err := s.ReserveEnvironmentInput(ctx, noneTenant, none.ID, "none", pending.Inputs); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("none reservation", err) } activeTenant, active := environmentInputSession(t, s) activeInput := submitMessage(t, s, activeTenant, active.ID, "active") steer, err := s.ReserveEnvironmentInput(ctx, activeTenant, active.ID, "new", pending.Inputs) - if err != nil || steer.State != EnvironmentInputAdmitted || steer.ID != "" || !steer.Deadline.IsZero() || len(steer.Receipts) != len(pending.Inputs) || steer.Receipts[0].TurnID != activeInput.TurnID { + if err != nil || steer.State != sessions.EnvironmentInputAdmitted || steer.ID != "" || !steer.Deadline.IsZero() || len(steer.Receipts) != len(pending.Inputs) || steer.Receipts[0].TurnID != activeInput.TurnID { t.Fatal("active input did not retain the existing Turn", steer, err) } } diff --git a/services/core/internal/store/environment_installation.go b/services/core/internal/store/environment_installation.go index e6519c00a..ae3089053 100644 --- a/services/core/internal/store/environment_installation.go +++ b/services/core/internal/store/environment_installation.go @@ -13,22 +13,11 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -var ErrInstallationAuthorization = errors.New("installation authorization is invalid or expired; obtain a new command from the Session") - -// InstallationAuthorization permits claiming one Environment's connect-only key. -// The Environment UUID is reserved as that key's ID. Reissuing an authorization -// never rotates or revives the key, and a retry must prove the same local secret. -type InstallationAuthorization struct { - Principal identity.Principal `json:"principal"` - Environment string `json:"environment_id"` - Version string `json:"version"` - ExpiresAt int64 `json:"expires_at"` -} - func (s *Store) AuthorizeEnvironmentInstallation(ctx context.Context, principal identity.Principal, environment, version string) (string, int64, error) { if err := s.selfHostedExecutorTarget(ctx, principal, environment); err != nil { return "", 0, err @@ -36,7 +25,7 @@ func (s *Store) AuthorizeEnvironmentInstallation(ctx context.Context, principal if err := activeProject(ctx, s.queries, principal); err != nil { return "", 0, err } - claim := InstallationAuthorization{Principal: principal, Environment: environment, Version: version, ExpiresAt: time.Now().Add(30 * time.Minute).Unix()} + claim := sessions.InstallationAuthorization{Principal: principal, Environment: environment, Version: version, ExpiresAt: time.Now().Add(30 * time.Minute).Unix()} payload, err := json.Marshal(claim) if err != nil { return "", 0, err @@ -49,25 +38,25 @@ func (s *Store) AuthorizeEnvironmentInstallation(ctx context.Context, principal return encoded + "." + signature, claim.ExpiresAt, nil } -func (s *Store) ValidateEnvironmentInstallation(ctx context.Context, token, version string) (InstallationAuthorization, error) { - var claim InstallationAuthorization +func (s *Store) ValidateEnvironmentInstallation(ctx context.Context, token, version string) (sessions.InstallationAuthorization, error) { + var claim sessions.InstallationAuthorization encoded, signature, ok := strings.Cut(token, ".") if !ok || len(token) > 4096 { - return claim, ErrInstallationAuthorization + return claim, sessions.ErrInstallationAuthorization } want, err := s.credentialCipher.Fingerprint("environment-installation", encoded) if err != nil || !hmac.Equal([]byte(want), []byte(signature)) { - return claim, ErrInstallationAuthorization + return claim, sessions.ErrInstallationAuthorization } payload, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil || json.Unmarshal(payload, &claim) != nil || claim.Version != version || claim.ExpiresAt <= time.Now().Unix() { - return InstallationAuthorization{}, ErrInstallationAuthorization + return sessions.InstallationAuthorization{}, sessions.ErrInstallationAuthorization } if err := s.selfHostedExecutorTarget(ctx, claim.Principal, claim.Environment); err != nil { - return InstallationAuthorization{}, ErrInstallationAuthorization + return sessions.InstallationAuthorization{}, sessions.ErrInstallationAuthorization } if err := activeProject(ctx, s.queries, claim.Principal); err != nil { - return InstallationAuthorization{}, ErrInstallationAuthorization + return sessions.InstallationAuthorization{}, sessions.ErrInstallationAuthorization } return claim, nil } @@ -82,7 +71,7 @@ func (s *Store) ClaimEnvironmentInstallation(ctx context.Context, token, version } decoded, err := base64.RawURLEncoding.DecodeString(secret) if err != nil || len(decoded) != 32 || base64.RawURLEncoding.EncodeToString(decoded) != secret { - return ErrInvalidInput + return sessions.ErrInvalidInput } principal := claim.Principal tenant, id, err := executorCredentialIdentity(principal, claim.Environment) @@ -102,17 +91,17 @@ func (s *Store) ClaimEnvironmentInstallation(ctx context.Context, token, version if len(keys) > 0 { // Existing operator-issued keys must not be replaced by onboarding. if len(keys) != 1 || keys[0].KeyID != id || keys[0].RevokedAt.Valid { - return ErrExecutorCredentialExists + return sessions.ErrExecutorCredentialExists } _, err := q.AuthenticateEnvironmentExecutor(ctx, sqlc.AuthenticateEnvironmentExecutorParams{EnvironmentID: id, TokenSha256: digest}) if errors.Is(err, pgx.ErrNoRows) { - return ErrExecutorCredentialExists + return sessions.ErrExecutorCredentialExists } return err } _, err = q.IssueExecutorCredential(ctx, sqlc.IssueExecutorCredentialParams{KeyID: id, TenantID: tenant, SubjectKind: pgtype.Text{String: principal.SubjectKind, Valid: true}, SubjectID: pgtype.Text{String: principal.SubjectID, Valid: true}, OrganizationID: principal.OrganizationID, ProjectID: principal.ProjectID, EnvironmentID: id, TokenSha256: digest}) if errors.Is(err, pgx.ErrNoRows) { - return ErrExecutorCredentialExists + return sessions.ErrExecutorCredentialExists } return err }) diff --git a/services/core/internal/store/environment_installation_test.go b/services/core/internal/store/environment_installation_test.go index 2b4e3aff1..4e3f3fc67 100644 --- a/services/core/internal/store/environment_installation_test.go +++ b/services/core/internal/store/environment_installation_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -38,19 +39,19 @@ func TestEnvironmentInstallationClaimLifetimeAndRetries(t *testing.T) { t.Fatal("authorization", err) } for _, pair := range [][2]string{{token + "x", "build"}, {token, "other-build"}, {"", "build"}} { - if _, err := s.ValidateEnvironmentInstallation(ctx, pair[0], pair[1]); !errors.Is(err, ErrInstallationAuthorization) { + if _, err := s.ValidateEnvironmentInstallation(ctx, pair[0], pair[1]); !errors.Is(err, sessions.ErrInstallationAuthorization) { t.Fatal("accepted invalid authorization", err) } } payload, _, _ := strings.Cut(token, ".") raw, _ := base64.RawURLEncoding.DecodeString(payload) - var expired InstallationAuthorization + var expired sessions.InstallationAuthorization _ = json.Unmarshal(raw, &expired) expired.ExpiresAt = time.Now().Add(-time.Second).Unix() raw, _ = json.Marshal(expired) payload = base64.RawURLEncoding.EncodeToString(raw) signature, _ := cipher.Fingerprint("environment-installation", payload) - if _, err := s.ValidateEnvironmentInstallation(ctx, payload+"."+signature, "build"); !errors.Is(err, ErrInstallationAuthorization) { + if _, err := s.ValidateEnvironmentInstallation(ctx, payload+"."+signature, "build"); !errors.Is(err, sessions.ErrInstallationAuthorization) { t.Fatal("accepted expired grant", err) } one, _, _ := newExecutorSecret() @@ -70,7 +71,7 @@ func TestEnvironmentInstallationClaimLifetimeAndRetries(t *testing.T) { t.Fatal("two machines claimed one Environment") } winner = i - } else if !errors.Is(err, ErrExecutorCredentialExists) { + } else if !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal(err) } } @@ -86,13 +87,13 @@ func TestEnvironmentInstallationClaimLifetimeAndRetries(t *testing.T) { if err := s.RevokeExecutorCredential(ctx, p, environment.ID); err != nil { t.Fatal(err) } - if err := s.ClaimEnvironmentInstallation(ctx, token, "build", secrets[winner]); !errors.Is(err, ErrExecutorCredentialExists) { + if err := s.ClaimEnvironmentInstallation(ctx, token, "build", secrets[winner]); !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal("revoked key resurrected", err) } if err := s.DeleteSession(ctx, p.TenantID, session.ID); err != nil { t.Fatal(err) } - if _, err := s.ValidateEnvironmentInstallation(ctx, token, "build"); !errors.Is(err, ErrInstallationAuthorization) { + if _, err := s.ValidateEnvironmentInstallation(ctx, token, "build"); !errors.Is(err, sessions.ErrInstallationAuthorization) { t.Fatal("deleted Session grant accepted", err) } } diff --git a/services/core/internal/store/environment_plugins_test.go b/services/core/internal/store/environment_plugins_test.go index 4245ea76a..61eaebda6 100644 --- a/services/core/internal/store/environment_plugins_test.go +++ b/services/core/internal/store/environment_plugins_test.go @@ -11,6 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -41,7 +42,7 @@ func TestPluginsFrozenInSession(t *testing.T) { } setup := environmentconfig.Setup{Plugins: []environmentconfig.Plugin{{Metadata: agentplugin.Metadata{Type: "inline", Name: "plugin-proof", Description: "A proof."}, Archive: archive.Bytes()}}, CapabilityDirectories: []string{"/workspace/generated"}} tenant, foreign := uuid.NewString(), uuid.NewString() - request := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: setup} + request := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: setup} session, err := s.CreateSession(t.Context(), tenant, request) if err != nil { t.Fatal(err) @@ -63,7 +64,7 @@ func TestPluginsFrozenInSession(t *testing.T) { if err != nil || retry.ID != session.ID { t.Fatal("retry", err) } - if _, err = s.ReadEnvironmentSetup(t.Context(), foreign, session.ID); !errors.Is(err, ErrNotFound) { + if _, err = s.ReadEnvironmentSetup(t.Context(), foreign, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign snapshot", err) } } diff --git a/services/core/internal/store/environment_runtime_fixture_test.go b/services/core/internal/store/environment_runtime_fixture_test.go index ba78d9e3f..501904ad7 100644 --- a/services/core/internal/store/environment_runtime_fixture_test.go +++ b/services/core/internal/store/environment_runtime_fixture_test.go @@ -8,12 +8,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/gorilla/websocket" ) -func enrollFixtureSession(t *testing.T, s *store.Store, tenant string, session store.Session) (store.ExecutionDevice, string) { +func enrollFixtureSession(t *testing.T, s *store.Store, tenant string, session sessions.Session) (sessions.ExecutionDevice, string) { t.Helper() environment, err := s.GetSessionEnvironment(t.Context(), tenant, session.ID) if err != nil { @@ -35,7 +36,7 @@ func enrollFixtureSession(t *testing.T, s *store.Store, tenant string, session s return bound, key.Token } -func connectFixtureRuntime(t *testing.T, h *dispatchHarness, session store.Session) *dispatchHarness { +func connectFixtureRuntime(t *testing.T, h *dispatchHarness, session sessions.Session) *dispatchHarness { t.Helper() // The Runtime shares the harness's Core, not its connection or write lock. other := &dispatchHarness{t: h.t, s: h.s, db: h.db, lease: h.lease, d: h.d, tenant: h.tenant, session: session, registry: h.registry, url: h.url, diff --git a/services/core/internal/store/environment_setup.go b/services/core/internal/store/environment_setup.go index 081be4d7f..bd4a25ddd 100644 --- a/services/core/internal/store/environment_setup.go +++ b/services/core/internal/store/environment_setup.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -30,14 +31,14 @@ func (s *Store) openEnvironmentSetup(tenant, resource, id, field string, ciphert return err } if environmentconfig.Decode(plaintext, output) != nil { - return ErrInvalidInput + return sessions.ErrInvalidInput } return nil } func (s *Store) saveEnvironmentSetup(ctx context.Context, q *sqlc.Queries, tenant string, session pgtype.UUID, setup environmentconfig.Setup) error { if setup.ValidateInstalled() != nil { - return ErrInvalidInput + return sessions.ErrInvalidInput } if setup.Empty() { return nil @@ -57,11 +58,11 @@ func (s *Store) ReadEnvironmentSetup(ctx context.Context, tenant, session string var result environmentconfig.Setup lookup, err := deviceLookup(tenant, session) if err != nil { - return result, ErrNotFound + return result, sessions.ErrNotFound } encrypted, err := s.queries.GetEnvironmentSetup(ctx, sqlc.GetEnvironmentSetupParams{TenantID: lookup.TenantID, ID: lookup.ID}) if errors.Is(err, pgx.ErrNoRows) { - return result, ErrNotFound + return result, sessions.ErrNotFound } if err != nil { return result, err @@ -70,7 +71,7 @@ func (s *Store) ReadEnvironmentSetup(ctx context.Context, tenant, session string return result, err } if result.ValidateInstalled() != nil { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } return result, nil } diff --git a/services/core/internal/store/environment_setup_test.go b/services/core/internal/store/environment_setup_test.go index 1a2f02ab2..00de50639 100644 --- a/services/core/internal/store/environment_setup_test.go +++ b/services/core/internal/store/environment_setup_test.go @@ -10,6 +10,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -22,7 +23,7 @@ func TestEnvironmentSetupEncryptedSnapshotAndIsolation(t *testing.T) { s := NewWithCredentialCipher(pool, cipher) tenant, foreign := uuid.NewString(), uuid.NewString() setup := environmentconfig.Setup{Env: map[string]string{"SECRET": "session-env-canary"}, Commands: []environmentconfig.SetupCommand{{Command: "printf session-command-canary > result"}}, Packages: v1.EnvironmentPackages{NPM: []string{"is-number@7.0.0"}}} - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: setup} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: setup} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -34,7 +35,7 @@ func TestEnvironmentSetupEncryptedSnapshotAndIsolation(t *testing.T) { if err != nil || !reflect.DeepEqual(frozen, setup) { t.Fatal("Session did not freeze setup", err) } - if _, err := s.ReadEnvironmentSetup(t.Context(), foreign, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.ReadEnvironmentSetup(t.Context(), foreign, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign Session initialization", err) } retry, err := s.CreateSession(t.Context(), tenant, input) @@ -42,10 +43,10 @@ func TestEnvironmentSetupEncryptedSnapshotAndIsolation(t *testing.T) { t.Fatal("creation replay", err) } input.Initialization.Env = map[string]string{"SECRET": "changed"} - if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed setup retried", err) } - if _, err := s.GetSessionExecutionBinding(t.Context(), tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionExecutionBinding(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("uninitialized execution admitted", err) } } diff --git a/services/core/internal/store/environment_skill_references.go b/services/core/internal/store/environment_skill_references.go index 27c355381..41a579818 100644 --- a/services/core/internal/store/environment_skill_references.go +++ b/services/core/internal/store/environment_skill_references.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -19,7 +20,7 @@ import ( // resource deletion. The returned copy no longer depends on any source resource. func (s *Store) freezeEnvironmentSkills(ctx context.Context, q *sqlc.Queries, tenantID string, setup environmentconfig.Setup) (environmentconfig.Setup, error) { if setup.Validate() != nil { - return environmentconfig.Setup{}, ErrInvalidInput + return environmentconfig.Setup{}, sessions.ErrInvalidInput } owners := make(map[string]sqlc.Skill) for _, skill := range setup.Skills { @@ -40,11 +41,11 @@ func (s *Store) freezeEnvironmentSkills(ctx context.Context, q *sqlc.Queries, te } skill, err := skills.ParseID(id) if err != nil { - return environmentconfig.Setup{}, ErrNotFound + return environmentconfig.Setup{}, sessions.ErrNotFound } owner, err := q.LockSkill(ctx, sqlc.LockSkillParams{TenantID: tenant, ID: pgtype.UUID{Bytes: skill, Valid: true}}) if errors.Is(err, pgx.ErrNoRows) { - err = ErrNotFound + err = sessions.ErrNotFound } if err != nil { return environmentconfig.Setup{}, err @@ -60,11 +61,11 @@ func (s *Store) freezeEnvironmentSkills(ctx context.Context, q *sqlc.Queries, te owner := owners[skill.Metadata.SkillID] number, err := skills.SelectVersion(skill.Metadata.Version, owner.DefaultVersion, owner.LatestVersion) if err != nil { - return environmentconfig.Setup{}, ErrInvalidInput + return environmentconfig.Setup{}, sessions.ErrInvalidInput } row, err := q.ReadSkillVersion(ctx, sqlc.ReadSkillVersionParams{TenantID: owner.TenantID, SkillID: owner.ID, Version: number}) if errors.Is(err, pgx.ErrNoRows) { - err = ErrNotFound + err = sessions.ErrNotFound } if err != nil { return environmentconfig.Setup{}, err @@ -77,7 +78,7 @@ func (s *Store) freezeEnvironmentSkills(ctx context.Context, q *sqlc.Queries, te result.Skills[i] = environmentconfig.Skill{Metadata: environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: version.SkillID, Version: strconv.FormatInt(version.Version, 10), Name: version.Name, Description: version.Description}, Archive: content.Archive} } if result.ValidateInstalled() != nil { - return environmentconfig.Setup{}, ErrInvalidInput + return environmentconfig.Setup{}, sessions.ErrInvalidInput } return result, nil } @@ -91,7 +92,7 @@ func (s *Store) openFrozenSkill(row sqlc.SkillVersion) (skills.Content, error) { } content := skills.Content{Version: skills.Version{ID: skills.FormatVersionID(row.ID.Bytes), SkillID: skills.FormatID(row.SkillID.Bytes), Version: row.Version, Name: row.Name, Description: row.Description, CreatedAt: row.CreatedAt.Time}, Archive: archive} if skills.VerifyContent(content) != nil { - return skills.Content{}, ErrInvalidInput + return skills.Content{}, sessions.ErrInvalidInput } return content, nil } diff --git a/services/core/internal/store/environment_skill_references_test.go b/services/core/internal/store/environment_skill_references_test.go index 96dc98c48..e575e51de 100644 --- a/services/core/internal/store/environment_skill_references_test.go +++ b/services/core/internal/store/environment_skill_references_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/google/uuid" ) @@ -32,7 +33,7 @@ func TestSkillReferencesFreezeWithinSessionCreation(t *testing.T) { t.Fatal(err) } intent := environmentconfig.Setup{Skills: []environmentconfig.Skill{{Metadata: environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: skill.ID}}}} - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: intent} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: intent} // Concurrent callers share one Session and one frozen installation. var group sync.WaitGroup ids := make(chan string, 6) @@ -118,7 +119,7 @@ func TestSkillReferencesFreezeWithinSessionCreation(t *testing.T) { t.Fatal("committed retry read deleted sources", err) } assertFrozen(sessionID, "1", first) - if _, err = s.ReadEnvironmentSetup(t.Context(), uuid.NewString(), sessionID); !errors.Is(err, ErrNotFound) { + if _, err = s.ReadEnvironmentSetup(t.Context(), uuid.NewString(), sessionID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign tenant read frozen Skill", err) } } @@ -135,8 +136,8 @@ func TestSkillReferenceAuthorizationRollsBackSession(t *testing.T) { if err != nil { t.Fatal(err) } - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: environmentconfig.Setup{Skills: []environmentconfig.Skill{{Metadata: environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: skill.ID}}}}} - if _, err := s.CreateSession(t.Context(), foreign, input); !errors.Is(err, ErrNotFound) { + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: environmentconfig.Setup{Skills: []environmentconfig.Skill{{Metadata: environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: skill.ID}}}}} + if _, err := s.CreateSession(t.Context(), foreign, input); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign reference accepted", err) } var count int diff --git a/services/core/internal/store/environment_skills_test.go b/services/core/internal/store/environment_skills_test.go index f8d9800af..0172bc20e 100644 --- a/services/core/internal/store/environment_skills_test.go +++ b/services/core/internal/store/environment_skills_test.go @@ -10,6 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -35,7 +36,7 @@ func TestSkillsFrozenInSession(t *testing.T) { } setup := environmentconfig.Setup{Skills: []environmentconfig.Skill{{Metadata: environmentconfig.SkillMetadata{Type: "inline", Name: "proof", Description: "A proof."}, Archive: archive.Bytes()}}} tenant, foreign := uuid.NewString(), uuid.NewString() - session, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: setup}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: setup}) if err != nil { t.Fatal(err) } @@ -43,7 +44,7 @@ func TestSkillsFrozenInSession(t *testing.T) { if err != nil || !reflect.DeepEqual(frozen.Skills, setup.Skills) { t.Fatal("frozen content changed", err) } - if _, err = s.ReadEnvironmentSetup(t.Context(), foreign, session.ID); !errors.Is(err, ErrNotFound) { + if _, err = s.ReadEnvironmentSetup(t.Context(), foreign, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign content", err) } } diff --git a/services/core/internal/store/environment_steering_order_test.go b/services/core/internal/store/environment_steering_order_test.go index d3bc6e4d6..32f895500 100644 --- a/services/core/internal/store/environment_steering_order_test.go +++ b/services/core/internal/store/environment_steering_order_test.go @@ -44,12 +44,12 @@ func TestEnvironmentActiveInputSerializesWithCompletion(t *testing.T) { return 0 } type admission struct { - value EnvironmentInputReservation + value sessions.EnvironmentInputReservation err error } admitted := make(chan admission, 1) completed := make(chan error, 1) - batch := []Input{messageInput("first"), messageInput("second")} + batch := []sessions.Input{messageInput("first"), messageInput("second")} input := func() { value, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "racing-input", batch) admitted <- admission{value, err} @@ -74,7 +74,7 @@ func TestEnvironmentActiveInputSerializesWithCompletion(t *testing.T) { t.Fatal(got.err) } if completionFirst { - if completionErr != nil || got.value.State != EnvironmentInputPending || got.value.ID == "" || len(got.value.Receipts) != 0 || got.value.Deadline.Sub(got.value.CreatedAt) != 5*time.Minute { + if completionErr != nil || got.value.State != sessions.EnvironmentInputPending || got.value.ID == "" || len(got.value.Receipts) != 0 || got.value.Deadline.Sub(got.value.CreatedAt) != 5*time.Minute { t.Fatal("completion winner did not leave new input waiting for preparation", completionErr, got.value) } environmentInputHistory(t, pool, session.ID, 1, 1) @@ -90,7 +90,7 @@ func TestEnvironmentActiveInputSerializesWithCompletion(t *testing.T) { t.Fatal(err) } } else { - if !errors.Is(completionErr, ErrUnappliedInputs) || got.value.State != EnvironmentInputAdmitted || got.value.ID != "" || !got.value.Deadline.IsZero() || len(got.value.Receipts) != 2 || got.value.Receipts[0].TurnID != original.TurnID || got.value.Receipts[0].Replayed { + if !errors.Is(completionErr, sessions.ErrUnappliedInputs) || got.value.State != sessions.EnvironmentInputAdmitted || got.value.ID != "" || !got.value.Deadline.IsZero() || len(got.value.Receipts) != 2 || got.value.Receipts[0].TurnID != original.TurnID || got.value.Receipts[0].Replayed { t.Fatal("admitted input escaped the original Turn or application fence", completionErr, got.value) } environmentInputHistory(t, pool, session.ID, 1, 3) diff --git a/services/core/internal/store/environment_work_test.go b/services/core/internal/store/environment_work_test.go index cb4010ce3..4e85ec56c 100644 --- a/services/core/internal/store/environment_work_test.go +++ b/services/core/internal/store/environment_work_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -26,7 +27,7 @@ func TestEnvironmentInputWorkFiltersAndPagesDevices(t *testing.T) { t.Fatal(err) } case "deleted": - if err := h.s.DeleteSession(t.Context(), h.tenant, pending.SessionID); !errors.Is(err, store.ErrSessionNotIdle) { + if err := h.s.DeleteSession(t.Context(), h.tenant, pending.SessionID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("pending input deleted", err) } if err := h.s.CommitLegacyDeletion(t.Context(), h.tenant, pending.SessionID); err != nil { diff --git a/services/core/internal/store/environment_worker_helpers_test.go b/services/core/internal/store/environment_worker_helpers_test.go index eaf17064f..e437e2f0e 100644 --- a/services/core/internal/store/environment_worker_helpers_test.go +++ b/services/core/internal/store/environment_worker_helpers_test.go @@ -31,7 +31,7 @@ func workerEnvironmentCapabilities() proto.AgentKindCapabilities { return prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, LocalEnvironment: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported, WorkspaceOutputExport: proto.CapabilitySupported}) } -func workerEnvironmentReservation(t *testing.T, h *dispatchHarness) store.EnvironmentInputReservation { +func workerEnvironmentReservation(t *testing.T, h *dispatchHarness) sessions.EnvironmentInputReservation { t.Helper() pending := unboundWorkerEnvironmentReservation(t, h) session, err := h.s.GetSession(t.Context(), h.tenant, pending.SessionID) @@ -42,13 +42,13 @@ func workerEnvironmentReservation(t *testing.T, h *dispatchHarness) store.Enviro return pending } -func unboundWorkerEnvironmentReservation(t *testing.T, h *dispatchHarness) store.EnvironmentInputReservation { +func unboundWorkerEnvironmentReservation(t *testing.T, h *dispatchHarness) sessions.EnvironmentInputReservation { t.Helper() - session, err := h.s.CreateSession(t.Context(), h.tenant, store.WithFixtureModelProvider(store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)})) + session, err := h.s.CreateSession(t.Context(), h.tenant, store.WithFixtureModelProvider(sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)})) if err != nil { t.Fatal(err) } - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, session.ID, "work", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, session.ID, "work", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) if err != nil { t.Fatal(err) } @@ -110,7 +110,7 @@ func nextWorkerFrame(t *testing.T, frames <-chan proto.Envelope, kind string) pr } } -func awaitWorkerEnvironmentRun(t *testing.T, ctx context.Context, s *store.Store, tenant string, pending store.EnvironmentInputReservation) execution.EnvironmentRun { +func awaitWorkerEnvironmentRun(t *testing.T, ctx context.Context, s *store.Store, tenant string, pending sessions.EnvironmentInputReservation) execution.EnvironmentRun { t.Helper() var run execution.EnvironmentRun awaitDaemonRemoteCondition(t, ctx, 5*time.Minute, "worker terminal Environment Turn", func() bool { diff --git a/services/core/internal/store/environment_worker_scan_test.go b/services/core/internal/store/environment_worker_scan_test.go index b517a9921..64fe1191f 100644 --- a/services/core/internal/store/environment_worker_scan_test.go +++ b/services/core/internal/store/environment_worker_scan_test.go @@ -6,7 +6,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestWorkerEnvironmentRetriesNewlyReadyAtNextScan(t *testing.T) { @@ -45,7 +44,7 @@ func TestWorkerEnvironmentRetriesNewlyReadyAtNextScan(t *testing.T) { nextWorkerFrame(t, frames, proto.TypeExecutionRelease) stop() stored, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, pending.SessionID, pending.ID) - if err != nil || stored.State != store.EnvironmentInputPending || !stored.Deadline.Equal(pending.Deadline) || len(stored.Receipts) != 0 { + if err != nil || stored.State != sessions.EnvironmentInputPending || !stored.Deadline.Equal(pending.Deadline) || len(stored.Receipts) != 0 { t.Fatal("readiness retry changed pending identity or admitted work", stored, err) } } @@ -53,7 +52,7 @@ func TestWorkerEnvironmentRetriesNewlyReadyAtNextScan(t *testing.T) { func TestWorkerEnvironmentPaginationReachesReadyTail(t *testing.T) { h := newDispatchHarness(t) enableWorkerEnvironment(t, h) - var last store.EnvironmentInputReservation + var last sessions.EnvironmentInputReservation for range 101 { pending := unboundWorkerEnvironmentReservation(t, h) if pending.ID > last.ID { diff --git a/services/core/internal/store/environment_worker_test.go b/services/core/internal/store/environment_worker_test.go index d28a0dcad..275e1220f 100644 --- a/services/core/internal/store/environment_worker_test.go +++ b/services/core/internal/store/environment_worker_test.go @@ -15,7 +15,7 @@ func TestWorkerEnvironmentSharesCapacityThroughClaimAndCleanup(t *testing.T) { h := newDispatchHarness(t) _, pool := store.NewTestStore(t) enableWorkerEnvironment(t, h) - pending := map[string]store.EnvironmentInputReservation{} + pending := map[string]sessions.EnvironmentInputReservation{} for range 2 { value := workerEnvironmentReservation(t, h) pending[value.SessionID] = value @@ -25,7 +25,7 @@ func TestWorkerEnvironmentSharesCapacityThroughClaimAndCleanup(t *testing.T) { runtimes = append(runtimes, runtime) } frames := workerFrames(t, runtimes...) - ordinary := map[string]store.Session{} + ordinary := map[string]sessions.Session{} for _, key := range []string{"one", "two", "three"} { session := publicSession(t, h, key) h.session = session @@ -160,7 +160,7 @@ func TestWorkerEnvironmentRetriesPendingWithoutExtendingDeadline(t *testing.T) { stop() nextWorkerFrame(t, frames, proto.TypeExecutionRelease) stored, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, pending.SessionID, pending.ID) - if err != nil || stored.State != store.EnvironmentInputPending || !stored.Deadline.Equal(pending.Deadline) || len(stored.Receipts) != 0 { + if err != nil || stored.State != sessions.EnvironmentInputPending || !stored.Deadline.Equal(pending.Deadline) || len(stored.Receipts) != 0 { t.Fatal("retry or shutdown changed the original reservation", stored, err) } _, stop = startEnvironmentExpiryWorker(t, h.db, h.d) diff --git a/services/core/internal/store/environment_write_audit_test.go b/services/core/internal/store/environment_write_audit_test.go index 8c38d9578..f681bd6a1 100644 --- a/services/core/internal/store/environment_write_audit_test.go +++ b/services/core/internal/store/environment_write_audit_test.go @@ -99,7 +99,7 @@ func TestArtifactDeleteWriteAuditAndRollback(t *testing.T) { if err == nil { t.Fatal("artifact deletion bypassed audit failure") } - if err := s.ReadSessionArtifact(t.Context(), tenant, session, artifact.ID, func(_ SessionArtifact, r io.Reader) error { + if err := s.ReadSessionArtifact(t.Context(), tenant, session, artifact.ID, func(_ sessions.Artifact, r io.Reader) error { body, err := io.ReadAll(r) if string(body) != "secret-file-body" { t.Error("large object did not roll back") diff --git a/services/core/internal/store/environments.go b/services/core/internal/store/environments.go index 68bb57f34..eb206c0a4 100644 --- a/services/core/internal/store/environments.go +++ b/services/core/internal/store/environments.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -14,19 +13,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -// Environment retains execution ownership; its configuration is an internal snapshot, not a public response. -type Environment struct { - Initialization string - ID string - SessionID string - TenantID string - Status string - CreatedAt time.Time - Configuration json.RawMessage -} - func createSessionEnvironment(ctx context.Context, q *sqlc.Queries, session sqlc.Session) error { var snapshot struct { Environment *struct { @@ -34,7 +23,7 @@ func createSessionEnvironment(ctx context.Context, q *sqlc.Queries, session sqlc } `json:"environment"` } if err := json.Unmarshal(session.Configuration, &snapshot); err != nil { - return fmt.Errorf("%w: invalid environment configuration", ErrInvalidInput) + return fmt.Errorf("%w: invalid environment configuration", sessions.ErrInvalidInput) } if snapshot.Environment == nil || snapshot.Environment.Type == "none" { return nil @@ -45,45 +34,45 @@ func createSessionEnvironment(ctx context.Context, q *sqlc.Queries, session sqlc ID: pgtype.UUID{Bytes: uuid.New(), Valid: true}, SessionID: session.ID, }) default: - return fmt.Errorf("%w: unsupported environment type", ErrInvalidInput) + return fmt.Errorf("%w: unsupported environment type", sessions.ErrInvalidInput) } } -func (s *Store) GetEnvironment(ctx context.Context, tenantID, environmentID string) (Environment, error) { +func (s *Store) GetEnvironment(ctx context.Context, tenantID, environmentID string) (sessions.Environment, error) { tenant, err := parseID(tenantID) if err != nil { - return Environment{}, err + return sessions.Environment{}, err } id := pgunit.PathID(environmentID) row, err := s.queries.GetEnvironment(ctx, sqlc.GetEnvironmentParams{TenantID: tenant, ID: id}) return environmentFromRow(row.Environment, row.TenantID, row.Configuration, err) } -func (s *Store) GetSessionEnvironment(ctx context.Context, tenantID, sessionID string) (Environment, error) { +func (s *Store) GetSessionEnvironment(ctx context.Context, tenantID, sessionID string) (sessions.Environment, error) { tenant, err := parseID(tenantID) if err != nil { - return Environment{}, err + return sessions.Environment{}, err } id, err := parseID(sessionID) if err != nil { - return Environment{}, err + return sessions.Environment{}, err } row, err := s.queries.GetSessionEnvironment(ctx, sqlc.GetSessionEnvironmentParams{TenantID: tenant, ID: id}) return environmentFromRow(row.Environment, row.TenantID, row.Configuration, err) } -func environmentFromRow(row sqlc.Environment, tenant pgtype.UUID, configuration []byte, err error) (Environment, error) { +func environmentFromRow(row sqlc.Environment, tenant pgtype.UUID, configuration []byte, err error) (sessions.Environment, error) { if errors.Is(err, pgx.ErrNoRows) { - return Environment{}, ErrNotFound + return sessions.Environment{}, sessions.ErrNotFound } if err != nil { - return Environment{}, fmt.Errorf("get environment: %w", err) + return sessions.Environment{}, fmt.Errorf("get environment: %w", err) } configuration, err = jsonobject.Normalize(configuration) if err != nil { - return Environment{}, fmt.Errorf("decode environment configuration: %w: %w", ErrInvalidInput, err) + return sessions.Environment{}, fmt.Errorf("decode environment configuration: %w: %w", sessions.ErrInvalidInput, err) } - return Environment{ + return sessions.Environment{ ID: uuid.UUID(row.ID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), TenantID: uuid.UUID(tenant.Bytes).String(), Status: row.Status, Initialization: row.Initialization, CreatedAt: row.CreatedAt.Time, Configuration: configuration, diff --git a/services/core/internal/store/environments_test.go b/services/core/internal/store/environments_test.go index d26daeb42..b82284250 100644 --- a/services/core/internal/store/environments_test.go +++ b/services/core/internal/store/environments_test.go @@ -11,15 +11,16 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) -func environmentInput(key, kind, directory string) CreateSessionInput { +func environmentInput(key, kind, directory string) sessions.CreateSession { configuration, _ := json.Marshal(map[string]any{ "agent": map[string]string{"model": "fixture-model"}, "environment": map[string]any{"type": kind, "workspace_directory": directory, "capability_directories": []string{}}, }) - return CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: configuration} + return sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: configuration} } func TestEnvironmentOwnershipPersistsAndStaysScoped(t *testing.T) { @@ -45,7 +46,7 @@ func TestEnvironmentOwnershipPersistsAndStaysScoped(t *testing.T) { func() error { _, err := s.GetEnvironment(ctx, foreign, first.ID); return err }, func() error { _, err := s.GetSessionEnvironment(ctx, foreign, session.ID); return err }, } { - if err := lookup(); !errors.Is(err, ErrNotFound) { + if err := lookup(); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign access", err) } } @@ -58,7 +59,7 @@ func TestEnvironmentOwnershipPersistsAndStaysScoped(t *testing.T) { t.Fatal(otherEnvironment, err) } changed := environmentInput(input.IdempotencyKey, kind, "/changed") - if _, err := s.CreateSession(ctx, tenant, changed); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(ctx, tenant, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed configuration accepted", err) } if _, err := s.UpdateSessionMetadata(ctx, tenant, session.ID, map[string]string{"updated": "yes"}); err != nil { @@ -86,8 +87,8 @@ func TestEnvironmentCreationWinnerOwnsSnapshotAndIdentity(t *testing.T) { intent := json.RawMessage(`{"request":"resolved-template"}`) const count = 8 type result struct { - creation SessionCreation - environment Environment + creation sessions.Creation + environment sessions.Environment } results := make(chan result, count) var wg sync.WaitGroup @@ -101,7 +102,7 @@ func TestEnvironmentCreationWinnerOwnsSnapshotAndIdentity(t *testing.T) { } input := environmentInput("winner", "openai_hosted", fmt.Sprintf("/workspace/%d", i)) input.CreationRequest = intent - input.InitialInputs = []Input{messageInput("initial")} + input.InitialInputs = []sessions.Input{messageInput("initial")} creation, err := st.CreateSessionStream(ctx, tenant, input) if err != nil { t.Error(err) @@ -160,7 +161,7 @@ func TestEnvironmentCreationWinnerOwnsSnapshotAndIdentity(t *testing.T) { restarted, _ := testStore(t) retryInput := environmentInput("winner", "openai_hosted", "/changed-resolution") retryInput.CreationRequest = intent - retryInput.InitialInputs = []Input{messageInput("initial")} + retryInput.InitialInputs = []sessions.Input{messageInput("initial")} retry, err := restarted.CreateSessionStream(ctx, tenant, retryInput) if err != nil || retry.Created || retry.Session.ID != first.creation.Session.ID { t.Fatal(retry, err) @@ -202,7 +203,7 @@ func TestEnvironmentCreationFailureRollsBackAllResources(t *testing.T) { } t.Cleanup(func() { _, _ = pool.Exec(ctx, "ALTER TABLE "+table+" DROP CONSTRAINT IF EXISTS "+constraint) }) input := environmentInput("rollback", "self_hosted", "/workspace") - input.InitialInputs = []Input{messageInput("first"), messageInput(marker)} + input.InitialInputs = []sessions.Input{messageInput("first"), messageInput(marker)} if got, err := s.CreateSession(ctx, tenant, input); err == nil || got.ID != "" { t.Fatal("partial creation succeeded", got, err) } @@ -244,13 +245,13 @@ func TestEnvironmentDeletionHidesWithoutDestroyingOwnership(t *testing.T) { if err := s.DeleteSession(ctx, tenant, session.ID); err != nil { t.Fatal(err) } - if _, err := s.GetEnvironment(ctx, tenant, environment.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetEnvironment(ctx, tenant, environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := s.GetSessionEnvironment(ctx, tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionEnvironment(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("deleted retry resurrected ownership", err) } var retained int @@ -264,12 +265,12 @@ func TestEnvironmentAbsentForNoneAndLegacySnapshots(t *testing.T) { ctx := context.Background() tenant := uuid.NewString() for i, configuration := range []json.RawMessage{nil, json.RawMessage(`{}`), json.RawMessage(`{"environment":{"type":"none"}}`)} { - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: fmt.Sprintf("none-%d", i), Configuration: configuration} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: fmt.Sprintf("none-%d", i), Configuration: configuration} session, err := s.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) } - if _, err := s.GetSessionEnvironment(ctx, tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionEnvironment(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unexpected Environment", err) } retry, err := s.CreateSession(ctx, tenant, input) diff --git a/services/core/internal/store/execution_test.go b/services/core/internal/store/execution_test.go index 9c1e466ce..eaecd1dbc 100644 --- a/services/core/internal/store/execution_test.go +++ b/services/core/internal/store/execution_test.go @@ -58,12 +58,12 @@ func TestExecutionLeaseLossFencesAllLifecycleWrites(t *testing.T) { if err = writer.BindSessionDevice(t.Context(), tenant, active.ID, host.ID); err != nil { t.Fatal(err) } - queued, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "queued"}) + queued, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "queued"}) if err != nil { t.Fatal(err) } pending := submitMessage(t, s, tenant, queued.ID, "queued") - waiting, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "waiting"}) + waiting, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "waiting"}) if err != nil { t.Fatal(err) } @@ -97,14 +97,14 @@ func TestExecutionLeaseLossFencesAllLifecycleWrites(t *testing.T) { } } mustReject("binding", writer.BindSessionDevice(t.Context(), tenant, queued.ID, host.ID)) - _, err = writer.TransitionTurn(t.Context(), tenant, queued.ID, pending.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = writer.TransitionTurn(t.Context(), tenant, queued.ID, pending.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) mustReject("claim", err) - mustReject("journal", writer.AppendTurnEvents(t.Context(), tenant, active.ID, input.TurnID, 1, []ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"stale"}`)}})) + mustReject("journal", writer.AppendTurnEvents(t.Context(), tenant, active.ID, input.TurnID, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"stale"}`)}})) mustReject("callback", writer.RecordFunctionCall(t.Context(), tenant, active.ID, input.TurnID, functionCallFixture("late"))) mustReject("receipt", writer.ConfirmFunctionResult(t.Context(), tenant, waiting.ID, waitInput.TurnID, call.CallID)) _, err = writer.CompleteExecution(t.Context(), tenant, active.ID, input.TurnID, sessions.TurnCompleted, json.RawMessage(`{"done":{"content":"stale"}}`), "stale-native", input.Sequence) mustReject("completion", err) - _, err = writer.TransitionTurn(t.Context(), tenant, active.ID, input.TurnID, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed}) + _, err = writer.TransitionTurn(t.Context(), tenant, active.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed}) mustReject("reconciliation", err) _, err = writer.ExpireEnvironmentInputs(t.Context()) mustReject("input expiry", err) @@ -125,7 +125,7 @@ func TestExecutionLeaseLossFencesAllLifecycleWrites(t *testing.T) { if err != nil || saved.Applied { t.Fatal("stale receipt persisted", saved, err) } - if _, err = s.GetSessionDevice(t.Context(), tenant, queued.ID); !errors.Is(err, ErrNotFound) { + if _, err = s.GetSessionDevice(t.Context(), tenant, queued.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("stale binding persisted", err) } queuedTurn, err := s.GetTurn(t.Context(), tenant, queued.ID, pending.TurnID) @@ -144,8 +144,8 @@ func TestExecutionLeaseLossFencesAllLifecycleWrites(t *testing.T) { if err != nil || bound.NativeSessionID != "successor-native" { t.Fatal(bound, err) } - _, err = successor.TransitionTurn(t.Context(), tenant, active.ID, input.TurnID, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed}) - if !errors.Is(err, ErrTurnConflict) { + _, err = successor.TransitionTurn(t.Context(), tenant, active.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed}) + if !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("terminal CAS changed", err) } if err = successor.lease.Close(t.Context()); err != nil { @@ -181,9 +181,9 @@ func TestExecutionWriterSerializesWritesOnItsLease(t *testing.T) { results := make(chan error, len(tasks)*2) for _, task := range tasks { group.Go(func() { - _, err := writer.TransitionTurn(t.Context(), task.tenant, task.session, task.turn, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err := writer.TransitionTurn(t.Context(), task.tenant, task.session, task.turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err == nil { - err = writer.AppendTurnEvents(t.Context(), task.tenant, task.session, task.turn, 1, []ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"accepted"}`)}}) + err = writer.AppendTurnEvents(t.Context(), task.tenant, task.session, task.turn, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"accepted"}`)}}) } results <- err }) @@ -235,7 +235,7 @@ func TestPooledStoreHasNoExecutionAuthority(t *testing.T) { } _, archiveErr := s.ArchiveManagedSession(t.Context(), tenant, session.ID, 0) _, expiryErr := s.ExpireEnvironmentInputs(t.Context()) - subagent := []ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"visible"}`)}, subagentIdentityEvent("child", "root", 100)} + subagent := []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"visible"}`)}, subagentIdentityEvent("child", "root", 100)} for name, err := range map[string]error{ "ownership check": s.checkExecutionOwnership(t.Context()), "archive": archiveErr, diff --git a/services/core/internal/store/executor_credential_target.go b/services/core/internal/store/executor_credential_target.go index 1ca267618..b6930966d 100644 --- a/services/core/internal/store/executor_credential_target.go +++ b/services/core/internal/store/executor_credential_target.go @@ -10,6 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -17,7 +18,7 @@ import ( func executorCredentialIdentity(principal identity.Principal, keyID string) (pgtype.UUID, pgtype.UUID, error) { if err := principal.Validate(); err != nil { - return pgtype.UUID{}, pgtype.UUID{}, fmt.Errorf("%w: %v", ErrInvalidInput, err) + return pgtype.UUID{}, pgtype.UUID{}, fmt.Errorf("%w: %v", sessions.ErrInvalidInput, err) } tenant, err := parseID(principal.TenantID) if err != nil { @@ -37,8 +38,8 @@ func newExecutorSecret() (string, string, error) { return token, hex.EncodeToString(hash[:]), nil } -func issuedExecutorCredential(id, environment pgtype.UUID, token string) IssuedExecutorCredential { - result := IssuedExecutorCredential{KeyID: uuid.UUID(id.Bytes).String(), Token: token} +func issuedExecutorCredential(id, environment pgtype.UUID, token string) sessions.IssuedExecutorCredential { + result := sessions.IssuedExecutorCredential{KeyID: uuid.UUID(id.Bytes).String(), Token: token} if environment.Valid { result.EnvironmentID = uuid.UUID(environment.Bytes).String() } @@ -61,7 +62,7 @@ func (s *Store) withExecutorCredentialTarget(ctx context.Context, principal iden return err } if !row.CreatorKind.Valid || !row.CreatorID.Valid || row.CreatorKind.String != principal.SubjectKind || row.CreatorID.String != principal.SubjectID { - return ErrNotFound + return sessions.ErrNotFound } return apply(ctx, q) }) diff --git a/services/core/internal/store/executor_principals_migration_test.go b/services/core/internal/store/executor_principals_migration_test.go index e5e681527..83b06d872 100644 --- a/services/core/internal/store/executor_principals_migration_test.go +++ b/services/core/internal/store/executor_principals_migration_test.go @@ -13,6 +13,8 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestExecutorPrincipalMigrationRetiresUnknownAuthority(t *testing.T) { @@ -103,16 +105,16 @@ func TestExecutorPrincipalMigrationRetiresUnknownAuthority(t *testing.T) { t.Cleanup(migrated.Close) s := New(migrated) p := FixtureExecutorPrincipal(t, s, tenant) - if _, err := s.AuthenticateEnvironmentExecutor(ctx, environment, digest); !errors.Is(err, ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(ctx, environment, digest); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("legacy credential accepted", err) } - if _, err := s.IssueExecutorCredential(ctx, p, environment, ""); !errors.Is(err, ErrExecutorCredentialExists) { + if _, err := s.IssueExecutorCredential(ctx, p, environment, ""); !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal("legacy key ID claimed", err) } - if _, err := s.RotateExecutorCredential(ctx, p, environment); !errors.Is(err, ErrNotFound) { + if _, err := s.RotateExecutorCredential(ctx, p, environment); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("legacy principal claimed", err) } - if err := s.RevokeExecutorCredential(ctx, p, environment); !errors.Is(err, ErrNotFound) { + if err := s.RevokeExecutorCredential(ctx, p, environment); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("legacy principal manufactured", err) } key, err := s.IssueExecutorCredential(ctx, p, uuid.NewString(), "") diff --git a/services/core/internal/store/executor_principals_test.go b/services/core/internal/store/executor_principals_test.go index 7f453761d..9712a5936 100644 --- a/services/core/internal/store/executor_principals_test.go +++ b/services/core/internal/store/executor_principals_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -21,7 +22,7 @@ func TestExecutorPrincipalBeforeSessionAndSharedLifecycle(t *testing.T) { if err != nil || len(page.Sessions) != 0 { t.Fatal("issuance created a Session", err) } - create := func(principal identity.Principal) (Session, Environment) { + create := func(principal identity.Principal) (sessions.Session, sessions.Environment) { t.Helper() input := environmentInput(uuid.NewString(), "self_hosted", "/workspace") input.Creator = principal.Subject() @@ -35,10 +36,10 @@ func TestExecutorPrincipalBeforeSessionAndSharedLifecycle(t *testing.T) { } return session, environment } - check := func(st *Store, environment string, key IssuedExecutorCredential, allowed bool) { + check := func(st *Store, environment string, key sessions.IssuedExecutorCredential, allowed bool) { t.Helper() owner, err := st.AuthenticateEnvironmentExecutor(ctx, environment, executorDigest(key.Token)) - if allowed && (err != nil || owner != p.TenantID) || !allowed && !errors.Is(err, ErrNotFound) { + if allowed && (err != nil || owner != p.TenantID) || !allowed && !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unexpected principal authorization", allowed, err) } } @@ -54,13 +55,13 @@ func TestExecutorPrincipalBeforeSessionAndSharedLifecycle(t *testing.T) { for _, different := range []identity.Principal{otherKind, otherID, foreign} { _, target := create(different) check(s, target.ID, issued, false) - if _, err := s.IssueExecutorCredential(ctx, p, uuid.NewString(), target.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueExecutorCredential(ctx, p, uuid.NewString(), target.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("restricted key accepted another creator/project", err) } - if _, err := s.RotateExecutorCredential(ctx, different, keyID); !errors.Is(err, ErrNotFound) { + if _, err := s.RotateExecutorCredential(ctx, different, keyID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign rotation", err) } - if err := s.RevokeExecutorCredential(ctx, different, keyID); !errors.Is(err, ErrNotFound) { + if err := s.RevokeExecutorCredential(ctx, different, keyID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign revocation", err) } } @@ -68,13 +69,13 @@ func TestExecutorPrincipalBeforeSessionAndSharedLifecycle(t *testing.T) { {ProjectScope: identity.ProjectScope{TenantID: p.TenantID, OrganizationID: "other-org", ProjectID: p.ProjectID}, SubjectKind: p.SubjectKind, SubjectID: p.SubjectID}, {ProjectScope: identity.ProjectScope{TenantID: p.TenantID, OrganizationID: p.OrganizationID, ProjectID: "other-project"}, SubjectKind: p.SubjectKind, SubjectID: p.SubjectID}, } { - if _, err := s.IssueExecutorCredential(ctx, different, uuid.NewString(), ""); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueExecutorCredential(ctx, different, uuid.NewString(), ""); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unverified scope issuance", err) } - if _, err := s.RotateExecutorCredential(ctx, different, keyID); !errors.Is(err, ErrNotFound) { + if _, err := s.RotateExecutorCredential(ctx, different, keyID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unverified scope rotation", err) } - if err := s.RevokeExecutorCredential(ctx, different, keyID); !errors.Is(err, ErrNotFound) { + if err := s.RevokeExecutorCredential(ctx, different, keyID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unverified scope revocation", err) } } @@ -113,7 +114,7 @@ func TestExecutorPrincipalBeforeSessionAndSharedLifecycle(t *testing.T) { t.Fatal(err) } check(restarted, two.ID, rotated, false) - if _, err := restarted.IssueExecutorCredential(ctx, p, keyID, ""); !errors.Is(err, ErrExecutorCredentialExists) { + if _, err := restarted.IssueExecutorCredential(ctx, p, keyID, ""); !errors.Is(err, sessions.ErrExecutorCredentialExists) { t.Fatal("issue restored revoked authority", err) } restored, err := restarted.RotateExecutorCredential(ctx, p, keyID) @@ -127,14 +128,14 @@ func TestExecutorPrincipalRequiresVerifiedScopeAndRecordedCreator(t *testing.T) s, pool := testStore(t) ctx := t.Context() p := identity.Principal{ProjectScope: identity.ProjectScope{TenantID: uuid.NewString(), OrganizationID: "org", ProjectID: uuid.NewString()}, SubjectKind: "user", SubjectID: "owner"} - if _, err := s.IssueExecutorCredential(ctx, p, uuid.NewString(), ""); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueExecutorCredential(ctx, p, uuid.NewString(), ""); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("issuer manufactured a project mapping", err) } if err := s.EnsureProjectScopes(ctx, []identity.ProjectScope{p.ProjectScope}); err != nil { t.Fatal(err) } for _, invalid := range []identity.Principal{{}, {ProjectScope: p.ProjectScope}, {ProjectScope: p.ProjectScope, SubjectKind: "workspace", SubjectID: "owner"}} { - if _, err := s.IssueExecutorCredential(ctx, invalid, uuid.NewString(), ""); !errors.Is(err, ErrInvalidInput) { + if _, err := s.IssueExecutorCredential(ctx, invalid, uuid.NewString(), ""); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("invalid principal", err) } } @@ -158,10 +159,10 @@ func TestExecutorPrincipalRequiresVerifiedScopeAndRecordedCreator(t *testing.T) if _, err := pool.Exec(ctx, "UPDATE sessions SET creator_kind=NULL,creator_id=NULL WHERE id=$1", session.ID); err != nil { t.Fatal(err) } - if _, err := s.AuthenticateEnvironmentExecutor(ctx, target.ID, executorDigest(key.Token)); !errors.Is(err, ErrNotFound) { + if _, err := s.AuthenticateEnvironmentExecutor(ctx, target.ID, executorDigest(key.Token)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unknown creator accepted", err) } - if _, err := s.IssueExecutorCredential(ctx, p, uuid.NewString(), target.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.IssueExecutorCredential(ctx, p, uuid.NewString(), target.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unknown creator claimed", err) } } diff --git a/services/core/internal/store/export_test.go b/services/core/internal/store/export_test.go index dbda66773..5c1051e26 100644 --- a/services/core/internal/store/export_test.go +++ b/services/core/internal/store/export_test.go @@ -8,6 +8,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5/pgxpool" "testing" ) @@ -39,7 +40,7 @@ func FixtureModelProvider(harness string) *v1.ModelProviderInput { // WithFixtureModelProvider adds the fixture provider, as a Session-supplied // bundle, to a hosted or self-hosted creation that has none. The store must // have a credential key; other inputs are returned unchanged. -func WithFixtureModelProvider(input CreateSessionInput) CreateSessionInput { +func WithFixtureModelProvider(input sessions.CreateSession) sessions.CreateSession { var configuration struct { Environment struct { Type string `json:"type"` diff --git a/services/core/internal/store/function_calls.go b/services/core/internal/store/function_calls.go index adbbb5d52..32c8a13d9 100644 --- a/services/core/internal/store/function_calls.go +++ b/services/core/internal/store/function_calls.go @@ -12,27 +12,19 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -// FunctionCall retains public identity and its opaque execution-adapter reference. -type FunctionCall struct { - CallID, ExecutorCallID, Name string - Arguments json.RawMessage - Result json.RawMessage - Applied bool -} - // RecordFunctionCall commits an execution callback and its required-action state together. -func (s *Store) RecordFunctionCall(ctx context.Context, tenantID, sessionID, turnID string, call FunctionCall) error { +func (s *Store) RecordFunctionCall(ctx context.Context, tenantID, sessionID, turnID string, call sessions.FunctionCall) error { p, err := turnLookup(tenantID, sessionID, turnID) if err != nil { return err } if !validFunctionIdentity(call.CallID) || !validFunctionIdentity(call.ExecutorCallID) || !validFunctionIdentity(call.Name) || len(call.Arguments) > 512*1024 || !json.Valid(call.Arguments) || call.Result != nil || call.Applied { - return ErrInvalidInput + return sessions.ErrInvalidInput } return s.withSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { turn, err := q.GetTurn(ctx, p) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -40,7 +32,7 @@ func (s *Store) RecordFunctionCall(ctx context.Context, tenantID, sessionID, tur matches, err := q.MatchFunctionCall(ctx, sqlc.MatchFunctionCallParams{SessionID: session, TurnID: p.ID, CallID: call.CallID, ExecutorCallID: call.ExecutorCallID, Name: call.Name, Arguments: call.Arguments}) if err == nil { if !matches { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } return nil } @@ -48,47 +40,47 @@ func (s *Store) RecordFunctionCall(ctx context.Context, tenantID, sessionID, tur return err } if !acceptsFunctionResult(turn) { - return ErrTurnConflict + return sessions.ErrTurnConflict } count, err := q.CreateFunctionCall(ctx, sqlc.CreateFunctionCallParams{SessionID: session, TurnID: p.ID, CallID: call.CallID, ExecutorCallID: call.ExecutorCallID, Name: call.Name, Arguments: call.Arguments}) if err != nil { return err } if count != 1 { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } return recordFunctionState(ctx, q, turn) }) } -func (s *Store) GetFunctionCall(ctx context.Context, tenantID, sessionID, turnID, callID string) (FunctionCall, error) { +func (s *Store) GetFunctionCall(ctx context.Context, tenantID, sessionID, turnID, callID string) (sessions.FunctionCall, error) { p, err := turnLookup(tenantID, sessionID, turnID) if err != nil { - return FunctionCall{}, err + return sessions.FunctionCall{}, err } if !validFunctionIdentity(callID) { - return FunctionCall{}, ErrInvalidInput + return sessions.FunctionCall{}, sessions.ErrInvalidInput } row, err := s.queries.GetFunctionCall(ctx, sqlc.GetFunctionCallParams{TenantID: p.TenantID, SessionID: p.SessionID, TurnID: p.ID, CallID: callID}) if errors.Is(err, pgx.ErrNoRows) { - return FunctionCall{}, ErrNotFound + return sessions.FunctionCall{}, sessions.ErrNotFound } if err != nil { - return FunctionCall{}, err + return sessions.FunctionCall{}, err } return functionCallFromRow(row), nil } // PendingFunctionCalls excludes applied results and cancelling or terminal Turns. -func (s *Store) PendingFunctionCalls(ctx context.Context, tenantID, sessionID, turnID string) ([]FunctionCall, error) { +func (s *Store) PendingFunctionCalls(ctx context.Context, tenantID, sessionID, turnID string) ([]sessions.FunctionCall, error) { p, err := turnLookup(tenantID, sessionID, turnID) if err != nil { return nil, err } - result := make([]FunctionCall, 0) + result := make([]sessions.FunctionCall, 0) err = s.withSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { if _, err := q.GetTurn(ctx, p); errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } else if err != nil { return err } @@ -104,8 +96,8 @@ func (s *Store) PendingFunctionCalls(ctx context.Context, tenantID, sessionID, t return result, err } -func functionCallFromRow(row sqlc.FunctionCall) FunctionCall { - return FunctionCall{CallID: row.CallID, ExecutorCallID: row.ExecutorCallID, Name: row.Name, Arguments: row.Arguments, Result: row.Result, Applied: row.Applied} +func functionCallFromRow(row sqlc.FunctionCall) sessions.FunctionCall { + return sessions.FunctionCall{CallID: row.CallID, ExecutorCallID: row.ExecutorCallID, Name: row.Name, Arguments: row.Arguments, Result: row.Result, Applied: row.Applied} } func validFunctionIdentity(id string) bool { return strings.TrimSpace(id) != "" && len(id) <= 512 } diff --git a/services/core/internal/store/function_calls_test.go b/services/core/internal/store/function_calls_test.go index 3ecee374c..446ac5957 100644 --- a/services/core/internal/store/function_calls_test.go +++ b/services/core/internal/store/function_calls_test.go @@ -15,8 +15,8 @@ import ( "github.com/google/uuid" ) -func functionCallFixture(id string) FunctionCall { - return FunctionCall{CallID: id, ExecutorCallID: "native-" + id, Name: "lookup", Arguments: json.RawMessage(`{"ticket":9007199254740993}`)} +func functionCallFixture(id string) sessions.FunctionCall { + return sessions.FunctionCall{CallID: id, ExecutorCallID: "native-" + id, Name: "lookup", Arguments: json.RawMessage(`{"ticket":9007199254740993}`)} } func TestFunctionCallsPersistCompleteResultsAndReceipts(t *testing.T) { @@ -43,7 +43,7 @@ func TestFunctionCallsPersistCompleteResultsAndReceipts(t *testing.T) { if err := s.RecordFunctionCall(t.Context(), tenant, session.ID, turn, retry); err != nil { t.Fatal(err) } - if err := s.ConfirmFunctionResult(t.Context(), tenant, session.ID, turn, call.CallID); !errors.Is(err, ErrTurnConflict) { + if err := s.ConfirmFunctionResult(t.Context(), tenant, session.ID, turn, call.CallID); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("unsubmitted result applied", err) } if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, call.CallID, json.RawMessage(raw)); err != nil { @@ -74,7 +74,7 @@ func TestFunctionCallsPersistCompleteResultsAndReceipts(t *testing.T) { if err := reopened.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, id, json.RawMessage(expected)); err != nil { t.Fatal(err) } - if err := reopened.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, id, json.RawMessage(`{"success":false,"error":"changed"}`)); !errors.Is(err, ErrFunctionResultConflict) { + if err := reopened.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, id, json.RawMessage(`{"success":false,"error":"changed"}`)); !errors.Is(err, sessions.ErrFunctionResultConflict) { t.Fatal(err) } for range 2 { @@ -99,7 +99,7 @@ func TestFunctionCallsAreScopedAndImmutable(t *testing.T) { tenant, session := newTurnSession(t, s) turn := submitMessage(t, s, tenant, session.ID, "start").TurnID call := functionCallFixture("call") - if err := s.RecordFunctionCall(t.Context(), tenant, session.ID, turn, call); !errors.Is(err, ErrTurnConflict) { + if err := s.RecordFunctionCall(t.Context(), tenant, session.ID, turn, call); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("queued turn accepted callback", err) } transition(t, s, tenant, session.ID, turn, sessions.TurnQueued, sessions.TurnInProgress) @@ -118,7 +118,7 @@ func TestFunctionCallsAreScopedAndImmutable(t *testing.T) { case "arguments": changed.Arguments = json.RawMessage(`{"ticket":9007199254740992}`) } - if err := s.RecordFunctionCall(t.Context(), tenant, session.ID, turn, changed); !errors.Is(err, ErrIdempotencyConflict) { + if err := s.RecordFunctionCall(t.Context(), tenant, session.ID, turn, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal(field, err) } } @@ -126,23 +126,23 @@ func TestFunctionCallsAreScopedAndImmutable(t *testing.T) { {uuid.NewString(), session.ID, turn}, {tenant, uuid.NewString(), turn}, {tenant, session.ID, uuid.NewString()}, } { _, err := s.GetFunctionCall(t.Context(), scope.tenant, scope.session, scope.turn, "call") - if !errors.Is(err, ErrNotFound) { + if !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := s.PendingFunctionCalls(t.Context(), scope.tenant, scope.session, scope.turn); !errors.Is(err, ErrNotFound) { + if _, err := s.PendingFunctionCalls(t.Context(), scope.tenant, scope.session, scope.turn); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if err := s.RecordFunctionCall(t.Context(), scope.tenant, scope.session, scope.turn, call); !errors.Is(err, ErrNotFound) { + if err := s.RecordFunctionCall(t.Context(), scope.tenant, scope.session, scope.turn, call); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if err := s.SubmitFunctionResult(t.Context(), scope.tenant, scope.session, scope.turn, "call", json.RawMessage(`{"success":true}`)); !errors.Is(err, ErrNotFound) { + if err := s.SubmitFunctionResult(t.Context(), scope.tenant, scope.session, scope.turn, "call", json.RawMessage(`{"success":true}`)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if err := s.ConfirmFunctionResult(t.Context(), scope.tenant, scope.session, scope.turn, "call"); !errors.Is(err, ErrNotFound) { + if err := s.ConfirmFunctionResult(t.Context(), scope.tenant, scope.session, scope.turn, "call"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } - if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "missing", json.RawMessage(`{"success":true}`)); !errors.Is(err, ErrNotFound) { + if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "missing", json.RawMessage(`{"success":true}`)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } @@ -168,22 +168,22 @@ func TestFunctionResultsCannotApplyAfterCancellationOrCompletion(t *testing.T) { t.Fatal(err) } assertNoPendingFunctions(t, s, tenant, session.ID, turn) - if err := s.ConfirmFunctionResult(t.Context(), tenant, session.ID, turn, "submitted"); !errors.Is(err, ErrTurnConflict) { + if err := s.ConfirmFunctionResult(t.Context(), tenant, session.ID, turn, "submitted"); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } - if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "pending", result); !errors.Is(err, ErrTurnConflict) { + if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "pending", result); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } } transition(t, s, tenant, session.ID, turn, sessions.TurnWaiting, terminal) assertNoPendingFunctions(t, s, tenant, session.ID, turn) - if err := s.ConfirmFunctionResult(t.Context(), tenant, session.ID, turn, "submitted"); !errors.Is(err, ErrTurnConflict) { + if err := s.ConfirmFunctionResult(t.Context(), tenant, session.ID, turn, "submitted"); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } - if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "pending", result); !errors.Is(err, ErrTurnConflict) { + if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "pending", result); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } - if err := s.RecordFunctionCall(t.Context(), tenant, session.ID, turn, functionCallFixture("late")); !errors.Is(err, ErrTurnConflict) { + if err := s.RecordFunctionCall(t.Context(), tenant, session.ID, turn, functionCallFixture("late")); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "submitted", result); err != nil { @@ -235,7 +235,7 @@ func TestFunctionResultConcurrentSubmissionsChooseOneValue(t *testing.T) { for err := range outcomes { if err == nil { winners++ - } else if errors.Is(err, ErrFunctionResultConflict) { + } else if errors.Is(err, sessions.ErrFunctionResultConflict) { conflicts++ } else { t.Fatal(err) @@ -255,7 +255,7 @@ func TestFunctionResultInvalidStorageInputDoesNotConsumeCall(t *testing.T) { t.Fatal(err) } for _, raw := range []string{"", "null", "[]", "{} {}", `{"output":"` + strings.Repeat("a", 512*1024) + `"}`} { - if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "call", json.RawMessage(raw)); !errors.Is(err, ErrInvalidInput) { + if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, "call", json.RawMessage(raw)); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal(err) } } diff --git a/services/core/internal/store/function_execution_native_test.go b/services/core/internal/store/function_execution_native_test.go index f4c773b84..1ee45d958 100644 --- a/services/core/internal/store/function_execution_native_test.go +++ b/services/core/internal/store/function_execution_native_test.go @@ -15,7 +15,7 @@ func TestNativeFunctionExecutionPersistsCallsResultsAndContinuity(t *testing.T) model, output, requests := nativeFunctionModel(t, home) defer model.Close() var err error - h.session, err = h.s.CreateSession(ctx, h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "native-functions", Configuration: json.RawMessage(functionConfiguration), ModelProvider: nativeModelProvider(model), ModelProviderSource: v1.ModelProviderSourceDeployment}) + h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "native-functions", Configuration: json.RawMessage(functionConfiguration), ModelProvider: nativeModelProvider(model), ModelProviderSource: v1.ModelProviderSourceDeployment}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/function_execution_test.go b/services/core/internal/store/function_execution_test.go index ef3f85063..199dee6ec 100644 --- a/services/core/internal/store/function_execution_test.go +++ b/services/core/internal/store/function_execution_test.go @@ -21,7 +21,7 @@ func newFunctionHarness(t *testing.T) *dispatchHarness { t.Helper() h := newDispatchHarness(t) var err error - h.session, err = h.s.CreateSession(t.Context(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)}) + h.session, err = h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)}) if err != nil { t.Fatal(err) } @@ -43,7 +43,7 @@ func newFunctionHarness(t *testing.T) *dispatchHarness { } } -func functionState(t *testing.T, h *dispatchHarness, count int) store.Session { +func functionState(t *testing.T, h *dispatchHarness, count int) sessions.Session { t.Helper() deadline := time.Now().Add(20 * time.Second) for { @@ -164,7 +164,7 @@ func TestExecutionFunctionsCancellationAndUnconfirmedResults(t *testing.T) { if err != nil || saved.Applied || len(saved.Result) == 0 { t.Fatal(saved, err) } - if err := h.s.ConfirmFunctionResult(t.Context(), h.tenant, h.session.ID, input.TurnID, id); !errors.Is(err, store.ErrTurnConflict) { + if err := h.s.ConfirmFunctionResult(t.Context(), h.tenant, h.session.ID, input.TurnID, id); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } functionState(t, h, 0) @@ -190,7 +190,7 @@ func TestExecutionFunctionsRejectUndeclaredCallsAndPrematureDone(t *testing.T) { func TestExecutionFunctionsRequireAdvertisedCapability(t *testing.T) { h := newDispatchHarness(t) - session, err := h.s.CreateSession(t.Context(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)}) + session, err := h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/function_input_execution_test.go b/services/core/internal/store/function_input_execution_test.go index 5975ab697..09444f86d 100644 --- a/services/core/internal/store/function_input_execution_test.go +++ b/services/core/internal/store/function_input_execution_test.go @@ -8,7 +8,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestExecutionFunctionInputBatchStillSteersMessages(t *testing.T) { @@ -18,8 +17,8 @@ func TestExecutionFunctionInputBatchStillSteersMessages(t *testing.T) { h.read(testExecutionRequest) h.write(input.TurnID, proto.TypeFunctionCall, proto.FunctionCallPayload{CallID: "a", Name: "lookup_ticket", Arguments: json.RawMessage(`{}`)}) state := functionState(t, h, 1) - raw, _ := json.Marshal(store.FunctionResultInput{TurnID: input.TurnID, CallID: state.RequiredActions[0].CallID, Result: json.RawMessage(`{"success":true,"output":"answer"}`)}) - batch := []store.Input{{Kind: "tool_result", Payload: raw}, {Kind: "message", Payload: json.RawMessage(`{"text":"Follow up"}`)}} + raw, _ := json.Marshal(sessions.FunctionResultInput{TurnID: input.TurnID, CallID: state.RequiredActions[0].CallID, Result: json.RawMessage(`{"success":true,"output":"answer"}`)}) + batch := []sessions.Input{{Kind: "tool_result", Payload: raw}, {Kind: "message", Payload: json.RawMessage(`{"text":"Follow up"}`)}} receipts, err := h.s.SubmitInputs(t.Context(), h.tenant, h.session.ID, "mixed", batch) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/function_inputs.go b/services/core/internal/store/function_inputs.go index a685e75b6..4da8ad754 100644 --- a/services/core/internal/store/function_inputs.go +++ b/services/core/internal/store/function_inputs.go @@ -8,51 +8,32 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -// Result targets are resolved only inside a tenant-owned Session, after its -// lookup, so a missing or foreign Session still returns ErrNotFound (EVT-11). -var ( - // ErrUnknownFunctionCall rejects a result whose call_id names no function - // call in the Session. - ErrUnknownFunctionCall = errors.New("unknown pending tool call") - // ErrFunctionCallTurnMismatch rejects a result whose call exists in the - // Session but not in the named Turn, including a malformed or unknown Turn. - ErrFunctionCallTurnMismatch = errors.New("tool call belongs to a different turn") -) - -// FunctionResultInput identifies a persisted call; Result is validated by the API. -// It is an internal command, not an upstream input event. TurnID is the caller's -// value and is resolved within the Session at admission. -type FunctionResultInput struct { - TurnID string `json:"turn_id"` - CallID string `json:"call_id"` - Result json.RawMessage `json:"result"` -} - -func functionInput(raw json.RawMessage) (FunctionResultInput, error) { - var input FunctionResultInput +func functionInput(raw json.RawMessage) (sessions.FunctionResultInput, error) { + var input sessions.FunctionResultInput if json.Unmarshal(raw, &input) != nil || input.TurnID == "" || !validFunctionIdentity(input.CallID) || len(input.Result) == 0 { - return input, ErrInvalidInput + return input, sessions.ErrInvalidInput } result, err := jsonobject.Normalize(input.Result) if err != nil { - return input, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return input, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } input.Result = result return input, nil } -func admitFunctionResult(ctx context.Context, q *sqlc.Queries, tenantID string, session pgtype.UUID, key string, position int32, input Input) (InputReceipt, error) { +func admitFunctionResult(ctx context.Context, q *sqlc.Queries, tenantID string, session pgtype.UUID, key string, position int32, input sessions.Input) (sessions.InputReceipt, error) { result, err := functionInput(input.Payload) if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } tenant, err := parseID(tenantID) if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } var turn sqlc.Turn found := false @@ -62,25 +43,25 @@ func admitFunctionResult(ctx context.Context, q *sqlc.Queries, tenantID string, if err == nil { found = true } else if !errors.Is(err, pgx.ErrNoRows) { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } } if found { err = storeFunctionResult(ctx, q, turn, result.CallID, result.Result) - if errors.Is(err, ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { found = false } else if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } } if !found { - return InputReceipt{}, unknownFunctionResultTarget(ctx, q, session, result.CallID) + return sessions.InputReceipt{}, unknownFunctionResultTarget(ctx, q, session, result.CallID) } sequence, err := q.CreateTurnInput(ctx, sqlc.CreateTurnInputParams{ SessionID: session, TurnID: turn.ID, IdempotencyKey: key, Kind: input.Kind, Payload: input.Payload, BatchPosition: position, }) if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } return inputReceipt(sequence, turn.ID, false), nil } @@ -93,7 +74,7 @@ func unknownFunctionResultTarget(ctx context.Context, q *sqlc.Queries, session p return err } if elsewhere { - return ErrFunctionCallTurnMismatch + return sessions.ErrFunctionCallTurnMismatch } - return ErrUnknownFunctionCall + return sessions.ErrUnknownFunctionCall } diff --git a/services/core/internal/store/function_inputs_public_test.go b/services/core/internal/store/function_inputs_public_test.go index 665a9e747..005acb0c9 100644 --- a/services/core/internal/store/function_inputs_public_test.go +++ b/services/core/internal/store/function_inputs_public_test.go @@ -26,11 +26,11 @@ func TestFunctionInputsOfficialClientAtomicAdmission(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "fixture"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "fixture"}) if err != nil { t.Fatal(err) } - other, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) + other, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) if err != nil { t.Fatal(err) } @@ -38,11 +38,11 @@ func TestFunctionInputsOfficialClientAtomicAdmission(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } for _, id := range []string{"a", "b", "c", "rollback", "late"} { - if err := s.RecordFunctionCall(ctx, tenant, session.ID, input.TurnID, store.FunctionCall{CallID: id, ExecutorCallID: "native-" + id, Name: "lookup", Arguments: json.RawMessage(`{}`)}); err != nil { + if err := s.RecordFunctionCall(ctx, tenant, session.ID, input.TurnID, sessions.FunctionCall{CallID: id, ExecutorCallID: "native-" + id, Name: "lookup", Arguments: json.RawMessage(`{}`)}); err != nil { t.Fatal(err) } } @@ -107,7 +107,7 @@ func TestFunctionInputsOfficialClientAtomicAdmission(t *testing.T) { if err != nil || len(history) != 6 { t.Fatal(history, err) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnWaiting, Status: sessions.TurnFailed}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnWaiting, Status: sessions.TurnFailed}); err != nil { t.Fatal(err) } next, err := s.SubmitMessage(ctx, tenant, session.ID, "next", json.RawMessage(`{"text":"next"}`)) diff --git a/services/core/internal/store/function_inputs_test.go b/services/core/internal/store/function_inputs_test.go index 23361ba75..bdb6b4179 100644 --- a/services/core/internal/store/function_inputs_test.go +++ b/services/core/internal/store/function_inputs_test.go @@ -13,16 +13,16 @@ import ( "github.com/google/uuid" ) -func resultInput(t *testing.T, turn, call, result string) Input { +func resultInput(t *testing.T, turn, call, result string) sessions.Input { t.Helper() - raw, err := json.Marshal(FunctionResultInput{TurnID: turn, CallID: call, Result: json.RawMessage(result)}) + raw, err := json.Marshal(sessions.FunctionResultInput{TurnID: turn, CallID: call, Result: json.RawMessage(result)}) if err != nil { t.Fatal(err) } - return Input{Kind: "tool_result", Payload: raw} + return sessions.Input{Kind: "tool_result", Payload: raw} } -func functionInputFixture(t *testing.T, s *Store) (string, Session, string) { +func functionInputFixture(t *testing.T, s *Store) (string, sessions.Session, string) { t.Helper() tenant, session := newTurnSession(t, s) turn := submitMessage(t, s, tenant, session.ID, "start").TurnID @@ -39,7 +39,7 @@ func TestFunctionInputBatchesPersistAndReplayWithoutRetargeting(t *testing.T) { s, pool := testStore(t) tenant, session, turn := functionInputFixture(t, s) full := `{"success":false,"output":[{"type":"input_text","text":""},{"type":"input_image","image_url":"data:image/png;base64,AA=="},{"type":"input_text","text":"after"}],"error":"failed"}` - batch := []Input{resultInput(t, turn, "a", full), {Kind: "message", Payload: json.RawMessage(`{"text":"Follow up"}`)}, resultInput(t, turn, "b", `{"success":true,"output":null,"error":null}`), {Kind: "cancel", Payload: json.RawMessage(`{}`)}} + batch := []sessions.Input{resultInput(t, turn, "a", full), {Kind: "message", Payload: json.RawMessage(`{"text":"Follow up"}`)}, resultInput(t, turn, "b", `{"success":true,"output":null,"error":null}`), {Kind: "cancel", Payload: json.RawMessage(`{}`)}} receipts, err := s.SubmitInputs(t.Context(), tenant, session.ID, "batch", batch) if err != nil || len(receipts) != 4 { t.Fatal(receipts, err) @@ -85,8 +85,8 @@ func TestFunctionInputBatchesPersistAndReplayWithoutRetargeting(t *testing.T) { if err != nil || !current.CancelRequestedAt.IsZero() || current.Status != sessions.TurnQueued { t.Fatal(current, err) } - changed := []Input{batch[1], batch[0], batch[2], batch[3]} - if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "batch", changed); !errors.Is(err, ErrIdempotencyConflict) { + changed := []sessions.Input{batch[1], batch[0], batch[2], batch[3]} + if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "batch", changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal(err) } // A new request identity can repeat an identical saved result, without native application. @@ -104,11 +104,11 @@ func TestFunctionInputBatchFailureRollsBackEveryWrite(t *testing.T) { t.Run(mode, func(t *testing.T) { s, _ := testStore(t) tenant, session, turn := functionInputFixture(t, s) - message := Input{Kind: "message", Payload: json.RawMessage(`{"text":"Must roll back"}`)} - cancel := Input{Kind: "cancel", Payload: json.RawMessage(`{}`)} + message := sessions.Input{Kind: "message", Payload: json.RawMessage(`{"text":"Must roll back"}`)} + cancel := sessions.Input{Kind: "cancel", Payload: json.RawMessage(`{}`)} first := resultInput(t, turn, "a", `{"success":true}`) - batch := []Input{message, first, cancel} - expected := ErrUnknownFunctionCall + batch := []sessions.Input{message, first, cancel} + expected := sessions.ErrUnknownFunctionCall switch mode { case "missing-call": batch = append(batch, resultInput(t, turn, "missing", `{"success":true}`)) @@ -117,7 +117,7 @@ func TestFunctionInputBatchFailureRollsBackEveryWrite(t *testing.T) { if mode == "foreign-turn" { otherTenant = uuid.NewString() } - other, err := s.CreateSession(t.Context(), otherTenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) + other, err := s.CreateSession(t.Context(), otherTenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) if err != nil { t.Fatal(err) } @@ -127,13 +127,13 @@ func TestFunctionInputBatchFailureRollsBackEveryWrite(t *testing.T) { t.Fatal(err) } batch = append(batch, resultInput(t, otherTurn, "a", `{"success":true}`)) - expected = ErrFunctionCallTurnMismatch + expected = sessions.ErrFunctionCallTurnMismatch case "cancel-first": - batch = []Input{message, cancel, first} - expected = ErrTurnConflict + batch = []sessions.Input{message, cancel, first} + expected = sessions.ErrTurnConflict case "changed-result": batch = append(batch, resultInput(t, turn, "a", `{"success":false}`)) - expected = ErrFunctionResultConflict + expected = sessions.ErrFunctionResultConflict } if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "failed-batch", batch); !errors.Is(err, expected) { t.Fatal(err) @@ -150,7 +150,7 @@ func TestFunctionInputBatchFailureRollsBackEveryWrite(t *testing.T) { if err != nil || len(history) != 1 { t.Fatal(history, err) } - if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "failed-batch", []Input{first, cancel}); err != nil { + if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "failed-batch", []sessions.Input{first, cancel}); err != nil { t.Fatal("failed transaction retained retry identity", err) } }) @@ -164,7 +164,7 @@ func TestFunctionInputConcurrentBatchesSelectOneResult(t *testing.T) { var wg sync.WaitGroup results := make(chan error, 2) for i := range 2 { - batch := []Input{{Kind: "message", Payload: json.RawMessage(fmt.Sprintf(`{"text":"message-%d"}`, i))}, resultInput(t, turn, "a", fmt.Sprintf(`{"success":true,"output":"%d"}`, i))} + batch := []sessions.Input{{Kind: "message", Payload: json.RawMessage(fmt.Sprintf(`{"text":"message-%d"}`, i))}, resultInput(t, turn, "a", fmt.Sprintf(`{"success":true,"output":"%d"}`, i))} wg.Add(1) go func() { defer wg.Done() @@ -178,7 +178,7 @@ func TestFunctionInputConcurrentBatchesSelectOneResult(t *testing.T) { for err := range results { if err == nil { wins++ - } else if errors.Is(err, ErrFunctionResultConflict) { + } else if errors.Is(err, sessions.ErrFunctionResultConflict) { conflicts++ } else { t.Fatal(err) @@ -197,16 +197,16 @@ func TestFunctionInputsRejectInvalidTargetsAndStorageObjects(t *testing.T) { s, _ := testStore(t) tenant, session, turn := functionInputFixture(t, s) for _, raw := range []string{`{}`, `{"turn_id":"","call_id":"a","result":{}}`, fmt.Sprintf(`{"turn_id":%q,"call_id":" ","result":{}}`, turn), fmt.Sprintf(`{"turn_id":%q,"call_id":"a"}`, turn), fmt.Sprintf(`{"turn_id":%q,"call_id":"a","result":null}`, turn), fmt.Sprintf(`{"turn_id":%q,"call_id":"a","result":[]}`, turn)} { - _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "invalid", []Input{{Kind: "tool_result", Payload: json.RawMessage(raw)}}) - if !errors.Is(err, ErrInvalidInput) { + _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "invalid", []sessions.Input{{Kind: "tool_result", Payload: json.RawMessage(raw)}}) + if !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal(err) } } input := resultInput(t, turn, "a", `{"success":true}`) // Missing, foreign and malformed Sessions are not found, whatever the target. for _, scope := range []struct{ tenant, session string }{{uuid.NewString(), session.ID}, {tenant, uuid.NewString()}, {tenant, "sess_malformed"}} { - for _, target := range []Input{input, resultInput(t, "bad", "missing", `{"success":true}`)} { - if _, err := s.SubmitInputs(t.Context(), scope.tenant, scope.session, "foreign", []Input{target}); !errors.Is(err, ErrNotFound) { + for _, target := range []sessions.Input{input, resultInput(t, "bad", "missing", `{"success":true}`)} { + if _, err := s.SubmitInputs(t.Context(), scope.tenant, scope.session, "foreign", []sessions.Input{target}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } @@ -217,15 +217,15 @@ func TestFunctionInputsRejectInvalidTargetsAndStorageObjects(t *testing.T) { turn, call string want error }{ - {uuid.NewString(), "a", ErrFunctionCallTurnMismatch}, {"bad", "a", ErrFunctionCallTurnMismatch}, - {turn, "missing", ErrUnknownFunctionCall}, {uuid.NewString(), "missing", ErrUnknownFunctionCall}, {"bad", "missing", ErrUnknownFunctionCall}, + {uuid.NewString(), "a", sessions.ErrFunctionCallTurnMismatch}, {"bad", "a", sessions.ErrFunctionCallTurnMismatch}, + {turn, "missing", sessions.ErrUnknownFunctionCall}, {uuid.NewString(), "missing", sessions.ErrUnknownFunctionCall}, {"bad", "missing", sessions.ErrUnknownFunctionCall}, } { - if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "target", []Input{resultInput(t, target.turn, target.call, `{"success":true}`)}); !errors.Is(err, target.want) { + if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "target", []sessions.Input{resultInput(t, target.turn, target.call, `{"success":true}`)}); !errors.Is(err, target.want) { t.Fatal(target, err) } } transition(t, s, tenant, session.ID, turn, sessions.TurnWaiting, sessions.TurnFailed) - if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "late", []Input{input}); !errors.Is(err, ErrTurnConflict) { + if _, err := s.SubmitInputs(t.Context(), tenant, session.ID, "late", []sessions.Input{input}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } current, err := s.GetSession(t.Context(), tenant, session.ID) diff --git a/services/core/internal/store/function_item_events_test.go b/services/core/internal/store/function_item_events_test.go index 7220af875..e5b601950 100644 --- a/services/core/internal/store/function_item_events_test.go +++ b/services/core/internal/store/function_item_events_test.go @@ -13,7 +13,7 @@ func TestFunctionResultEventsAreInputs(t *testing.T) { tenant, session := newTurnSession(t, s) turn := submitMessage(t, s, tenant, session.ID, "start").TurnID transition(t, s, tenant, session.ID, turn, sessions.TurnQueued, sessions.TurnInProgress) - events := []ExecutionEvent{ + events := []sessions.ExecutionEvent{ {Kind: "tool_call", Payload: json.RawMessage(`{"id":"call","stage":"after","observation":{"status":"completed","kind":"function","name":"lookup","arguments":{},"content":[{"type":"input_text","text":"result"}]}}`)}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"answer","delta":"answer"}`)}, } @@ -74,8 +74,8 @@ func TestFunctionResultItemsRetainSubmittedFields(t *testing.T) { if err := s.SubmitFunctionResult(t.Context(), tenant, session.ID, turn, call.CallID, json.RawMessage(raw)); err != nil { t.Fatal(err) } - event := ExecutionEvent{Kind: "tool_call", Payload: json.RawMessage(`{"id":"call","stage":"after","observation":{"status":"completed","kind":"function","name":"lookup","arguments":{},"content":[{"type":"input_text","text":"normalized"}]}}`)} - if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, turn, 1, []ExecutionEvent{event}); err != nil { + event := sessions.ExecutionEvent{Kind: "tool_call", Payload: json.RawMessage(`{"id":"call","stage":"after","observation":{"status":"completed","kind":"function","name":"lookup","arguments":{},"content":[{"type":"input_text","text":"normalized"}]}}`)} + if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, turn, 1, []sessions.ExecutionEvent{event}); err != nil { t.Fatal(err) } assertFields := func(value any) { diff --git a/services/core/internal/store/function_results.go b/services/core/internal/store/function_results.go index 911650ac5..55283b705 100644 --- a/services/core/internal/store/function_results.go +++ b/services/core/internal/store/function_results.go @@ -8,22 +8,19 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -// ErrFunctionResultConflict rejects a result that differs from the one already -// saved for its call, including after the Turn ended (EVT-12). -var ErrFunctionResultConflict = errors.New("tool call already has a different result") - // SubmitFunctionResult stores a caller-validated result object; its wire schema belongs to the API. func (s *Store) SubmitFunctionResult(ctx context.Context, tenantID, sessionID, turnID, callID string, result json.RawMessage) error { if len(result) == 0 || len(result) > 512*1024 { - return ErrInvalidInput + return sessions.ErrInvalidInput } result, err := jsonobject.Normalize(result) if err != nil { - return fmt.Errorf("%w: %w", ErrInvalidInput, err) + return fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } return s.withFunctionCall(ctx, tenantID, sessionID, turnID, callID, func(ctx context.Context, q *sqlc.Queries, turn sqlc.Turn, call sqlc.FunctionCall) error { return storeFunctionResult(ctx, q, turn, call.CallID, result) @@ -37,7 +34,7 @@ func (s *Store) ConfirmFunctionResult(ctx context.Context, tenantID, sessionID, return nil } if len(call.Result) == 0 || !acceptsFunctionResult(turn) { - return ErrTurnConflict + return sessions.ErrTurnConflict } if err := q.ApplyFunctionResult(ctx, sqlc.ApplyFunctionResultParams{SessionID: turn.SessionID, TurnID: turn.ID, CallID: call.CallID}); err != nil { return err @@ -52,19 +49,19 @@ func (s *Store) withFunctionCall(ctx context.Context, tenantID, sessionID, turnI return err } if !validFunctionIdentity(callID) { - return ErrInvalidInput + return sessions.ErrInvalidInput } return s.withSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { turn, err := q.GetTurn(ctx, p) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } call, err := q.GetFunctionCall(ctx, sqlc.GetFunctionCallParams{TenantID: p.TenantID, SessionID: session, TurnID: p.ID, CallID: callID}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -76,19 +73,19 @@ func (s *Store) withFunctionCall(ctx context.Context, tenantID, sessionID, turnI func storeFunctionResult(ctx context.Context, q *sqlc.Queries, turn sqlc.Turn, callID string, result json.RawMessage) error { match, err := q.MatchFunctionResult(ctx, sqlc.MatchFunctionResultParams{SessionID: turn.SessionID, TurnID: turn.ID, CallID: callID, Result: result}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } if match.Submitted { if !match.Matches { - return ErrFunctionResultConflict + return sessions.ErrFunctionResultConflict } return nil } if !acceptsFunctionResult(turn) { - return ErrTurnConflict + return sessions.ErrTurnConflict } return q.SubmitFunctionResult(ctx, sqlc.SubmitFunctionResultParams{SessionID: turn.SessionID, TurnID: turn.ID, CallID: callID, Result: result}) } diff --git a/services/core/internal/store/function_state_public_test.go b/services/core/internal/store/function_state_public_test.go index 63b974094..af5a21325 100644 --- a/services/core/internal/store/function_state_public_test.go +++ b/services/core/internal/store/function_state_public_test.go @@ -27,7 +27,7 @@ func TestFunctionStateOfficialClientReadsAndLiveEvents(t *testing.T) { defer cancel() tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() cfg := json.RawMessage(`{"agent":{"id":"agent_fixture","model":"fixture","tools":[],"multi_agent":{"enabled":false,"max_concurrent_subagents":null},"reasoning":{},"service_tier":"auto","text":{"format":{"type":"text"},"verbosity":"medium"}},"environment":{"type":"none"}}`) - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "fixture", Configuration: cfg}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "fixture", Configuration: cfg}) if err != nil { t.Fatal(err) } @@ -35,12 +35,12 @@ func TestFunctionStateOfficialClientReadsAndLiveEvents(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } record := func(id string) { t.Helper() - if err := s.RecordFunctionCall(ctx, tenant, session.ID, input.TurnID, store.FunctionCall{CallID: id, ExecutorCallID: "private-" + id, Name: "lookup", Arguments: json.RawMessage(`{"ticket":9007199254740993}`)}); err != nil { + if err := s.RecordFunctionCall(ctx, tenant, session.ID, input.TurnID, sessions.FunctionCall{CallID: id, ExecutorCallID: "private-" + id, Name: "lookup", Arguments: json.RawMessage(`{"ticket":9007199254740993}`)}); err != nil { t.Fatal(err) } } diff --git a/services/core/internal/store/function_state_test.go b/services/core/internal/store/function_state_test.go index 76236ae35..9cb4711a4 100644 --- a/services/core/internal/store/function_state_test.go +++ b/services/core/internal/store/function_state_test.go @@ -37,7 +37,7 @@ func TestFunctionStateSnapshotsRecoveryAndRetries(t *testing.T) { pool.Close() s, _ = testStore(t) assertFunctionState(t, s, tenant, session.ID, sessions.TurnWaiting, 2) - if _, err := s.CompleteExecution(t.Context(), tenant, session.ID, turn, sessions.TurnCompleted, nil, "", 1); !errors.Is(err, ErrTurnConflict) { + if _, err := s.CompleteExecution(t.Context(), tenant, session.ID, turn, sessions.TurnCompleted, nil, "", 1); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("waiting execution completed", err) } for i, id := range []string{"first", "second"} { @@ -75,10 +75,10 @@ func TestFunctionStateSnapshotsRecoveryAndRetries(t *testing.T) { } } } - if _, err := s.GetSession(t.Context(), uuid.NewString(), session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSession(t.Context(), uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := s.ListSessionEvents(t.Context(), uuid.NewString(), session.ID, before); !errors.Is(err, ErrNotFound) { + if _, err := s.ListSessionEvents(t.Context(), uuid.NewString(), session.ID, before); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } diff --git a/services/core/internal/store/function_worker_test.go b/services/core/internal/store/function_worker_test.go index 0279a72a1..4f630b84c 100644 --- a/services/core/internal/store/function_worker_test.go +++ b/services/core/internal/store/function_worker_test.go @@ -37,7 +37,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { } if !prebound || isMCP { var err error - h.session, err = h.s.CreateSession(t.Context(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "unbound", Configuration: []byte(configuration)}) + h.session, err = h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "unbound", Configuration: []byte(configuration)}) if err != nil { t.Fatal(err) } @@ -84,7 +84,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { t.Fatal(current, err) } if !prebound { - if _, err := h.s.GetSessionDevice(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSessionDevice(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("bound an incapable device", err) } } diff --git a/services/core/internal/store/hosted_initialization_failure_public_test.go b/services/core/internal/store/hosted_initialization_failure_public_test.go index 1c2d61174..c34a1bbb2 100644 --- a/services/core/internal/store/hosted_initialization_failure_public_test.go +++ b/services/core/internal/store/hosted_initialization_failure_public_test.go @@ -23,6 +23,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -108,7 +109,7 @@ func hostedFailureStore(t *testing.T) (*store.Store, fixtureDB) { return store.NewWithCredentialCipher(pool, cipher), fixtureDB{pool: pool, cipher: cipher} } -func hostedFailureSession(t *testing.T, s *store.Store, tenant string, input store.CreateSessionInput) (store.Session, store.Environment) { +func hostedFailureSession(t *testing.T, s *store.Store, tenant string, input sessions.CreateSession) (sessions.Session, sessions.Environment) { t.Helper() input.Creator, input.Engine, input.IdempotencyKey = store.FixtureCreator(), "codex", uuid.NewString() input.Configuration = json.RawMessage(`{"agent":{"id":"agent_test","model":"test-model","tools":[]},"environment":{"type":"openai_hosted","network":{"access":"enabled"}}}`) @@ -126,7 +127,7 @@ func hostedFailureSession(t *testing.T, s *store.Store, tenant string, input sto return session, environment } -func failHostedInitialization(t *testing.T, s *store.Store, db fixtureDB, tenant string, environment store.Environment, p *hostedFailureProvider) { +func failHostedInitialization(t *testing.T, s *store.Store, db fixtureDB, tenant string, environment sessions.Environment, p *hostedFailureProvider) { t.Helper() key := uuid.NewString() w, _ := managedWorkerMode(t, s, db, key, p, false, true) @@ -149,33 +150,33 @@ func TestHostedInitializationFailureRecordsSafeSessionFailure(t *testing.T) { } for _, test := range []struct { name string - input store.CreateSessionInput + input sessions.CreateSession p failure reason string steps []string }{ - {"setup exit status", store.CreateSessionInput{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, + {"setup exit status", sessions.CreateSession{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, failure{fail: "setup", result: failedInitialization(3)}, `Failed to provision environment: script "setup_commands[0]" failed with exit code 3`, []string{"configure", "setup"}}, - {"later setup command", store.CreateSessionInput{Initialization: environmentconfig.Setup{Commands: commands}}, + {"later setup command", sessions.CreateSession{Initialization: environmentconfig.Setup{Commands: commands}}, failure{fail: "setup", skip: 1, result: failedInitialization(3)}, `Failed to provision environment: script "setup_commands[1]" failed with exit code 3`, []string{"configure", "setup", "setup"}}, - {"python package", store.CreateSessionInput{Initialization: environmentconfig.Setup{Packages: v1.EnvironmentPackages{Python: []string{"oac-nonexistent-zz"}}, Commands: commands[2:]}}, + {"python package", sessions.CreateSession{Initialization: environmentconfig.Setup{Packages: v1.EnvironmentPackages{Python: []string{"oac-nonexistent-zz"}}, Commands: commands[2:]}}, failure{fail: "python", result: failedInitialization(1)}, `Failed to provision environment: script "Python package installation" failed with exit code 1`, []string{"configure", "python"}}, - {"failure without exit status", store.CreateSessionInput{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, + {"failure without exit status", sessions.CreateSession{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, failure{fail: "setup", result: failedInitialization(0)}, "Failed to provision environment: initialization did not complete", []string{"configure", "setup"}}, - {"unknown effect", store.CreateSessionInput{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, + {"unknown effect", sessions.CreateSession{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, failure{fail: "setup", err: sandbox.ErrCommandUnconfirmed}, "Failed to provision environment: initialization did not complete", []string{"configure", "setup"}}, - {"invalid failure code", store.CreateSessionInput{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, + {"invalid failure code", sessions.CreateSession{Initialization: environmentconfig.Setup{Commands: commands[1:]}}, failure{fail: "setup", result: proto.RuntimePrepareResultPayload{Outcome: "failed", ErrorCode: hostedFailureCanary}}, "Failed to provision environment: initialization did not complete", []string{"configure", "setup"}}, - {"initial file", store.CreateSessionInput{InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/a", Data: []byte(hostedFailureCanary)}}}, + {"initial file", sessions.CreateSession{InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/a", Data: []byte(hostedFailureCanary)}}}, failure{fail: "file", result: failedInitialization(0)}, "Failed to provision environment: initial file installation failed", []string{"file"}}, - {"Skill", store.CreateSessionInput{Initialization: environmentconfig.Setup{Skills: []environmentconfig.Skill{hostedFailureSkill(t)}, Commands: commands[2:]}}, + {"Skill", sessions.CreateSession{Initialization: environmentconfig.Setup{Skills: []environmentconfig.Skill{hostedFailureSkill(t)}, Commands: commands[2:]}}, failure{fail: "skill", result: failedInitialization(0)}, "Failed to provision environment: Skill installation failed", []string{"configure", "skill"}}, } { @@ -220,7 +221,7 @@ func TestHostedInitializationFailureRecordsSafeSessionFailure(t *testing.T) { !last.EnvironmentFailure.FailedAt.Equal(read.EnvironmentFailure.FailedAt) || last.EnvironmentInputActivity != nil || !last.Settled { t.Fatal("failed snapshot", last) } - if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "later", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}); !errors.Is(err, store.ErrHostedEnvironmentFailed) { + if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "later", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}); !errors.Is(err, sessions.ErrHostedEnvironmentFailed) { t.Fatal("failed hosted Environment admitted input", err) } raw, _ := json.Marshal(events) @@ -229,10 +230,10 @@ func TestHostedInitializationFailureRecordsSafeSessionFailure(t *testing.T) { } // Tenant B cannot observe the failure. other := uuid.NewString() - if _, err := s.GetSession(t.Context(), other, session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := s.GetSession(t.Context(), other, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign Session read", err) } - if _, err := s.ListSessionEvents(t.Context(), other, session.ID, 0); !errors.Is(err, store.ErrNotFound) { + if _, err := s.ListSessionEvents(t.Context(), other, session.ID, 0); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign Session events", err) } if page, err := s.ListSessions(t.Context(), other, "", 10, false, nil); err != nil || len(page.Sessions) != 0 { @@ -247,9 +248,9 @@ func TestHostedInitializationFailureRecordsSafeSessionFailure(t *testing.T) { func TestHostedInitializationFailureSettlesPendingInitialInput(t *testing.T) { s, db := hostedFailureStore(t) tenant := uuid.NewString() - session, environment := hostedFailureSession(t, s, tenant, store.CreateSessionInput{ + session, environment := hostedFailureSession(t, s, tenant, sessions.CreateSession{ Initialization: environmentconfig.Setup{Commands: []environmentconfig.SetupCommand{{Command: "exit 3"}}}, - InitialInputs: []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"initial"}`)}}, + InitialInputs: []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"initial"}`)}}, }) p := &hostedFailureProvider{lifecycleProvider: lifecycleProvider{resources: map[string]sandbox.Info{}}, fail: "setup", result: failedInitialization(3)} @@ -281,7 +282,7 @@ func TestHostedInitializationFailureSettlesPendingInitialInput(t *testing.T) { func TestHostedInitializationFailurePublicHTTP(t *testing.T) { s, db := hostedFailureStore(t) tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - session, environment := hostedFailureSession(t, s, tenant, store.CreateSessionInput{ + session, environment := hostedFailureSession(t, s, tenant, sessions.CreateSession{ Initialization: environmentconfig.Setup{Commands: []environmentconfig.SetupCommand{{Command: "echo " + hostedFailureCanary + "; exit 3"}}}, Metadata: map[string]string{"case": "setup-exit3"}, }) diff --git a/services/core/internal/store/initial_files.go b/services/core/internal/store/initial_files.go index e9758d9fe..a8a51b732 100644 --- a/services/core/internal/store/initial_files.go +++ b/services/core/internal/store/initial_files.go @@ -10,6 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -17,7 +18,7 @@ import ( func (s *Store) saveInitialFiles(ctx context.Context, q *sqlc.Queries, tx pgx.Tx, tenant string, session pgtype.UUID, initial []environmentconfig.InitialFile) ([]byte, error) { if environmentconfig.ValidateInitialFiles(initial) != nil { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } tenantID, err := parseID(tenant) if err != nil { @@ -30,11 +31,11 @@ func (s *Store) saveInitialFiles(ctx context.Context, q *sqlc.Queries, tx pgx.Tx if f.Type == "file_id" { sourceID, ok := files.ParseID(f.FileID) if !ok { - return nil, ErrNotFound + return nil, sessions.ErrNotFound } source, err := q.LockInitialSourceFile(ctx, sqlc.LockInitialSourceFileParams{TenantID: tenantID, ID: pgtype.UUID{Bytes: sourceID, Valid: true}}) if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrNotFound + return nil, sessions.ErrNotFound } if err != nil { return nil, err @@ -64,7 +65,7 @@ func (s *Store) saveInitialFiles(ctx context.Context, q *sqlc.Queries, tx pgx.Tx // store. func readInitialSourceFile(ctx context.Context, tx pgx.Tx, source sqlc.SourceFile) ([]byte, error) { if source.SizeBytes > environmentconfig.MaxInitialFileBytes { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } objects := tx.LargeObjects() reader, err := objects.Open(ctx, source.BodyOid.Uint32, pgx.LargeObjectModeRead) @@ -76,7 +77,7 @@ func readInitialSourceFile(ctx context.Context, tx pgx.Tx, source sqlc.SourceFil return nil, err } if len(body) > environmentconfig.MaxInitialFileBytes || int64(len(body)) != source.SizeBytes { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } return body, reader.Close() } @@ -94,7 +95,7 @@ func (s *Store) ReadInitialEnvironmentFile(ctx context.Context, tenant, session id := uuid.UUID(row.ID.Bytes).String() body, err := s.credentialCipher.OpenEnvironmentFile(row.Contents, credentialcrypto.EnvironmentFileBinding{TenantID: uuid.UUID(lookup.TenantID.Bytes).String(), Resource: "session", OwnerID: uuid.UUID(lookup.ID.Bytes).String(), FileID: id}) if err == nil && int64(len(body)) != row.SizeBytes { - err = ErrInvalidInput + err = sessions.ErrInvalidInput } return environmentconfig.InitialFileMetadata{ID: id, Path: row.Path, SizeBytes: &row.SizeBytes}, body, err } diff --git a/services/core/internal/store/initial_files_test.go b/services/core/internal/store/initial_files_test.go index 38fa96a32..fbc56225c 100644 --- a/services/core/internal/store/initial_files_test.go +++ b/services/core/internal/store/initial_files_test.go @@ -13,6 +13,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/filepg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) @@ -50,7 +51,7 @@ func TestInitialFilesFrozenEncryptedIsolatedAndRetryable(t *testing.T) { t.Fatal(err) } initial := []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/a/data", Data: canary}, {Type: "file_id", Path: "/workspace/b", FileID: upload.ID}} - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), InitialFiles: initial} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), InitialFiles: initial} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -81,10 +82,10 @@ func TestInitialFilesFrozenEncryptedIsolatedAndRetryable(t *testing.T) { changed := input changed.InitialFiles = append([]environmentconfig.InitialFile(nil), initial...) changed.InitialFiles[0].Data = []byte("changed") - if _, err := s.CreateSession(t.Context(), tenant, changed); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(t.Context(), tenant, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed bytes retried", err) } - if _, err := s.GetSessionDevice(t.Context(), tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionDevice(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("uninitialized environment exposed", err) } } diff --git a/services/core/internal/store/input_batches_test.go b/services/core/internal/store/input_batches_test.go index 16a75011d..222816197 100644 --- a/services/core/internal/store/input_batches_test.go +++ b/services/core/internal/store/input_batches_test.go @@ -14,9 +14,9 @@ import ( "github.com/google/uuid" ) -func messageInput(text string) Input { +func messageInput(text string) sessions.Input { payload, _ := json.Marshal(map[string]string{"text": text}) - return Input{Kind: "message", Payload: payload} + return sessions.Input{Kind: "message", Payload: payload} } func TestInputBatchesAreOrderedAndIdempotentAcrossConnections(t *testing.T) { @@ -25,10 +25,10 @@ func TestInputBatchesAreOrderedAndIdempotentAcrossConnections(t *testing.T) { tenant, session := newTurnSession(t, s) ctx := context.Background() const count = 8 - batch := []Input{messageInput("first"), messageInput("second")} + batch := []sessions.Input{messageInput("first"), messageInput("second")} for _, repeated := range []bool{true, false} { var wg sync.WaitGroup - receipts := make(chan []InputReceipt, count) + receipts := make(chan []sessions.InputReceipt, count) errs := make(chan error, count) for i := range count { wg.Add(1) @@ -99,8 +99,8 @@ func TestBatchRetriesCompareTheWholeRequestAndRetainTargets(t *testing.T) { s, pool := testStore(t) tenant, session := newTurnSession(t, s) ctx := context.Background() - cancel := Input{Kind: "cancel", Payload: json.RawMessage(`{}`)} - batch := []Input{cancel, messageInput("one"), cancel, messageInput("two")} + cancel := sessions.Input{Kind: "cancel", Payload: json.RawMessage(`{}`)} + batch := []sessions.Input{cancel, messageInput("one"), cancel, messageInput("two")} first, err := s.SubmitInputs(ctx, tenant, session.ID, "mixed", batch) if err != nil { t.Fatal(err) @@ -115,8 +115,8 @@ func TestBatchRetriesCompareTheWholeRequestAndRetainTargets(t *testing.T) { transition(t, s, tenant, session.ID, first[3].TurnID, sessions.TurnQueued, sessions.TurnInProgress) transition(t, s, tenant, session.ID, first[3].TurnID, sessions.TurnInProgress, sessions.TurnCompleted) next := submitMessage(t, s, tenant, session.ID, "next") - for _, changed := range [][]Input{batch[:3], append(append([]Input{}, batch...), cancel), {batch[0], batch[3], batch[2], batch[1]}} { - if _, err := s.SubmitInputs(ctx, tenant, session.ID, "mixed", changed); !errors.Is(err, ErrIdempotencyConflict) { + for _, changed := range [][]sessions.Input{batch[:3], append(append([]sessions.Input{}, batch...), cancel), {batch[0], batch[3], batch[2], batch[1]}} { + if _, err := s.SubmitInputs(ctx, tenant, session.ID, "mixed", changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatalf("changed batch accepted: %v", err) } } @@ -135,7 +135,7 @@ func TestBatchRetriesCompareTheWholeRequestAndRetainTargets(t *testing.T) { t.Fatalf("retry cancelled later work: %+v, %v", current, err) } otherTenant, _ := newTurnSession(t, restarted) - if _, err := restarted.SubmitInputs(ctx, otherTenant, session.ID, "mixed", batch); !errors.Is(err, ErrNotFound) { + if _, err := restarted.SubmitInputs(ctx, otherTenant, session.ID, "mixed", batch); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("batch retry escaped tenant: %v", err) } } @@ -154,7 +154,7 @@ func TestFailedBatchRollsBackEarlierCancellationAndInputs(t *testing.T) { t.Fatal(err) } t.Cleanup(func() { _, _ = pool.Exec(ctx, "ALTER TABLE turn_inputs DROP CONSTRAINT IF EXISTS "+constraint) }) - batch := []Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}, messageInput("after cancel")} + batch := []sessions.Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}, messageInput("after cancel")} if got, err := s.SubmitInputs(ctx, tenant, session.ID, key, batch); err == nil || got != nil { t.Fatalf("partial batch succeeded: %+v %v", got, err) } @@ -176,13 +176,13 @@ func TestFailedBatchRollsBackEarlierCancellationAndInputs(t *testing.T) { } func TestInputBatchValidation(t *testing.T) { - for _, input := range [][]Input{ - nil, make([]Input, 65), {messageInput("ok"), {Kind: "unsupported", Payload: json.RawMessage(`{}`)}}, + for _, input := range [][]sessions.Input{ + nil, make([]sessions.Input, 65), {messageInput("ok"), {Kind: "unsupported", Payload: json.RawMessage(`{}`)}}, {{Kind: "message"}}, {{Kind: "message", Payload: json.RawMessage(`[]`)}}, {{Kind: "cancel", Payload: json.RawMessage(`{"target":"other"}`)}}, {messageInput(strings.Repeat("x", 300*1024)), messageInput(strings.Repeat("y", 300*1024))}, } { - if _, _, err := validateInputs(input); !errors.Is(err, ErrInvalidInput) { + if _, _, err := validateInputs(input); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("invalid batch accepted: %v", err) } } diff --git a/services/core/internal/store/input_conflicts_public_test.go b/services/core/internal/store/input_conflicts_public_test.go index f9955f059..a02953e37 100644 --- a/services/core/internal/store/input_conflicts_public_test.go +++ b/services/core/internal/store/input_conflicts_public_test.go @@ -55,10 +55,10 @@ func TestSessionInputConflictsAndResultTargetsPostgres(t *testing.T) { create := func(environment string, initial bool) string { t.Helper() - input := store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), + input := sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{` + conflictAgent + `,"environment":` + environment + `}`)} if initial { - input.InitialInputs = []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"reserved"}`)}} + input.InitialInputs = []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"reserved"}`)}} } session, err := s.CreateSession(ctx, tenant, input) if err != nil { @@ -67,21 +67,21 @@ func TestSessionInputConflictsAndResultTargetsPostgres(t *testing.T) { return session.ID } // waiting starts a Turn that waits for one function result. - waiting := func(session, key, call string) store.InputReceipt { + waiting := func(session, key, call string) sessions.InputReceipt { t.Helper() receipt, err := s.SubmitMessage(ctx, tenant, session, key, json.RawMessage(`{"text":"work"}`)) if err != nil { t.Fatal(err) } - if _, err := s.TransitionTurn(ctx, tenant, session, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, session, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - if err := s.RecordFunctionCall(ctx, tenant, session, receipt.TurnID, store.FunctionCall{CallID: call, ExecutorCallID: "native-" + call, Name: "lookup", Arguments: json.RawMessage(`{}`)}); err != nil { + if err := s.RecordFunctionCall(ctx, tenant, session, receipt.TurnID, sessions.FunctionCall{CallID: call, ExecutorCallID: "native-" + call, Name: "lookup", Arguments: json.RawMessage(`{}`)}); err != nil { t.Fatal(err) } return receipt } - complete := func(session string, receipt store.InputReceipt, call string) { + complete := func(session string, receipt sessions.InputReceipt, call string) { t.Helper() if err := s.ConfirmFunctionResult(ctx, tenant, session, receipt.TurnID, call); err != nil { t.Fatal(err) diff --git a/services/core/internal/store/item_order_migration_test.go b/services/core/internal/store/item_order_migration_test.go index 64d02f5b7..16d25cac7 100644 --- a/services/core/internal/store/item_order_migration_test.go +++ b/services/core/internal/store/item_order_migration_test.go @@ -150,10 +150,10 @@ func TestItemOrderMigrationPreservesIndexedHistory(t *testing.T) { } t.Cleanup(migratedPool.Close) s := store.New(migratedPool) - if _, err = s.TransitionTurn(ctx, tenant, session, turn, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = s.TransitionTurn(ctx, tenant, session, turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - if err = s.AppendTurnEvents(ctx, tenant, session, turn, 1, []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"after-upgrade","delta":"continued"}`)}}); err != nil { + if err = s.AppendTurnEvents(ctx, tenant, session, turn, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"after-upgrade","delta":"continued"}`)}}); err != nil { t.Fatal(err) } addedID := items.Identity(turn, "message:after-upgrade") diff --git a/services/core/internal/store/item_order_test.go b/services/core/internal/store/item_order_test.go index e26a6e2c0..60e4e3ee3 100644 --- a/services/core/internal/store/item_order_test.go +++ b/services/core/internal/store/item_order_test.go @@ -18,7 +18,7 @@ func TestItemObservationOrderSurvivesTiesUpdatesRetriesAndRecovery(t *testing.T) ctx := context.Background() s, pool := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "ordered"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "ordered"}) if err != nil { t.Fatal(err) } @@ -26,7 +26,7 @@ func TestItemObservationOrderSurvivesTiesUpdatesRetriesAndRecovery(t *testing.T) if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } @@ -40,10 +40,10 @@ func TestItemObservationOrderSurvivesTiesUpdatesRetriesAndRecovery(t *testing.T) sort.Slice(keys, func(i, j int) bool { return items.Identity(input.TurnID, "message:"+keys[i]) > items.Identity(input.TurnID, "message:"+keys[j]) }) - var batch []store.ExecutionEvent + var batch []sessions.ExecutionEvent for _, key := range keys { payload, _ := json.Marshal(map[string]string{"id": key, "status": "in_progress"}) - batch = append(batch, store.ExecutionEvent{Kind: "output_message", Payload: payload}) + batch = append(batch, sessions.ExecutionEvent{Kind: "output_message", Payload: payload}) want = append(want, items.Identity(input.TurnID, "message:"+key)) } for range 2 { @@ -60,21 +60,21 @@ func TestItemObservationOrderSurvivesTiesUpdatesRetriesAndRecovery(t *testing.T) } want = append(want, page.Items[4].ID) completion, _ := json.Marshal(map[string]string{"id": keys[0], "status": "completed", "text": "final"}) - batch = []store.ExecutionEvent{{Kind: "output_message", Payload: completion}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"last","delta":"partial"}`)}} + batch = []sessions.ExecutionEvent{{Kind: "output_message", Payload: completion}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"last","delta":"partial"}`)}} for range 2 { if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, batch); err != nil { t.Fatal(err) } } want = append(want, items.Identity(input.TurnID, "message:last")) - bad := []store.ExecutionEvent{ + bad := []sessions.ExecutionEvent{ {Kind: "delta", Payload: json.RawMessage(`{"item_id":"rollback","delta":"discard"}`)}, {Kind: "output_message", Payload: json.RawMessage(`{"id":"invalid","status":"invalid"}`)}, } if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 6, bad); err == nil { t.Fatal("invalid batch accepted") } - if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 6, []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"after-rollback","delta":"retained"}`)}}); err != nil { + if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 6, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"after-rollback","delta":"retained"}`)}}); err != nil { t.Fatal(err) } want = append(want, items.Identity(input.TurnID, "message:after-rollback")) @@ -135,10 +135,10 @@ func TestItemObservationOrderSurvivesTiesUpdatesRetriesAndRecovery(t *testing.T) if err != nil { t.Fatal(err) } - if _, err = s.TransitionTurn(ctx, tenant, session.ID, next.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = s.TransitionTurn(ctx, tenant, session.ID, next.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - if err = s.AppendTurnEvents(ctx, tenant, session.ID, next.TurnID, 1, []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"new","delta":"new turn"}`)}}); err != nil { + if err = s.AppendTurnEvents(ctx, tenant, session.ID, next.TurnID, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"new","delta":"new turn"}`)}}); err != nil { t.Fatal(err) } var index int diff --git a/services/core/internal/store/item_reads.go b/services/core/internal/store/item_reads.go index d0c3f37e3..facbfed05 100644 --- a/services/core/internal/store/item_reads.go +++ b/services/core/internal/store/item_reads.go @@ -8,20 +8,16 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -type ItemPage struct { - Items []v1.Item - HasMore bool -} - -func (s *Store) ListItems(ctx context.Context, tenantID, sessionID, cursor string, limit int, ascending bool) (ItemPage, error) { +func (s *Store) ListItems(ctx context.Context, tenantID, sessionID, cursor string, limit int, ascending bool) (sessions.ItemPage, error) { if limit < 1 || limit > 100 { - return ItemPage{}, ErrInvalidInput + return sessions.ItemPage{}, sessions.ErrInvalidInput } - page := ItemPage{Items: make([]v1.Item, 0, limit)} + page := sessions.ItemPage{Items: make([]v1.Item, 0, limit)} err := s.withPublicSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { p := sqlc.ListSessionItemsParams{SessionID: session, PageLimit: int32(limit + 1), Ascending: ascending, AfterID: pgtype.UUID{Valid: true}} if cursor != "" { @@ -29,7 +25,7 @@ func (s *Store) ListItems(ctx context.Context, tenantID, sessionID, cursor strin // malformed one, is an invalid cursor rather than a missing resource. row, err := q.GetSessionItem(ctx, sqlc.GetSessionItemParams{SessionID: session, ID: pgunit.PathID(cursor)}) if errors.Is(err, pgx.ErrNoRows) { - return errItemCursor + return sessions.ErrItemCursor } if err != nil { return err diff --git a/services/core/internal/store/item_reads_test.go b/services/core/internal/store/item_reads_test.go index a33b99777..1f2bdf2b1 100644 --- a/services/core/internal/store/item_reads_test.go +++ b/services/core/internal/store/item_reads_test.go @@ -18,7 +18,7 @@ func TestItemsRecoverSnapshotsPartialResultsPaginationAndIsolation(t *testing.T) ctx := context.Background() s, pool := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "items"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "items"}) if err != nil { t.Fatal(err) } @@ -26,11 +26,11 @@ func TestItemsRecoverSnapshotsPartialResultsPaginationAndIsolation(t *testing.T) if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } - batch := []store.ExecutionEvent{ + batch := []sessions.ExecutionEvent{ {Kind: "output_message", Payload: json.RawMessage(`{"id":"answer","status":"in_progress"}`)}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"answer","delta":"draft"}`)}, {Kind: "tool_call", Payload: json.RawMessage(`{"id":"cmd","stage":"before","observation":{"status":"in_progress","kind":"command","command":"exit 7"}}`)}, @@ -102,14 +102,14 @@ func TestItemsRecoverSnapshotsPartialResultsPaginationAndIsolation(t *testing.T) } } } - other, _ := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) + other, _ := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) // A foreign parent is not found before the cursor is read. - if _, err = s.ListItems(ctx, uuid.NewString(), session.ID, page.Items[0].ID, 20, true); !errors.Is(err, store.ErrNotFound) { + if _, err = s.ListItems(ctx, uuid.NewString(), session.ID, page.Items[0].ID, 20, true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } // Another Session's Item is an invalid cursor here, like a missing or malformed one. for _, cursor := range []string{page.Items[0].ID, uuid.NewString(), "not-a-uuid"} { - var invalid *store.InvalidCursorError + var invalid *sessions.CursorError if _, err = s.ListItems(ctx, tenant, other.ID, cursor, 20, true); !errors.As(err, &invalid) || invalid.Message != "Invalid session item ID in `after`" { t.Fatal(cursor, err) } @@ -120,16 +120,16 @@ func TestItemProjectionFailureRollsBackJournalAndAggregateRecovers(t *testing.T) ctx := context.Background() s, _ := store.NewTestStore(t) tenant := uuid.NewString() - session, _ := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "legacy"}) + session, _ := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "legacy"}) input, err := s.SubmitMessage(ctx, tenant, session.ID, "input", json.RawMessage(`{"text":"test"}`)) if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } - bad := []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"must roll back"}`)}, {Kind: "tool_call", Payload: json.RawMessage(`{"id":"mismatch","stage":"after","observation":{"status":"completed","kind":"invalid"}}`)}} + bad := []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"must roll back"}`)}, {Kind: "tool_call", Payload: json.RawMessage(`{"id":"mismatch","stage":"after","observation":{"status":"completed","kind":"invalid"}}`)}} if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 1, bad); err == nil { t.Fatal("invalid snapshot accepted") } @@ -141,7 +141,7 @@ func TestItemProjectionFailureRollsBackJournalAndAggregateRecovers(t *testing.T) if err != nil || len(page.Items) != 1 { t.Fatal(page, err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted, Outcome: json.RawMessage(`{"done":{"content":"legacy answer","metadata":{"private":"SECRET"}}}`)}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted, Outcome: json.RawMessage(`{"done":{"content":"legacy answer","metadata":{"private":"SECRET"}}}`)}) if err != nil { t.Fatal(err) } @@ -156,17 +156,17 @@ func TestReceiptOnlyTextRecoversWithoutInventingCompletion(t *testing.T) { s, pool := store.NewTestStore(t) tenant := uuid.NewString() for _, receiptOnly := range []bool{true, false} { - session, _ := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString()}) + session, _ := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString()}) input, err := s.SubmitMessage(ctx, tenant, session.ID, "first", json.RawMessage(`{"text":"test"}`)) if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } if receiptOnly { - err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 1, []store.ExecutionEvent{{Kind: "cancel_receipt", Payload: json.RawMessage(`{"applied":true,"outcome":{"content":"retained cancellation text"}}`)}}) + err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 1, []sessions.ExecutionEvent{{Kind: "cancel_receipt", Payload: json.RawMessage(`{"applied":true,"outcome":{"content":"retained cancellation text"}}`)}}) if err != nil { t.Fatal(err) } @@ -186,7 +186,7 @@ func TestLegacyFailureRetainsPartialAnswerAcrossRecovery(t *testing.T) { ctx := context.Background() s, pool := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "failed-items"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "failed-items"}) if err != nil { t.Fatal(err) } @@ -194,11 +194,11 @@ func TestLegacyFailureRetainsPartialAnswerAcrossRecovery(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } - batch := []store.ExecutionEvent{ + batch := []sessions.ExecutionEvent{ {Kind: "delta", Payload: json.RawMessage(`{"delta":"partial answer"}`)}, {Kind: "tool_call", Payload: json.RawMessage(`{"id":"open","stage":"after","observation":{"status":"completed","kind":"web_search","action":{"type":"open_page","url":"https://example.com"}}}`)}, {Kind: "tool_call", Payload: json.RawMessage(`{"id":"find","stage":"after","observation":{"status":"completed","kind":"web_search","action":{"type":"find_in_page","url":"https://example.com","pattern":"needle"}}}`)}, diff --git a/services/core/internal/store/item_serialization_test.go b/services/core/internal/store/item_serialization_test.go index f08e1de24..8a04344b3 100644 --- a/services/core/internal/store/item_serialization_test.go +++ b/services/core/internal/store/item_serialization_test.go @@ -46,23 +46,23 @@ func TestAssistantMessageEventsFollowOfficialSequence(t *testing.T) { answer := " {\"text\":\"red é \\u00e9\\n\"}\n" for _, test := range []struct { name string - events []ExecutionEvent + events []sessions.ExecutionEvent phase string deltas []string final string }{ - {"streamed deltas", []ExecutionEvent{ + {"streamed deltas", []sessions.ExecutionEvent{ {Kind: "delta", Payload: json.RawMessage(`{"item_id":"a","delta":"Hel"}`)}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"a","delta":"lo"}`)}, {Kind: "output_message", Payload: json.RawMessage(`{"id":"a","status":"completed","text":"Hello"}`)}, }, "null", []string{"Hel", "lo"}, "Hello"}, - {"native start", []ExecutionEvent{ + {"native start", []sessions.ExecutionEvent{ {Kind: "output_message", Payload: json.RawMessage(`{"id":"a","status":"in_progress","phase":"final_answer"}`)}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"a","delta":"Hi"}`)}, {Kind: "output_message", Payload: json.RawMessage(`{"id":"a","status":"completed","phase":"final_answer","text":"Hi"}`)}, }, `"final_answer"`, []string{"Hi"}, "Hi"}, // A non-streamed native final carries its exact text in one delta. - {"non-streamed final", []ExecutionEvent{ + {"non-streamed final", []sessions.ExecutionEvent{ {Kind: "output_message", Payload: json.RawMessage(`{"id":"a","status":"completed","phase":"final_answer","text":` + mustJSON(t, answer) + `}`)}, }, `"final_answer"`, []string{answer}, answer}, } { diff --git a/services/core/internal/store/list_cursor_public_test.go b/services/core/internal/store/list_cursor_public_test.go index 79981e39b..3d3a52026 100644 --- a/services/core/internal/store/list_cursor_public_test.go +++ b/services/core/internal/store/list_cursor_public_test.go @@ -65,7 +65,7 @@ func seedCursorFixture(t *testing.T, s *store.Store, writer *store.Store, skillS f.session = client.created(token, "/v1/agents/sessions", newSession) f.turn = first("/v1/agents/sessions/" + f.session + "/turns") f.item = first("/v1/agents/sessions/" + f.session + "/items") - if _, err := s.TransitionTurn(ctx, tenant, f.session, f.turn, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnCancelled}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, f.session, f.turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnCancelled}); err != nil { t.Fatal(err) } if _, err := s.SubmitMessage(ctx, tenant, f.session, label+"-second", json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"second"}]}]}`)); err != nil { @@ -85,7 +85,7 @@ func seedCursorFixture(t *testing.T, s *store.Store, writer *store.Store, skillS // Subagent history is seeded through the execution lease, as a daemon would. seedSubagents := func(key string) (session, rootTurn string) { t.Helper() - created, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: key, + created, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"cursor-model","multi_agent":{"enabled":true,"max_concurrent_subagents":4}}}`)}) if err != nil { t.Fatal(err) @@ -101,23 +101,23 @@ func seedCursorFixture(t *testing.T, s *store.Store, writer *store.Store, skillS if err = writer.BindSessionDevice(ctx, tenant, created.ID, host.ID); err != nil { t.Fatal(err) } - if _, err = writer.TransitionTurn(ctx, tenant, created.ID, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = writer.TransitionTurn(ctx, tenant, created.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } opened := int64(1700000001000) - identity := func(child string) store.ExecutionEvent { + identity := func(child string) sessions.ExecutionEvent { return subagentFixture(proto.TypeSubagentIdentity, proto.SubagentIdentityPayload{NativeID: child, ParentNativeID: "root", NativeCreatedAt: 1700000001, ParentTurnID: "native-root", SourceItemID: "spawn-" + child}) } // Distinct creation times keep child-turn before later-child-turn. - turn := func(child, id string, created int64) store.ExecutionEvent { + turn := func(child, id string, created int64) sessions.ExecutionEvent { return subagentFixture(proto.TypeSubagentTurn, proto.SubagentTurnPayload{NativeID: child, TurnID: id, Status: sessions.TurnInProgress, CreatedAtMS: created, StartedAtMS: &created}) } - message := func(child, turn, id string, position int32) store.ExecutionEvent { + message := func(child, turn, id string, position int32) sessions.ExecutionEvent { text := "answer " + id payload, _ := json.Marshal(proto.OutputMessagePayload{ID: id, Status: "completed", Text: &text}) return subagentFixture(proto.TypeSubagentItem, proto.SubagentItemPayload{NativeID: child, TurnID: turn, ItemID: id, Position: position, Kind: proto.TypeOutputMessage, Payload: payload}) } - facts := []store.ExecutionEvent{identity("child"), identity("sibling"), + facts := []sessions.ExecutionEvent{identity("child"), identity("sibling"), turn("child", "child-turn", opened), message("child", "child-turn", "child-item", 0), message("child", "child-turn", "child-item-2", 1), turn("child", "later-child-turn", opened+1000), message("child", "later-child-turn", "later-child-item", 0), turn("sibling", "sibling-turn", opened), message("sibling", "sibling-turn", "sibling-item", 0)} diff --git a/services/core/internal/store/list_cursors.go b/services/core/internal/store/list_cursors.go index 697573ca5..270a634c0 100644 --- a/services/core/internal/store/list_cursors.go +++ b/services/core/internal/store/list_cursors.go @@ -1,31 +1,15 @@ package store -import "errors" +import ( + "errors" -// InvalidCursorError reports a list `after` cursor that does not name a -// resource of that list once its parents have resolved. Message is the list -// family's observed official message; the API layer selects the family's code -// and param. Missing, malformed, other-type, other-parent and foreign cursors -// all produce the same error, so a cursor never reveals another tenant's -// resources. -type InvalidCursorError struct{ Message string } - -func (e *InvalidCursorError) Error() string { return e.Message } - -// Official messages for lists that reject an unresolved cursor with 400. -var ( - // Session Items, Subagent Items and Subagent Turn Items. - errItemCursor = &InvalidCursorError{Message: "Invalid session item ID in `after`"} - // Subagents and Subagent Turns. - errResourceCursor = &InvalidCursorError{Message: "Invalid resource ID in `after`"} - // Session Artifacts. - errArtifactCursor = &InvalidCursorError{Message: "after is not a valid artifact ID"} + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // unresolvedCursor replaces a missing cursor resource with the list's cursor // error and keeps every other failure. func unresolvedCursor(err, cursor error) error { - if errors.Is(err, ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { return cursor } return err diff --git a/services/core/internal/store/local_artifact_export_test.go b/services/core/internal/store/local_artifact_export_test.go index 115efbe6c..d505e062e 100644 --- a/services/core/internal/store/local_artifact_export_test.go +++ b/services/core/internal/store/local_artifact_export_test.go @@ -8,10 +8,10 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -func completeLocalArtifactExport(t *testing.T, h *dispatchHarness, worker *execution.Worker, environment store.Environment) { +func completeLocalArtifactExport(t *testing.T, h *dispatchHarness, worker *execution.Worker, environment sessions.Environment) { t.Helper() prepared := h.read(proto.TypeExecutionPrepare) var request proto.ExecutionPreparePayload @@ -46,8 +46,8 @@ func completeLocalArtifactExport(t *testing.T, h *dispatchHarness, worker *execu t.Fatal("capture published before native completion", err) } completeCaptureDirectoryRead(t, h, worker, environment) - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "during-artifact-capture", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"run after the completed native execution"}`)}}) - if err != nil || pending.State != store.EnvironmentInputPending || len(pending.Receipts) != 0 { + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "during-artifact-capture", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"run after the completed native execution"}`)}}) + if err != nil || pending.State != sessions.EnvironmentInputPending || len(pending.Receipts) != 0 { t.Fatalf("input during artifact capture was assigned to the finished executor: %+v %v", pending, err) } h.write(begin.ID, proto.TypeWorkspaceExportResult, proto.WorkspaceExportResultPayload{Outcome: "completed", Offset: export.Offset}) @@ -59,7 +59,7 @@ func completeLocalArtifactExport(t *testing.T, h *dispatchHarness, worker *execu h.write(prepared.ID, proto.TypePreparationStatus, proto.PreparationStatusPayload{Handle: handle, Revision: 3, State: "released"}) } -func completeCaptureDirectoryRead(t *testing.T, h *dispatchHarness, worker *execution.Worker, environment store.Environment) { +func completeCaptureDirectoryRead(t *testing.T, h *dispatchHarness, worker *execution.Worker, environment sessions.Environment) { t.Helper() result := startDirectoryRead(t.Context(), worker, environment) frame := h.read(proto.TypeExecutionPrepare) diff --git a/services/core/internal/store/local_environment_devices.go b/services/core/internal/store/local_environment_devices.go index 9ed2e63c8..92691f6c0 100644 --- a/services/core/internal/store/local_environment_devices.go +++ b/services/core/internal/store/local_environment_devices.go @@ -6,44 +6,45 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) // CreateEnvironmentDevice provisions one dedicated Runtime without widening an existing credential. -func (s *Store) CreateEnvironmentDevice(ctx context.Context, tenantID, environmentID, name, credentialHash string) (ExecutionDevice, error) { +func (s *Store) CreateEnvironmentDevice(ctx context.Context, tenantID, environmentID, name, credentialHash string) (sessions.ExecutionDevice, error) { environment, err := s.GetEnvironment(ctx, tenantID, environmentID) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } var configuration struct { Type string `json:"type"` } if json.Unmarshal(environment.Configuration, &configuration) != nil || configuration.Type != "openai_hosted" { - return ExecutionDevice{}, ErrInvalidInput + return sessions.ExecutionDevice{}, sessions.ErrInvalidInput } lookup, err := deviceLookup(tenantID, environment.ID) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } params, err := newDeviceParams(lookup.TenantID, name, credentialHash) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } err = s.withPublicSession(ctx, tenantID, environment.SessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { return createEnvironmentDevice(ctx, q, lookup, session, params) }) if err != nil { - return ExecutionDevice{}, err + return sessions.ExecutionDevice{}, err } - return ExecutionDevice{ID: uuid.UUID(params.ID.Bytes).String(), Name: params.Name, EnvironmentID: environment.ID}, nil + return sessions.ExecutionDevice{ID: uuid.UUID(params.ID.Bytes).String(), Name: params.Name, EnvironmentID: environment.ID}, nil } func createEnvironmentDevice(ctx context.Context, q *sqlc.Queries, lookup sqlc.GetDeviceParams, session pgtype.UUID, params sqlc.CreateDeviceParams) error { _, err := q.GetSessionDevice(ctx, sqlc.GetSessionDeviceParams{TenantID: lookup.TenantID, ID: session}) if err == nil { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } if !errors.Is(err, pgx.ErrNoRows) { return err @@ -53,7 +54,7 @@ func createEnvironmentDevice(ctx context.Context, q *sqlc.Queries, lookup sqlc.G CredentialHash: params.CredentialHash, EnvironmentID: lookup.ID, }) if errors.Is(err, pgx.ErrNoRows) { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } if err != nil { return err diff --git a/services/core/internal/store/local_environment_devices_test.go b/services/core/internal/store/local_environment_devices_test.go index c4caad34d..25ec16580 100644 --- a/services/core/internal/store/local_environment_devices_test.go +++ b/services/core/internal/store/local_environment_devices_test.go @@ -7,12 +7,13 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) -func localEnvironment(t *testing.T, s *Store, tenant string) (Session, Environment) { +func localEnvironment(t *testing.T, s *Store, tenant string) (sessions.Session, sessions.Environment) { t.Helper() - session, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{ + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{ Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), }) @@ -33,14 +34,14 @@ func TestEnvironmentDeviceAuthorityAndLifecycle(t *testing.T) { sibling, _ := localEnvironment(t, s, tenant) foreign, _ := localEnvironment(t, s, foreignTenant) digest := runtimedevice.HashCredential(uuid.NewString()) - if _, err := s.CreateEnvironmentDevice(t.Context(), foreignTenant, environment.ID, "foreign", digest); !errors.Is(err, ErrNotFound) { + if _, err := s.CreateEnvironmentDevice(t.Context(), foreignTenant, environment.ID, "foreign", digest); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign provisioning: %v", err) } bound, err := s.CreateEnvironmentDevice(t.Context(), tenant, environment.ID, "dedicated", digest) if err != nil || bound.EnvironmentID != environment.ID { t.Fatalf("provision: %+v %v", bound, err) } - for _, other := range []Session{sibling, foreign} { + for _, other := range []sessions.Session{sibling, foreign} { if err := s.BindSessionDevice(t.Context(), other.TenantID, other.ID, bound.ID); err == nil { t.Fatal("dedicated credential bound to another Session") } @@ -89,7 +90,7 @@ func TestEnvironmentDeviceProvisioningHasOneWinner(t *testing.T) { for err := range results { if err == nil { winners++ - } else if !errors.Is(err, ErrDeviceBindingConflict) { + } else if !errors.Is(err, sessions.ErrDeviceBindingConflict) { t.Fatal(err) } } @@ -103,7 +104,7 @@ func TestEnvironmentDeviceProvisioningHasOneWinner(t *testing.T) { if err := s.RevokeDevice(t.Context(), tenant, bound.ID); err != nil { t.Fatal(err) } - if _, err := s.CreateEnvironmentDevice(t.Context(), tenant, environment.ID, "replacement", runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, ErrDeviceBindingConflict) { + if _, err := s.CreateEnvironmentDevice(t.Context(), tenant, environment.ID, "replacement", runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, sessions.ErrDeviceBindingConflict) { t.Fatalf("silent placement replacement: %v", err) } } diff --git a/services/core/internal/store/local_environment_file_write_test.go b/services/core/internal/store/local_environment_file_write_test.go index 9622519ae..d08028fc7 100644 --- a/services/core/internal/store/local_environment_file_write_test.go +++ b/services/core/internal/store/local_environment_file_write_test.go @@ -8,7 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -17,7 +17,7 @@ type localWriteResult struct { err error } -func startLocalWrite(ctx context.Context, w *execution.Worker, e store.Environment) <-chan localWriteResult { +func startLocalWrite(ctx context.Context, w *execution.Worker, e sessions.Environment) <-chan localWriteResult { done := make(chan localWriteResult, 1) go func() { size, err := w.WriteEnvironmentFile(ctx, e, "input", []byte("abc")) @@ -41,7 +41,7 @@ func TestLocalEnvironmentFileWriteOwnsMutationBeforeDispatch(t *testing.T) { h, w, environment := localWorker(t, true, false) foreign := environment foreign.TenantID = uuid.NewString() - if _, err := w.WriteEnvironmentFile(t.Context(), foreign, "input", nil); !errors.Is(err, store.ErrNotFound) { + if _, err := w.WriteEnvironmentFile(t.Context(), foreign, "input", nil); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign upload", err) } ctx, cancel := context.WithCancel(t.Context()) @@ -56,7 +56,7 @@ func TestLocalEnvironmentFileWriteOwnsMutationBeforeDispatch(t *testing.T) { if err != nil || intent.State != "pending" || intent.Identity.DeviceID != h.device.ID { t.Fatal("dispatch preceded durable ownership", intent, err) } - if _, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "concurrent", []store.Input{{Kind: "message", Payload: []byte(`{"text":"work"}`)}}); !errors.Is(err, store.ErrTurnConflict) { + if _, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "concurrent", []sessions.Input{{Kind: "message", Payload: []byte(`{"text":"work"}`)}}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("upload admitted concurrent execution", err) } cancel() @@ -98,7 +98,7 @@ func TestLocalEnvironmentFileWriteLostReceiptRemainsPending(t *testing.T) { if err != nil || intent.State != "pending" { t.Fatal("disconnect guessed rejection", intent, err) } - if _, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "after-loss", []store.Input{{Kind: "message", Payload: []byte(`{"text":"work"}`)}}); !errors.Is(err, store.ErrTurnConflict) { + if _, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "after-loss", []sessions.Input{{Kind: "message", Payload: []byte(`{"text":"work"}`)}}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("unknown upload admitted execution", err) } } diff --git a/services/core/internal/store/local_environment_worker_test.go b/services/core/internal/store/local_environment_worker_test.go index be1b01036..e81318b9d 100644 --- a/services/core/internal/store/local_environment_worker_test.go +++ b/services/core/internal/store/local_environment_worker_test.go @@ -15,7 +15,7 @@ import ( "github.com/google/uuid" ) -func localWorker(t *testing.T, scoped, execute bool) (*dispatchHarness, *execution.Worker, store.Environment) { +func localWorker(t *testing.T, scoped, execute bool) (*dispatchHarness, *execution.Worker, sessions.Environment) { t.Helper() h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), scoped) if scoped { @@ -61,7 +61,7 @@ func TestLocalEnvironmentWorkerDirectoryUsesExactAuthorityWithoutModel(t *testin h, w, environment := localWorker(t, true, false) foreign := environment foreign.TenantID = uuid.NewString() - if _, err := w.ReadEnvironmentDirectory(t.Context(), foreign, "reports"); !errors.Is(err, store.ErrNotFound) { + if _, err := w.ReadEnvironmentDirectory(t.Context(), foreign, "reports"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign read admitted", err) } result := startDirectoryRead(t.Context(), w, environment) @@ -92,7 +92,7 @@ func TestLocalEnvironmentWorkerRejectsGeneralDeviceDespiteCapability(t *testing. if _, err := w.ReadEnvironmentDirectory(t.Context(), environment, "reports"); !errors.Is(err, execution.ErrExecutionUnavailable) { t.Fatal("general device used as local authority", err) } - other, err := h.s.CreateSession(t.Context(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "unassigned", Configuration: h.session.Configuration}) + other, err := h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "unassigned", Configuration: h.session.Configuration}) if err != nil { t.Fatal(err) } @@ -103,14 +103,14 @@ func TestLocalEnvironmentWorkerRejectsGeneralDeviceDespiteCapability(t *testing. if _, err := w.ReadEnvironmentDirectory(t.Context(), unassigned, "reports"); !errors.Is(err, execution.ErrExecutionUnavailable) { t.Fatal("unassigned environment selected general device", err) } - if _, err := h.s.GetSessionDevice(t.Context(), h.tenant, other.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSessionDevice(t.Context(), h.tenant, other.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("read persisted an unauthorized placement", err) } } func TestLocalEnvironmentWorkerSchedulesPreparationWithoutRemoteResolver(t *testing.T) { h, worker, environment := localWorker(t, true, true) - reservation, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "local-input", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) + reservation, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "local-input", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) if err != nil { t.Fatal(err) } @@ -145,7 +145,7 @@ func TestLocalEnvironmentWorkerSchedulesPreparationWithoutRemoteResolver(t *test return err == nil && turn.Status == sessions.TurnCompleted }) settled, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, h.session.ID, reservation.ID) - if err != nil || settled.State != store.EnvironmentInputAdmitted || len(settled.Receipts) != 1 { + if err != nil || settled.State != sessions.EnvironmentInputAdmitted || len(settled.Receipts) != 1 { t.Fatal("local reservation did not settle", err) } bound, err := h.s.GetSessionExecutionBinding(t.Context(), h.tenant, h.session.ID) diff --git a/services/core/internal/store/mcp_credential_selection_public_test.go b/services/core/internal/store/mcp_credential_selection_public_test.go index 92952c459..88837bdb2 100644 --- a/services/core/internal/store/mcp_credential_selection_public_test.go +++ b/services/core/internal/store/mcp_credential_selection_public_test.go @@ -282,7 +282,7 @@ func TestMCPCredentialSelectionPublicPostgres(t *testing.T) { if err := pool.QueryRow(t.Context(), "SELECT id FROM turns WHERE session_id=$1", streamed).Scan(&turn); err != nil { t.Fatal(err) } - if _, err := s.TransitionTurn(t.Context(), tenantA, streamed, turn, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnCancelled}); err != nil { + if _, err := s.TransitionTurn(t.Context(), tenantA, streamed, turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnCancelled}); err != nil { t.Fatal(err) } _, tools = snapshot(stream, "agent.session.idle") diff --git a/services/core/internal/store/message_image_admission_test.go b/services/core/internal/store/message_image_admission_test.go index 9a356ac70..9ee2a74a6 100644 --- a/services/core/internal/store/message_image_admission_test.go +++ b/services/core/internal/store/message_image_admission_test.go @@ -9,11 +9,12 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) -func imageAdmissionBatch() []store.Input { - return []store.Input{ +func imageAdmissionBatch() []sessions.Input { + return []sessions.Input{ {Kind: "message", Payload: json.RawMessage(`{"text":"do not partially admit"}`)}, {Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_image","image_url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII="}]}]}`)}, } @@ -31,8 +32,8 @@ func TestUnqualifiedImageAdmissionIsAtomic(t *testing.T) { worker := startWorker(t, t.Context(), h.db, h.d) defer func() { ctx, cancel := context.WithCancel(context.Background()); cancel(); _ = worker.Run(ctx) }() configuration := json.RawMessage(`{"agent":{"model":"fixture"},"environment":{"type":"` + placement + `","workspace_directory":"/workspace"}}`) - create := store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "image-create", Configuration: configuration, InitialInputs: imageAdmissionBatch()} - if _, err := worker.CreateSession(t.Context(), h.tenant, create); !errors.Is(err, store.ErrInvalidInput) { + create := sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "image-create", Configuration: configuration, InitialInputs: imageAdmissionBatch()} + if _, err := worker.CreateSession(t.Context(), h.tenant, create); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("creation accepted an unqualified image", err) } // Create a Session without starting work to exercise both ordinary and @@ -42,7 +43,7 @@ func TestUnqualifiedImageAdmissionIsAtomic(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := worker.SubmitInputs(t.Context(), h.tenant, session.ID, "image-batch", imageAdmissionBatch()); !errors.Is(err, store.ErrInvalidInput) { + if _, err := worker.SubmitInputs(t.Context(), h.tenant, session.ID, "image-batch", imageAdmissionBatch()); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("batch accepted an unqualified image", err) } session, err = h.s.GetSession(t.Context(), h.tenant, session.ID) diff --git a/services/core/internal/store/native_environment_test.go b/services/core/internal/store/native_environment_test.go index cb28243d5..e83773c85 100644 --- a/services/core/internal/store/native_environment_test.go +++ b/services/core/internal/store/native_environment_test.go @@ -99,7 +99,7 @@ func TestNativeNoExecutionEnvironment(t *testing.T) { provider := nativeModelProvider(model) config, _ := json.Marshal(map[string]any{"agent": map[string]string{"model": "gpt-5.5", "instructions": "Keep this instruction."}, "environment": map[string]string{"type": "none"}}) var err error - h.session, err = h.s.CreateSession(ctx, h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "native-session", Configuration: config, ModelProvider: provider, ModelProviderSource: v1.ModelProviderSourceDeployment}) + h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "native-session", Configuration: config, ModelProvider: provider, ModelProviderSource: v1.ModelProviderSourceDeployment}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/native_recovery_test.go b/services/core/internal/store/native_recovery_test.go index baa2033c6..16064b132 100644 --- a/services/core/internal/store/native_recovery_test.go +++ b/services/core/internal/store/native_recovery_test.go @@ -20,7 +20,7 @@ func TestSessionExecutionBindingRetainsStartedExecutionRequirement(t *testing.T) if err != nil || bound.HasStartedTurn != want || bound.NativeSessionID != "" { t.Fatalf("binding=%+v err=%v", bound, err) } - if _, err := st.GetSessionExecutionBinding(t.Context(), foreign, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := st.GetSessionExecutionBinding(t.Context(), foreign, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign binding", err) } } diff --git a/services/core/internal/store/no_environment_test.go b/services/core/internal/store/no_environment_test.go index fb8384fef..b58049d9e 100644 --- a/services/core/internal/store/no_environment_test.go +++ b/services/core/internal/store/no_environment_test.go @@ -12,7 +12,7 @@ func TestNoEnvironmentRejectsUnadvertisedDeviceBeforeClaim(t *testing.T) { h := newDispatchHarness(t) ctx := context.Background() var err error - h.session, err = h.s.CreateSession(ctx, h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "none", Configuration: []byte(`{"agent":{"model":"test-model"},"environment":{"type":"none"}}`)}) + h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "none", Configuration: []byte(`{"agent":{"model":"test-model"},"environment":{"type":"none"}}`)}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/path_id_semantics_public_test.go b/services/core/internal/store/path_id_semantics_public_test.go index a3e9dca85..50d897c1d 100644 --- a/services/core/internal/store/path_id_semantics_public_test.go +++ b/services/core/internal/store/path_id_semantics_public_test.go @@ -12,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -117,7 +118,7 @@ func TestMalformedPathIDsMatchMissingPostgres(t *testing.T) { t.Fatalf("fixture Turn: %d %s", status, raw) } turn := turns.Data[0].ID - hosted, err := s.CreateSession(t.Context(), ownerTenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "path-environment", + hosted, err := s.CreateSession(t.Context(), ownerTenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "path-environment", Configuration: json.RawMessage(`{"agent":{"model":"path-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace","capability_directories":[]}}`)}) if err != nil || hosted.Environment == nil { t.Fatal("fixture Environment", err) diff --git a/services/core/internal/store/prepared_dispatch_failure_test.go b/services/core/internal/store/prepared_dispatch_failure_test.go index c97670008..5ed6a8480 100644 --- a/services/core/internal/store/prepared_dispatch_failure_test.go +++ b/services/core/internal/store/prepared_dispatch_failure_test.go @@ -32,7 +32,7 @@ func TestPreparedDispatchSettlesOnlyReadyInput(t *testing.T) { t.Fatal(err) } case "delete": - if err := h.s.DeleteSession(t.Context(), h.tenant, h.session.ID); !errors.Is(err, store.ErrSessionNotIdle) { + if err := h.s.DeleteSession(t.Context(), h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("pending input deleted", err) } if err := h.s.CommitLegacyDeletion(t.Context(), h.tenant, h.session.ID); err != nil { @@ -48,9 +48,9 @@ func TestPreparedDispatchSettlesOnlyReadyInput(t *testing.T) { t.Fatal("unready preparation admitted work", got) } if action == "cancel" || action == "expire" { - want := store.EnvironmentInputCancelled + want := sessions.EnvironmentInputCancelled if action == "expire" { - want = store.EnvironmentInputExpired + want = sessions.EnvironmentInputExpired } if got.err != nil || got.run.Reservation.State != want { t.Fatal("terminal reservation outcome changed", got) @@ -58,7 +58,7 @@ func TestPreparedDispatchSettlesOnlyReadyInput(t *testing.T) { } else if got.err == nil { t.Fatal("preparation failure was hidden") } - if action == "delete" && !errors.Is(got.err, store.ErrNotFound) { + if action == "delete" && !errors.Is(got.err, sessions.ErrNotFound) { t.Fatal("deleted reservation remained accessible", got.err) } if action != "disconnect" { @@ -67,7 +67,7 @@ func TestPreparedDispatchSettlesOnlyReadyInput(t *testing.T) { assertEnvironmentExpiryHasNoHistory(t, pool, h.session.ID) if action == "prepare-failure" || action == "disconnect" { stored, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, h.session.ID, pending.ID) - if err != nil || stored.State != store.EnvironmentInputPending || !stored.Deadline.Equal(pending.Deadline) { + if err != nil || stored.State != sessions.EnvironmentInputPending || !stored.Deadline.Equal(pending.Deadline) { t.Fatal("preparation failure changed the pending identity", stored, err) } } diff --git a/services/core/internal/store/prepared_dispatch_test.go b/services/core/internal/store/prepared_dispatch_test.go index 2a095364e..ce7f87c0d 100644 --- a/services/core/internal/store/prepared_dispatch_test.go +++ b/services/core/internal/store/prepared_dispatch_test.go @@ -19,21 +19,21 @@ type preparedDispatchResult struct { err error } -func preparedDispatchHarness(t *testing.T) (*dispatchHarness, store.EnvironmentInputReservation) { +func preparedDispatchHarness(t *testing.T) (*dispatchHarness, sessions.EnvironmentInputReservation) { t.Helper() h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"test-model","instructions":"Keep this instruction."},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), true) assertNoRuntimeAllocation(t, h) owner := executionOwner(t, h.db, h.s) h.d.Store, h.lease = owner.Store, owner.Lease enableWorkerEnvironment(t, h) - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "pending", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}, {Kind: "message", Payload: json.RawMessage(`{"text":"second"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "pending", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}, {Kind: "message", Payload: json.RawMessage(`{"text":"second"}`)}}) if err != nil { t.Fatal(err) } return h, pending } -func runPreparedDispatch(h *dispatchHarness, ctx context.Context, pending store.EnvironmentInputReservation) <-chan preparedDispatchResult { +func runPreparedDispatch(h *dispatchHarness, ctx context.Context, pending sessions.EnvironmentInputReservation) <-chan preparedDispatchResult { out := make(chan preparedDispatchResult, 1) go func() { result, err := h.d.RunEnvironmentInput(ctx, h.lease, h.tenant, h.session.ID, pending.ID) @@ -130,7 +130,7 @@ func TestPreparedDispatchOwnerOutlivesReservationDeadline(t *testing.T) { t.Fatal(err) } stored, err := h.d.Store.ExpireEnvironmentInput(t.Context(), h.tenant, h.session.ID, pending.ID) - if err != nil || stored.State != store.EnvironmentInputAdmitted { + if err != nil || stored.State != sessions.EnvironmentInputAdmitted { t.Fatal("admitted execution lost its owner to the pending-input deadline", err) } h.write(frame.ID, proto.TypePreparationStatus, proto.PreparationStatusPayload{Handle: handle, Revision: 3, State: "started", RunID: start.RunID}) @@ -147,7 +147,7 @@ func TestPreparedDispatchOwnerOutlivesReservationDeadline(t *testing.T) { // to the executor bound to that Session, not to another executor of the tenant. func TestSelfHostedProviderReachesOnlyBoundExecutor(t *testing.T) { h, pending := preparedDispatchHarness(t) - other, err := h.s.CreateSession(t.Context(), h.tenant, store.WithFixtureModelProvider(store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), + other, err := h.s.CreateSession(t.Context(), h.tenant, store.WithFixtureModelProvider(sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)})) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/provisioning_failure_detail.go b/services/core/internal/store/provisioning_failure_detail.go deleted file mode 100644 index fc1dc3a23..000000000 --- a/services/core/internal/store/provisioning_failure_detail.go +++ /dev/null @@ -1,36 +0,0 @@ -package store - -import "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - -func (f ProvisioningFailure) detail() *sessions.ProvisioningFailureDetail { - return sanitizedProvisioningDetail(sessions.ProvisioningFailureDetail{Step: &f.Step, Index: &f.Index, ExitCode: &f.ExitCode}) -} - -func sanitizedProvisioningDetail(f sessions.ProvisioningFailureDetail) *sessions.ProvisioningFailureDetail { - if f.Step == nil { - return nil - } - result := &sessions.ProvisioningFailureDetail{} - switch *f.Step { - case ProvisioningSetupCommand: - // JSON clients can represent these integer positions exactly. - if f.Index != nil && *f.Index >= 0 && int64(*f.Index) <= 9007199254740991 { - value := *f.Index - result.Index = &value - } - case ProvisioningPythonPackages, ProvisioningNPMPackages: - case ProvisioningInitialFile, ProvisioningSkill, ProvisioningHarness: - value := *f.Step - result.Step = &value - return result - default: - return nil - } - value := *f.Step - result.Step = &value - if f.ExitCode != nil && *f.ExitCode > 0 && *f.ExitCode < 256 { - value := *f.ExitCode - result.ExitCode = &value - } - return result -} diff --git a/services/core/internal/store/public_execution_test.go b/services/core/internal/store/public_execution_test.go index 1803ff726..b34755d39 100644 --- a/services/core/internal/store/public_execution_test.go +++ b/services/core/internal/store/public_execution_test.go @@ -13,9 +13,9 @@ import ( "github.com/google/uuid" ) -func publicSession(t *testing.T, h *dispatchHarness, key string) store.Session { +func publicSession(t *testing.T, h *dispatchHarness, key string) sessions.Session { t.Helper() - value, err := h.s.CreateSession(context.Background(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"test-model","instructions":"Keep this."},"environment":{"type":"none"}}`)}) + value, err := h.s.CreateSession(context.Background(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"test-model","instructions":"Keep this."},"environment":{"type":"none"}}`)}) if err != nil { t.Fatal(err) } @@ -44,7 +44,7 @@ func TestExecutionWorkerAdmissionBindingAndRecovery(t *testing.T) { go second.Run(ctx) t.Fatal("second service acquired database") } - inputs := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"First"}]}]}`)}, {Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"Second"}]}]}`)}} + inputs := []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"First"}]}]}`)}, {Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"Second"}]}]}`)}} receipts, err := worker.SubmitInputs(ctx, h.tenant, h.session.ID, "batch", inputs) if err != nil { t.Fatal(err) @@ -112,13 +112,13 @@ func TestWorkerRestartReconcilesClaimedButPreservesQueuedWork(t *testing.T) { h.session = publicSession(t, h, "interrupted") first := h.message("first", "Already sent") ctx := context.Background() - if _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, first.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, first.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } // A native measurement committed before process loss must survive startup // reconciliation even when no Done frame can be recovered. usage := json.RawMessage(`{"tokens":{"input_tokens":10,"cached_input_tokens":4,"output_tokens":3,"reasoning_output_tokens":2,"total_tokens":13}}`) - if err := h.s.AppendTurnEvents(ctx, h.tenant, h.session.ID, first.TurnID, 1, []store.ExecutionEvent{{Kind: proto.TypeUsage, Payload: usage}}); err != nil { + if err := h.s.AppendTurnEvents(ctx, h.tenant, h.session.ID, first.TurnID, 1, []sessions.ExecutionEvent{{Kind: proto.TypeUsage, Payload: usage}}); err != nil { t.Fatal(err) } checkMeasurement := func(ended bool) { diff --git a/services/core/internal/store/public_handler_fixture_test.go b/services/core/internal/store/public_handler_fixture_test.go index f25d83c34..b28c1c8d6 100644 --- a/services/core/internal/store/public_handler_fixture_test.go +++ b/services/core/internal/store/public_handler_fixture_test.go @@ -26,6 +26,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -79,8 +80,13 @@ func publicHandler(t testing.TB, s *store.Store, db fixtureDB, keys fixtureKeyRe EnvironmentTemplates: environmentTemplates, EnvironmentTemplatesReader: templates, Skills: skillService, SkillsReader: skillStore, Agents: agentService, AgentsReader: agentStore, - Sessions: s, SessionEvents: s, SessionHistory: s, Subagents: s, - Artifacts: s, SessionAdmin: s, Environments: s, Admin: s, AdminAudit: audit, WriteAudit: audit, + Sessions: s, + SessionCreation: s, + SessionEvents: s, + Turns: s, + Items: s, + Subagents: s, + Artifacts: s, SessionAdmin: s, Environments: s, Admin: s, AdminAudit: audit, WriteAudit: audit, ExecutorConnections: strict, Metrics: strict, RuntimeObservations: strict, RuntimeHistory: strict, } for _, c := range configure { @@ -135,14 +141,26 @@ func withPolicy(policy execution.Policy) func(*api.Dependencies) { // Worker, so nothing runs them. func storeExecution(t testing.TB, s *store.Store) func(*api.Dependencies) { return func(d *api.Dependencies) { - d.Execution = &api.Execution{ExecutorURL: testExecutorURL, Admission: s, SessionArchive: strictStandIn{t}, Workspaces: strictStandIn{t}} + d.Execution = &api.Execution{ + ExecutorURL: testExecutorURL, + SessionAdmission: s, + InputAdmission: s, + SessionArchive: strictStandIn{t}, + Workspaces: strictStandIn{t}, + } } } // workerExecution runs Sessions through worker. func workerExecution(worker *execution.Worker) func(*api.Dependencies) { return func(d *api.Dependencies) { - d.Execution = &api.Execution{ExecutorURL: testExecutorURL, Admission: worker, SessionArchive: worker, Workspaces: worker} + d.Execution = &api.Execution{ + ExecutorURL: testExecutorURL, + SessionAdmission: worker, + InputAdmission: worker, + SessionArchive: worker, + Workspaces: worker, + } } } @@ -236,17 +254,17 @@ func (s strictStandIn) ExecutorConnected(context.Context, string, string) (bool, return false, nil } -func (s strictStandIn) ArchiveManagedSession(context.Context, string, string, uint64) (store.ManagedSessionArchive, error) { +func (s strictStandIn) ArchiveManagedSession(context.Context, string, string, uint64) (sessions.ManagedArchive, error) { s.unexpected("ArchiveManagedSession") - return store.ManagedSessionArchive{}, nil + return sessions.ManagedArchive{}, nil } -func (s strictStandIn) ReadEnvironmentDirectory(context.Context, store.Environment, string) (proto.WorkspaceDirectoryResult, error) { +func (s strictStandIn) ReadEnvironmentDirectory(context.Context, sessions.Environment, string) (proto.WorkspaceDirectoryResult, error) { s.unexpected("ReadEnvironmentDirectory") return proto.WorkspaceDirectoryResult{}, nil } -func (s strictStandIn) WriteEnvironmentFile(context.Context, store.Environment, string, []byte) (int64, error) { +func (s strictStandIn) WriteEnvironmentFile(context.Context, sessions.Environment, string, []byte) (int64, error) { s.unexpected("WriteEnvironmentFile") return 0, nil } diff --git a/services/core/internal/store/request_body_public_test.go b/services/core/internal/store/request_body_public_test.go index 74baf2b2f..50b0ba0ad 100644 --- a/services/core/internal/store/request_body_public_test.go +++ b/services/core/internal/store/request_body_public_test.go @@ -14,6 +14,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -50,7 +51,7 @@ func TestRequestBodyGateRejectsWithoutWritesPostgres(t *testing.T) { credential := client.created(owner, "/v1/vaults/"+vault+"/credentials", `{"name":"body","auth":{"type":"static_bearer","mcp_server_url":"https://mcp.example/mcp","token":"body-token"}}`) template := client.created(owner, "/v1/agents/environments/templates", `{"name":"body-template"}`) session := client.created(owner, "/v1/agents/sessions", `{"agent":{"model":"body-model"},"environment":{"type":"none"},"input":"Keep this Session.","metadata":{"k":"v"}}`) - prepared, err := s.CreateSession(t.Context(), ownerTenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "body-environment", + prepared, err := s.CreateSession(t.Context(), ownerTenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "body-environment", Configuration: json.RawMessage(`{"agent":{"model":"body-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace","capability_directories":[]}}`)}) if err != nil || prepared.Environment == nil { t.Fatal("fixture Environment", err) @@ -234,6 +235,6 @@ func TestRequestBodyGateExcludedRoutesPostgres(t *testing.T) { type unavailableWorkspaces struct{ strictStandIn } -func (unavailableWorkspaces) WriteEnvironmentFile(context.Context, store.Environment, string, []byte) (int64, error) { +func (unavailableWorkspaces) WriteEnvironmentFile(context.Context, sessions.Environment, string, []byte) (int64, error) { return 0, execution.ErrExecutionUnavailable } diff --git a/services/core/internal/store/runtime_allocation_state.go b/services/core/internal/store/runtime_allocation_state.go index bdaa3f278..71059f182 100644 --- a/services/core/internal/store/runtime_allocation_state.go +++ b/services/core/internal/store/runtime_allocation_state.go @@ -42,13 +42,13 @@ func (s *Store) SettleRuntimeCreation(ctx context.Context, owner RuntimeAllocati // Cancellation requests do not prove existing native work has stopped. A live, // unexpired Environment fails with the generic provisioning reason. func (s *Store) RequestRuntimeCleanup(ctx context.Context, owner RuntimeAllocation) (RuntimeAllocation, error) { - return s.requestRuntimeCleanup(ctx, owner, provisioningFailureReason, nil, false) + return s.requestRuntimeCleanup(ctx, owner, sessions.ProvisioningFailureReason, nil, false) } // ReleaseAbsentRuntimeCreation consumes provider proof that the original attempt // is settled and owns no resources. Authority revocation and release commit together. func (s *Store) ReleaseAbsentRuntimeCreation(ctx context.Context, owner RuntimeAllocation) (RuntimeAllocation, error) { - return s.requestRuntimeCleanup(ctx, owner, provisioningFailureReason, nil, true) + return s.requestRuntimeCleanup(ctx, owner, sessions.ProvisioningFailureReason, nil, true) } func (s *Store) requestRuntimeCleanup(ctx context.Context, owner RuntimeAllocation, reason string, detail *sessions.ProvisioningFailureDetail, absent bool) (RuntimeAllocation, error) { @@ -119,7 +119,7 @@ func (s *Store) mutateRuntimeAllocation(ctx context.Context, owner RuntimeAlloca return RuntimeAllocation{}, err } if previous.ID != owner.ID || previous.DeviceID != owner.DeviceID || previous.ProviderKey != owner.ProviderKey || previous.NodeID != owner.NodeID { - return RuntimeAllocation{}, ErrIdempotencyConflict + return RuntimeAllocation{}, sessions.ErrIdempotencyConflict } lookup, _ := deviceLookup(owner.TenantID, owner.EnvironmentID) var result RuntimeAllocation @@ -130,19 +130,19 @@ func (s *Store) mutateRuntimeAllocation(ctx context.Context, owner RuntimeAlloca } if live { if current.DeletedAt.Valid { - return ErrNotFound + return sessions.ErrNotFound } device, err := q.GetSessionDevice(ctx, sqlc.GetSessionDeviceParams{TenantID: lookup.TenantID, ID: session}) if err != nil { return err } if device.ID != current.RuntimeAllocation.DeviceID || device.EnvironmentID != lookup.ID { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } } row, err := apply(ctx, q, current.RuntimeAllocation) if errors.Is(err, pgx.ErrNoRows) { - return ErrTurnConflict + return sessions.ErrTurnConflict } if err == nil { result = runtimeAllocationFromRow(row, session, lookup.TenantID, current.DeletedAt, current.Expired) diff --git a/services/core/internal/store/runtime_allocations.go b/services/core/internal/store/runtime_allocations.go index 14efb2687..5cbd323e3 100644 --- a/services/core/internal/store/runtime_allocations.go +++ b/services/core/internal/store/runtime_allocations.go @@ -12,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // RuntimeAllocation retains compute ownership, not public readiness. It survives @@ -62,7 +63,7 @@ func (s *Store) ReserveRuntimeAllocation(ctx context.Context, tenant, environmen Type string `json:"type"` } if json.Unmarshal(owned.Configuration, &config) != nil || config.Type != "openai_hosted" { - return RuntimeAllocation{}, ErrInvalidInput + return RuntimeAllocation{}, sessions.ErrInvalidInput } lookup, err := deviceLookup(tenant, environment) if err != nil { @@ -77,7 +78,7 @@ func (s *Store) ReserveRuntimeAllocation(ctx context.Context, tenant, environmen previous, err := q.GetRuntimeAllocation(ctx, sqlc.GetRuntimeAllocationParams{TenantID: lookup.TenantID, EnvironmentID: lookup.ID}) if err == nil { if previous.RuntimeAllocation.ProviderKey != provider { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } result = runtimeAllocationFromRow(previous.RuntimeAllocation, session, lookup.TenantID, previous.DeletedAt, previous.Expired) result.Replayed = true @@ -94,7 +95,7 @@ func (s *Store) ReserveRuntimeAllocation(ctx context.Context, tenant, environmen return err } if current.Environment.Status == "failed" || current.Environment.Status == "expired" { - return ErrInvalidInput + return sessions.ErrInvalidInput } var nodeID pgtype.UUID active, err := q.GetRuntimeDeployment(ctx) @@ -139,7 +140,7 @@ func (s *Store) GetRuntimeAllocation(ctx context.Context, tenant, environment st } row, err := s.queries.GetRuntimeAllocation(ctx, sqlc.GetRuntimeAllocationParams{TenantID: lookup.TenantID, EnvironmentID: lookup.ID}) if errors.Is(err, pgx.ErrNoRows) { - return RuntimeAllocation{}, ErrNotFound + return RuntimeAllocation{}, sessions.ErrNotFound } if err != nil { return RuntimeAllocation{}, err @@ -176,7 +177,7 @@ func (s *Store) ListRuntimeAllocations(ctx context.Context, after string) ([]Run // released allocations; it does not acquire, renew, or mutate Runtime state. func (s *Store) ListRuntimeObservationSessions(ctx context.Context, after string, limit int) (RuntimeObservationSessionPage, error) { if limit < 1 || limit > 100 { - return RuntimeObservationSessionPage{}, ErrInvalidInput + return RuntimeObservationSessionPage{}, sessions.ErrInvalidInput } id := pgtype.UUID{Valid: true} if after != "" { diff --git a/services/core/internal/store/runtime_allocations_test.go b/services/core/internal/store/runtime_allocations_test.go index 700f9e2e7..29929f859 100644 --- a/services/core/internal/store/runtime_allocations_test.go +++ b/services/core/internal/store/runtime_allocations_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -26,10 +27,10 @@ func TestRuntimeAllocationAtomicOwnershipAndRecovery(t *testing.T) { if err != nil || bound.ID != owner.DeviceID || bound.EnvironmentID != environment.ID { t.Fatalf("binding not committed with allocation: %+v %v", bound, err) } - if _, err := w.ReserveRuntimeAllocation(t.Context(), uuid.NewString(), environment.ID, provider, runtimedevice.HashCredential(secret)); !errors.Is(err, ErrNotFound) { + if _, err := w.ReserveRuntimeAllocation(t.Context(), uuid.NewString(), environment.ID, provider, runtimedevice.HashCredential(secret)); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign allocation accepted: %v", err) } - if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, environment.ID, uuid.NewString(), runtimedevice.HashCredential(secret)); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, environment.ID, uuid.NewString(), runtimedevice.HashCredential(secret)); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatalf("provider target changed: %v", err) } awaitRelease := pgtest.ObserveExecutionLeaseRelease(t, w.pool) @@ -57,13 +58,13 @@ func TestRuntimeAllocationAtomicOwnershipAndRecovery(t *testing.T) { if err != nil || !retained.SessionDeleted || retained.ID != owner.ID { t.Fatalf("deletion discarded cleanup identity: %+v %v", retained, err) } - if _, err := next.ObserveRuntimeRunning(t.Context(), owner); !errors.Is(err, ErrNotFound) { + if _, err := next.ObserveRuntimeRunning(t.Context(), owner); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("late creation revived deleted Session: %v", err) } if _, err := next.RequestRuntimeCleanup(t.Context(), owner); err != nil { t.Fatal(err) } - if _, err := next.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, ErrTurnConflict) { + if _, err := next.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("unknown Create forgotten: %v", err) } found, cursor := false, "" @@ -172,7 +173,7 @@ func TestRuntimeAllocationExpiryAndRevocation(t *testing.T) { if _, err := pool.Exec(t.Context(), "UPDATE runtime_allocations SET kept_at=clock_timestamp()-interval '61 minutes' WHERE id=$1", owner.ID); err != nil { t.Fatal(err) } - if _, err := w.KeepRuntimeAllocation(t.Context(), owner); !errors.Is(err, ErrTurnConflict) { + if _, err := w.KeepRuntimeAllocation(t.Context(), owner); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("expired allocation renewed: %v", err) } if _, err := w.RequestRuntimeCleanup(t.Context(), owner); err != nil { diff --git a/services/core/internal/store/runtime_capabilities_pending_test.go b/services/core/internal/store/runtime_capabilities_pending_test.go index 0c6902ccc..55e7a1d48 100644 --- a/services/core/internal/store/runtime_capabilities_pending_test.go +++ b/services/core/internal/store/runtime_capabilities_pending_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -21,7 +22,7 @@ func TestManagedCapabilitiesWaitBeforeInitializationClaim(t *testing.T) { } s, db := store.NewWithCredentialCipher(pool, cipher), fixtureDB{pool: pool, cipher: cipher} tenant := uuid.NewString() - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{ + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{ Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: environmentconfig.Setup{CapabilityDirectories: []string{"/workspace/generated"}}, @@ -49,7 +50,7 @@ func TestManagedCapabilitiesWaitBeforeInitializationClaim(t *testing.T) { if err != nil || initializationState(t, s, owner.TenantID, owner.EnvironmentID) != "pending" || owner.State != "running" || provider.writes.Load() != 0 || provider.kills != 0 { t.Fatal("missing socket consumed initialization or requested cleanup", owner, err, provider.writes.Load(), provider.kills) } - if _, err := s.GetSessionExecutionBinding(t.Context(), tenant, session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := s.GetSessionExecutionBinding(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("ordinary readiness gate bypassed", err) } } diff --git a/services/core/internal/store/runtime_compute_lifecycle_test.go b/services/core/internal/store/runtime_compute_lifecycle_test.go index ea36ef8f2..e40085f4d 100644 --- a/services/core/internal/store/runtime_compute_lifecycle_test.go +++ b/services/core/internal/store/runtime_compute_lifecycle_test.go @@ -18,6 +18,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -303,7 +304,7 @@ func (f *computeLifecycleFixture) sql(query string, args ...any) { f.t.Fatal(err) } } -func (f *computeLifecycleFixture) create() (string, store.Session, store.Environment, store.RuntimeAllocation) { +func (f *computeLifecycleFixture) create() (string, sessions.Session, sessions.Environment, store.RuntimeAllocation) { t := f.t t.Helper() tenant, session, environment := managedSession(t, f.store) diff --git a/services/core/internal/store/runtime_connection_test.go b/services/core/internal/store/runtime_connection_test.go index 5c5899550..965195511 100644 --- a/services/core/internal/store/runtime_connection_test.go +++ b/services/core/internal/store/runtime_connection_test.go @@ -16,7 +16,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestManagedRuntimeConnectionTracksAuthenticatedSocket(t *testing.T) { @@ -90,7 +90,7 @@ func TestManagedRuntimeConnectionTracksAuthenticatedSocket(t *testing.T) { if err != nil || got.LastTurn != nil || got.EnvironmentInputActivity != nil { t.Fatal("connection fabricated native execution", err) } - if _, err := s.GetEnvironment(t.Context(), uuid.NewString(), environment.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := s.GetEnvironment(t.Context(), uuid.NewString(), environment.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign Environment access", err) } p.unavailable = true diff --git a/services/core/internal/store/runtime_deployment.go b/services/core/internal/store/runtime_deployment.go index 809d6025c..8eae86660 100644 --- a/services/core/internal/store/runtime_deployment.go +++ b/services/core/internal/store/runtime_deployment.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // sandboxProviders interprets the deployment's provider declarations for @@ -28,18 +29,18 @@ func checkRuntimeDeploymentAdmission(ctx context.Context, q *sqlc.Queries, insta return ErrSandboxResetAdmission } if current.AdmissionPaused { - return fmt.Errorf("%w: sandbox creation is paused for provider maintenance", ErrEnvironmentUnavailable) + return fmt.Errorf("%w: sandbox creation is paused for provider maintenance", sessions.ErrEnvironmentUnavailable) } if current.ProviderKind != "" { var spec sandbox.DeploymentSpec if json.Unmarshal(current.Specification, &spec) != nil || sandboxProviders.ValidateSpecification(current.ProviderKind, spec) != nil { - return fmt.Errorf("%w: sandbox creation requires a deployment specification", ErrEnvironmentUnavailable) + return fmt.Errorf("%w: sandbox creation requires a deployment specification", sessions.ErrEnvironmentUnavailable) } } if installation != "" { id, err := parseConnectionGeneration(installation) if err != nil || id != current.InstallationID { - return fmt.Errorf("%w: sandbox installation does not match deployment", ErrEnvironmentUnavailable) + return fmt.Errorf("%w: sandbox installation does not match deployment", sessions.ErrEnvironmentUnavailable) } } return nil diff --git a/services/core/internal/store/runtime_deployment_test.go b/services/core/internal/store/runtime_deployment_test.go index 124ab7a3c..5beb39acb 100644 --- a/services/core/internal/store/runtime_deployment_test.go +++ b/services/core/internal/store/runtime_deployment_test.go @@ -12,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func deploymentSelection() deployment.ProcessDeployment { @@ -85,7 +86,7 @@ func TestRuntimeDeploymentUnknownAllocationsBlockAdoptionAndSwitch(t *testing.T) if _, err := w.RequestRuntimeCleanup(t.Context(), owner); err != nil { t.Fatal(err) } - if _, err := w.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, ErrTurnConflict) { + if _, err := w.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("unknown creation lost cleanup ownership", err) } if err := deploymentExecution(t, w).ConfigureProcess(t.Context(), &old); err == nil { @@ -128,7 +129,7 @@ func TestRuntimeDeploymentMaintenancePreservesCreationRetriesAndOtherPlacements( old := deploymentSelection() deploymentConfigure(t, w, &old) tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted"}}`)} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted"}}`)} existing, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -140,14 +141,14 @@ func TestRuntimeDeploymentMaintenancePreservesCreationRetriesAndOtherPlacements( t.Fatal("creation retry lost identity", err) } input.IdempotencyKey = uuid.NewString() - if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, ErrEnvironmentUnavailable) { + if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Fatal("maintenance created hosted Session", err) } var count int if err := pool.QueryRow(t.Context(), "SELECT count(*) FROM sessions WHERE tenant_id=$1", tenant).Scan(&count); err != nil || count != 1 { t.Fatal("rejection left partial Session", count, err) } - if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, existing.Environment.ID, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, ErrEnvironmentUnavailable) { + if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, existing.Environment.ID, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Fatal("maintenance reserved new allocation", err) } for _, kind := range []string{"none", "self_hosted"} { @@ -159,7 +160,7 @@ func TestRuntimeDeploymentMaintenancePreservesCreationRetriesAndOtherPlacements( } old.AdmissionPaused = false deploymentConfigure(t, w, &old) - if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, existing.Environment.ID, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, ErrEnvironmentUnavailable) { + if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, existing.Environment.ID, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Fatal("wrong installation reserved resource", err) } if _, err := w.ReserveRuntimeAllocation(t.Context(), tenant, existing.Environment.ID, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())); err != nil { @@ -186,7 +187,7 @@ func TestRuntimeDeploymentMaintenanceSerializesHostedCreation(t *testing.T) { done := make(chan error, 1) tenant := uuid.NewString() go func() { - _, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`)}) + _, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`)}) done <- err }() runtimeSuspensionWaitBlocked(t, ctx, pool, blocker, done) @@ -196,7 +197,7 @@ func TestRuntimeDeploymentMaintenanceSerializesHostedCreation(t *testing.T) { if err := tx.Commit(ctx); err != nil { t.Fatal(err) } - if err := <-done; !errors.Is(err, ErrEnvironmentUnavailable) { + if err := <-done; !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Fatal("creation bypassed committed maintenance", err) } var count int diff --git a/services/core/internal/store/runtime_enrollment.go b/services/core/internal/store/runtime_enrollment.go index 2c56ed6aa..dbed9d76c 100644 --- a/services/core/internal/store/runtime_enrollment.go +++ b/services/core/internal/store/runtime_enrollment.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -41,7 +42,7 @@ func (s *Store) EnrollRuntime(ctx context.Context, environmentID, credentialHash EnvironmentID: lookup.ID, TenantID: lookup.TenantID, TokenSha256: credentialHash, }) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -51,14 +52,14 @@ func (s *Store) EnrollRuntime(ctx context.Context, environmentID, credentialHash EnvironmentID: lookup.ID, ExecutorKeyID: authority.KeyID, }) if errors.Is(err, pgx.ErrNoRows) { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } if err != nil { return err } _, err = q.BindSessionDevice(ctx, sqlc.BindSessionDeviceParams{TenantID: lookup.TenantID, ID: session, ID_2: bound.ID}) if errors.Is(err, pgx.ErrNoRows) { - return ErrDeviceBindingConflict + return sessions.ErrDeviceBindingConflict } if err != nil { return err diff --git a/services/core/internal/store/runtime_enrollment_connection_test.go b/services/core/internal/store/runtime_enrollment_connection_test.go index 3441b2562..f8d57ecb9 100644 --- a/services/core/internal/store/runtime_enrollment_connection_test.go +++ b/services/core/internal/store/runtime_enrollment_connection_test.go @@ -16,6 +16,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeenrollment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -24,7 +25,7 @@ import ( func TestEnrolledDaemonConnectionRevocationAndRestart(t *testing.T) { s, db := newTestStoreDB(t) principal := store.FixtureExecutorPrincipal(t, s, uuid.NewString()) - session, err := s.CreateSession(t.Context(), principal.TenantID, store.CreateSessionInput{ + session, err := s.CreateSession(t.Context(), principal.TenantID, sessions.CreateSession{ Creator: principal.Subject(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"fixture"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), }) diff --git a/services/core/internal/store/runtime_enrollment_test.go b/services/core/internal/store/runtime_enrollment_test.go index 630178c33..bc576a3b4 100644 --- a/services/core/internal/store/runtime_enrollment_test.go +++ b/services/core/internal/store/runtime_enrollment_test.go @@ -8,10 +8,11 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) -func runtimeEnrollmentFixture(t *testing.T, s *Store, p identity.Principal) (Session, Environment, IssuedExecutorCredential) { +func runtimeEnrollmentFixture(t *testing.T, s *Store, p identity.Principal) (sessions.Session, sessions.Environment, sessions.IssuedExecutorCredential) { t.Helper() input := environmentInput(uuid.NewString(), "self_hosted", "/workspace") input.Creator = p.Subject() @@ -41,7 +42,7 @@ func TestRuntimeEnrollmentAuthorityAndRotation(t *testing.T) { p, } { _, target, _ := runtimeEnrollmentFixture(t, s, other) - if _, err := s.EnrollRuntime(ctx, target.ID, executorDigest(key.Token)); !errors.Is(err, ErrNotFound) { + if _, err := s.EnrollRuntime(ctx, target.ID, executorDigest(key.Token)); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign target enrollment: %v", err) } } @@ -63,7 +64,7 @@ func TestRuntimeEnrollmentAuthorityAndRotation(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := s.EnrollRuntime(ctx, environment.ID, executorDigest(otherKey.Token)); !errors.Is(err, ErrDeviceBindingConflict) { + if _, err := s.EnrollRuntime(ctx, environment.ID, executorDigest(otherKey.Token)); !errors.Is(err, sessions.ErrDeviceBindingConflict) { t.Fatalf("another key replaced binding: %v", err) } rotated, err := s.RotateExecutorCredential(ctx, p, key.KeyID) @@ -94,7 +95,7 @@ func TestRuntimeEnrollmentAuthorityAndRotation(t *testing.T) { if _, ok, err := s.GetDeviceCredential(ctx, bound.DeviceID); err != nil || ok { t.Fatalf("revoked key authenticates: %v", err) } - if _, err := s.GetSessionDevice(ctx, p.TenantID, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionDevice(ctx, p.TenantID, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("revoked binding dispatchable: %v", err) } } @@ -139,7 +140,7 @@ func TestRuntimeEnrollmentConcurrentAndDeletion(t *testing.T) { if err := s.DeleteSession(t.Context(), p.TenantID, session.ID); err != nil { t.Fatal(err) } - if _, err := s.EnrollRuntime(t.Context(), environment.ID, executorDigest(key.Token)); !errors.Is(err, ErrNotFound) { + if _, err := s.EnrollRuntime(t.Context(), environment.ID, executorDigest(key.Token)); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("deleted enrollment: %v", err) } if _, ok, err := s.GetDeviceCredential(t.Context(), bound.DeviceID); err != nil || ok { diff --git a/services/core/internal/store/runtime_environment_terminal.go b/services/core/internal/store/runtime_environment_terminal.go index 286146f9e..86e7de6d4 100644 --- a/services/core/internal/store/runtime_environment_terminal.go +++ b/services/core/internal/store/runtime_environment_terminal.go @@ -3,7 +3,6 @@ package store import ( "context" "encoding/json" - "fmt" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" @@ -12,55 +11,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -// provisioningFailureReason is the safe reason for an Environment that -// failed without a confirmed failed step: timeouts, unknown effects, missing or -// old receipts, bootstrap rejection and Core restart during initialization. -const provisioningFailureReason = "Failed to provision environment: initialization did not complete" - -// Provisioning step kinds for ProvisioningFailure.Step. -const ( - ProvisioningSetupCommand = "setup" - ProvisioningPythonPackages = "python" - ProvisioningNPMPackages = "npm" - ProvisioningInitialFile = "file" - ProvisioningSkill = "skill" - ProvisioningHarness = "harness" -) - -// ProvisioningFailure identifies a confirmed failed initialization step. -// It cannot carry Runtime output: Step selects a fixed label, Index is the setup -// command position and ExitCode is the Runtime-reported status (0 when absent). -type ProvisioningFailure struct { - Step string - Index int - ExitCode int -} - -// reason renders the public Session error. The setup_commands and Python package -// labels match observed official errors (which append raw pip output for Python; -// Core never does). The npm, file and Skill labels are unverified. -// A script step without a reported exit status keeps the generic reason. -func (f ProvisioningFailure) reason() string { - label := map[string]string{ - ProvisioningPythonPackages: "Python package installation", - ProvisioningNPMPackages: "npm package installation", - }[f.Step] - if f.Step == ProvisioningSetupCommand && f.Index >= 0 { - label = fmt.Sprintf("setup_commands[%d]", f.Index) - } - switch { - case label != "" && f.ExitCode > 0 && f.ExitCode < 256: - return fmt.Sprintf("Failed to provision environment: script %q failed with exit code %d", label, f.ExitCode) - case f.Step == ProvisioningInitialFile: - return "Failed to provision environment: initial file installation failed" - case f.Step == ProvisioningHarness: - return "Failed to prepare environment: the selected Harness is unavailable. Install the supported Harness version on the Runtime and create a new Session." - case f.Step == ProvisioningSkill: - return "Failed to provision environment: Skill installation failed" - } - return provisioningFailureReason -} - func environmentFailure(row sqlc.Environment) *sessions.EnvironmentFailure { if row.Status != "failed" || !row.FailureReason.Valid || !row.FailedAt.Valid { return nil @@ -68,7 +18,7 @@ func environmentFailure(row sqlc.Environment) *sessions.EnvironmentFailure { failure := &sessions.EnvironmentFailure{Reason: row.FailureReason.String, FailedAt: row.FailedAt.Time} var detail sessions.ProvisioningFailureDetail if json.Unmarshal(row.FailureDetail, &detail) == nil { - failure.Detail = sanitizedProvisioningDetail(detail) + failure.Detail = sessions.SanitizedProvisioningDetail(detail) } return failure } diff --git a/services/core/internal/store/runtime_environment_terminal_test.go b/services/core/internal/store/runtime_environment_terminal_test.go index 18e170a18..4249b354c 100644 --- a/services/core/internal/store/runtime_environment_terminal_test.go +++ b/services/core/internal/store/runtime_environment_terminal_test.go @@ -7,6 +7,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) @@ -17,7 +18,7 @@ func TestManagedEnvironmentTerminationSettlesInputAndPreservesIdentity(t *testin s, pool := testStore(t) tenant := uuid.NewString() input := environmentInput("initial-terminal", "openai_hosted", "/workspace") - input.InitialInputs = []Input{messageInput("initial")} + input.InitialInputs = []sessions.Input{messageInput("initial")} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -45,17 +46,17 @@ func TestManagedEnvironmentTerminationSettlesInputAndPreservesIdentity(t *testin t.Fatal("terminal projection", ended, err) } // Only a hosted failure records a provisioning failure, with the generic reason. - if failure := ended.EnvironmentFailure; expired != (failure == nil) || !expired && (failure.Reason != provisioningFailureReason || failure.FailedAt.IsZero()) { + if failure := ended.EnvironmentFailure; expired != (failure == nil) || !expired && (failure.Reason != sessions.ProvisioningFailureReason || failure.FailedAt.IsZero()) { t.Fatal("terminal failure projection", failure) } if _, ok, err := s.GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { t.Fatal("terminal credential remained usable", err) } failed, err := writer.PromoteEnvironmentInput(t.Context(), tenant, session.ID, reservation.ID) - if err != nil || failed.State != EnvironmentInputFailed || len(failed.Receipts) != 0 || failed.SettledAt == nil || !failed.Deadline.Equal(reservation.Deadline) { + if err != nil || failed.State != sessions.EnvironmentInputFailed || len(failed.Receipts) != 0 || failed.SettledAt == nil || !failed.Deadline.Equal(reservation.Deadline) { t.Fatal("late preparation resurrected failed input", failed, err) } - if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "new", []Input{messageInput("later")}); !errors.Is(err, ErrEnvironmentUnavailable) || expired == errors.Is(err, ErrHostedEnvironmentFailed) { + if _, err := s.ReserveEnvironmentInput(t.Context(), tenant, session.ID, "new", []sessions.Input{messageInput("later")}); !errors.Is(err, sessions.ErrEnvironmentUnavailable) || expired == errors.Is(err, sessions.ErrHostedEnvironmentFailed) { t.Fatal("terminal environment admitted new input", err) } if _, err := s.CreateSession(t.Context(), tenant, input); err != nil { @@ -82,7 +83,7 @@ func TestManagedEnvironmentTerminationSettlesInputAndPreservesIdentity(t *testin *environment.Environment.Error != (v1.StreamError{Type: "environment_error", Code: "environment_connection_failed", Message: "The environment failed to connect."}) { t.Fatal("missing safe environment failure", events) } - if events[1].Event.Type != "error" || failure == nil || *failure != (v1.StreamError{Type: "environment_error", Code: "sandbox_error", Message: provisioningFailureReason}) { + if events[1].Event.Type != "error" || failure == nil || *failure != (v1.StreamError{Type: "environment_error", Code: "sandbox_error", Message: sessions.ProvisioningFailureReason}) { t.Fatal("missing safe error event", events) } if snapshot := last.EnvironmentFailure; snapshot == nil || snapshot.Reason != ended.EnvironmentFailure.Reason || !snapshot.FailedAt.Equal(ended.EnvironmentFailure.FailedAt) { @@ -98,10 +99,10 @@ func TestManagedEnvironmentTerminationSettlesInputAndPreservesIdentity(t *testin if next, err := s.SessionEventCursor(t.Context(), tenant, session.ID); err != nil || next != cursor { t.Fatal("cleanup repeated terminal events", next, err) } - if err := writer.ReplaceEnvironmentConnection(t.Context(), tenant, session.Environment.ID, uuid.NewString()); !errors.Is(err, ErrInvalidInput) { + if err := writer.ReplaceEnvironmentConnection(t.Context(), tenant, session.Environment.ID, uuid.NewString()); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("late connection revived terminal environment", err) } - if _, err := writer.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, ErrTurnConflict) { + if _, err := writer.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("unknown creation was forgotten", err) } environmentInputHistory(t, pool, session.ID, 0, 0) @@ -113,7 +114,7 @@ func TestManagedEnvironmentFailureRollsBackWithSessionEvent(t *testing.T) { s, pool := testStore(t) tenant := uuid.NewString() input := environmentInput("rollback-terminal", "openai_hosted", "/workspace") - input.InitialInputs = []Input{messageInput("initial")} + input.InitialInputs = []sessions.Input{messageInput("initial")} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -137,7 +138,7 @@ func TestManagedEnvironmentFailureRollsBackWithSessionEvent(t *testing.T) { if err != nil || current.Environment.Status != "pending" || current.EnvironmentInputActivity != nil { t.Fatal("partial public termination", err) } - if reservation := initialEnvironmentReservation(t, s, pool, tenant, session.ID); reservation.State != EnvironmentInputPending { + if reservation := initialEnvironmentReservation(t, s, pool, tenant, session.ID); reservation.State != sessions.EnvironmentInputPending { t.Fatal("partial input failure", reservation) } allocation, err := s.GetRuntimeAllocation(t.Context(), tenant, session.Environment.ID) @@ -148,27 +149,3 @@ func TestManagedEnvironmentFailureRollsBackWithSessionEvent(t *testing.T) { t.Fatal("partial credential revocation", err) } } - -// Reasons contain only a fixed label and an exit status. Setup and Python labels -// match official samples; npm, system, file and Skill labels are unverified. -func TestProvisioningFailureReasons(t *testing.T) { - for failure, want := range map[ProvisioningFailure]string{ - {Step: ProvisioningSetupCommand, Index: 0, ExitCode: 3}: `Failed to provision environment: script "setup_commands[0]" failed with exit code 3`, - {Step: ProvisioningSetupCommand, Index: 12, ExitCode: 1}: `Failed to provision environment: script "setup_commands[12]" failed with exit code 1`, - {Step: ProvisioningPythonPackages, ExitCode: 1}: `Failed to provision environment: script "Python package installation" failed with exit code 1`, - {Step: ProvisioningNPMPackages, ExitCode: 1}: `Failed to provision environment: script "npm package installation" failed with exit code 1`, - {Step: ProvisioningInitialFile}: "Failed to provision environment: initial file installation failed", - {Step: ProvisioningSkill}: "Failed to provision environment: Skill installation failed", - // Missing or impossible statuses, unknown steps and old receipts stay generic. - {Step: ProvisioningSetupCommand, Index: 0}: provisioningFailureReason, - {Step: ProvisioningSetupCommand, Index: -1, ExitCode: 3}: provisioningFailureReason, - {Step: ProvisioningPythonPackages, ExitCode: 256}: provisioningFailureReason, - {Step: ProvisioningNPMPackages, ExitCode: -9}: provisioningFailureReason, - {Step: "configure", ExitCode: 1}: provisioningFailureReason, - {}: provisioningFailureReason, - } { - if got := failure.reason(); got != want || len(got) > 256 { - t.Errorf("%+v: %q", failure, got) - } - } -} diff --git a/services/core/internal/store/runtime_file_admission_test.go b/services/core/internal/store/runtime_file_admission_test.go index 9e46539ad..c647eec26 100644 --- a/services/core/internal/store/runtime_file_admission_test.go +++ b/services/core/internal/store/runtime_file_admission_test.go @@ -8,10 +8,12 @@ import ( "time" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -func runtimeFileWriteKey(owner RuntimeAllocation) FileWriteIdentity { - return FileWriteIdentity{ID: uuid.NewString(), DeviceID: owner.DeviceID, RequestSHA256: strings.Repeat("a", 64)} +func runtimeFileWriteKey(owner RuntimeAllocation) sessions.FileWriteIdentity { + return sessions.FileWriteIdentity{ID: uuid.NewString(), DeviceID: owner.DeviceID, RequestSHA256: strings.Repeat("a", 64)} } func TestRuntimeFileWriteRequiresRunningComputeBeforeNewIntent(t *testing.T) { @@ -23,10 +25,10 @@ func TestRuntimeFileWriteRequiresRunningComputeBeforeNewIntent(t *testing.T) { write, err := w.ReserveEnvironmentFileWrite(t.Context(), owner.TenantID, owner.EnvironmentID, key) blocked := phase != "disabled" && phase != "running" if blocked { - if !errors.Is(err, ErrTurnConflict) { + if !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("suspended compute admitted a new write", write, err) } - if _, err := s.GetEnvironmentFileWrite(t.Context(), owner.TenantID, owner.EnvironmentID, key.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetEnvironmentFileWrite(t.Context(), owner.TenantID, owner.EnvironmentID, key.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("rejected admission retained a blocking intent", err) } runtimeSuspensionSQL(t, pool, `UPDATE runtime_allocations SET compute_phase='running',compute_retained_until=NULL WHERE id=$1`, owner.ID) @@ -59,10 +61,10 @@ func TestRuntimeFileWritePhaseFencePreservesExistingReceipts(t *testing.T) { } changed := key changed.RequestSHA256 = strings.Repeat("b", 64) - if _, err := w.ReserveEnvironmentFileWrite(t.Context(), owner.TenantID, owner.EnvironmentID, changed); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := w.ReserveEnvironmentFileWrite(t.Context(), owner.TenantID, owner.EnvironmentID, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("phase fence masked changed retry identity", err) } - if _, err := w.ReserveEnvironmentFileWrite(t.Context(), owner.TenantID, owner.EnvironmentID, runtimeFileWriteKey(owner)); !errors.Is(err, ErrTurnConflict) { + if _, err := w.ReserveEnvironmentFileWrite(t.Context(), owner.TenantID, owner.EnvironmentID, runtimeFileWriteKey(owner)); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("receipt authorized a successor while waking", err) } if state == "pending" { @@ -106,7 +108,7 @@ func TestRuntimeFileWriteAndQuiesceSerializeBothOrders(t *testing.T) { if err := tx.Commit(ctx); err != nil { t.Fatal(err) } - if err := <-done; !errors.Is(err, ErrTurnConflict) { + if err := <-done; !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("competing durable owner bypassed the phase fence", err) } allocation, err := s.GetRuntimeAllocation(ctx, owner.TenantID, owner.EnvironmentID) @@ -115,7 +117,7 @@ func TestRuntimeFileWriteAndQuiesceSerializeBothOrders(t *testing.T) { } write, err := s.GetEnvironmentFileWrite(ctx, owner.TenantID, owner.EnvironmentID, key.ID) if first == "quiesce" { - if allocation.ComputePhase != "quiescing" || !errors.Is(err, ErrNotFound) { + if allocation.ComputePhase != "quiescing" || !errors.Is(err, sessions.ErrNotFound) { t.Fatal("late write survived quiesce", allocation.ComputePhase, write, err) } } else if allocation.ComputePhase != "running" || err != nil || write.State != "pending" { diff --git a/services/core/internal/store/runtime_history_acceptance_test.go b/services/core/internal/store/runtime_history_acceptance_test.go index bd3340e56..1334b9e45 100644 --- a/services/core/internal/store/runtime_history_acceptance_test.go +++ b/services/core/internal/store/runtime_history_acceptance_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory/postgresreader" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -30,7 +31,7 @@ func historyBackend(t *testing.T, s *store.Store) *postgresreader.Reader { func historyOwner(t *testing.T, s *store.Store) runtimehistory.Scope { t.Helper() tenant := uuid.NewString() - session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"fixture-model"},"environment":{"type":"openai_hosted","workspace_directory":"/workspace","capability_directories":[]}}`)}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"fixture-model"},"environment":{"type":"openai_hosted","workspace_directory":"/workspace","capability_directories":[]}}`)}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/runtime_idle_clock_test.go b/services/core/internal/store/runtime_idle_clock_test.go index bdb5fec9d..d1315d1e7 100644 --- a/services/core/internal/store/runtime_idle_clock_test.go +++ b/services/core/internal/store/runtime_idle_clock_test.go @@ -66,7 +66,7 @@ func verifyManagedIdleClock(t *testing.T, s, w *Store, owner RuntimeAllocation, t.Fatal("idle clock did not use committed terminal ingestion", activity, before, after, err) } until := runtimeDatabaseTime(t, s).Add(time.Hour) - if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, idleTimeout); !errors.Is(err, ErrTurnConflict) { + if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, idleTimeout); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("new completion admitted premature idle", err) } // Advance only the internal activity age; the remote public timestamp remains unchanged. @@ -94,7 +94,7 @@ func TestManagedIdleClockIgnoresRootHostSkew(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := w.CompleteExecution(t.Context(), owner.TenantID, owner.SessionID, turn, sessions.TurnCompleted, outcome, "", 0); !errors.Is(err, ErrTurnConflict) { + if _, err := w.CompleteExecution(t.Context(), owner.TenantID, owner.SessionID, turn, sessions.TurnCompleted, outcome, "", 0); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("terminal replay accepted", err) } unchanged, err := w.RuntimeActivity(t.Context(), owner) @@ -167,7 +167,7 @@ func TestUnmanagedRootCompletionPreservesHostSkew(t *testing.T) { environment["workspace_directory"] = "/workspace" } configuration, _ := json.Marshal(map[string]any{"agent": map[string]string{"model": "test"}, "environment": environment}) - session, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: configuration}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: configuration}) if err != nil { t.Fatal(err) } @@ -183,7 +183,7 @@ func TestUnmanagedRootCompletionPreservesHostSkew(t *testing.T) { if err != nil || read.Status != sessions.TurnCompleted || read.CompletedAt.UnixMilli() != source { t.Fatal("public native timestamp rewritten", read, err) } - if _, err := s.CompleteExecution(t.Context(), tenant, session.ID, input.TurnID, sessions.TurnCompleted, outcome, "", input.Sequence); !errors.Is(err, ErrTurnConflict) { + if _, err := s.CompleteExecution(t.Context(), tenant, session.ID, input.TurnID, sessions.TurnCompleted, outcome, "", input.Sequence); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("terminal replay accepted", err) } }) @@ -199,7 +199,7 @@ func TestRootCompletionRejectsNonpositiveSourceTime(t *testing.T) { input := submitMessage(t, s, tenant, session.ID, "invalid-clock") transition(t, s, tenant, session.ID, input.TurnID, sessions.TurnQueued, sessions.TurnInProgress) outcome := json.RawMessage(fmt.Sprintf(`{"done":{"source_completed_at_ms":%d}}`, source)) - if _, err := s.CompleteExecution(t.Context(), tenant, session.ID, input.TurnID, sessions.TurnCompleted, outcome, "", input.Sequence); !errors.Is(err, ErrInvalidInput) { + if _, err := s.CompleteExecution(t.Context(), tenant, session.ID, input.TurnID, sessions.TurnCompleted, outcome, "", input.Sequence); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("invalid native timestamp accepted", err) } }) diff --git a/services/core/internal/store/runtime_initialization.go b/services/core/internal/store/runtime_initialization.go index 1d38f09ad..d156ef9ad 100644 --- a/services/core/internal/store/runtime_initialization.go +++ b/services/core/internal/store/runtime_initialization.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -12,13 +13,13 @@ import ( func (s *Store) requireInitializedEnvironment(ctx context.Context, tenant, session pgtype.UUID) error { ready, err := s.queries.GetSessionInitializationReady(ctx, sqlc.GetSessionInitializationReadyParams{TenantID: tenant, ID: session}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } if !ready { - return ErrNotFound + return sessions.ErrNotFound } return nil } diff --git a/services/core/internal/store/runtime_initialization_test.go b/services/core/internal/store/runtime_initialization_test.go index 8b445181f..5aefa29ca 100644 --- a/services/core/internal/store/runtime_initialization_test.go +++ b/services/core/internal/store/runtime_initialization_test.go @@ -18,6 +18,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -51,7 +52,7 @@ func TestEnvironmentInitializationCompletionUnknownAndRestart(t *testing.T) { } s, db := store.NewWithCredentialCipher(pool, cipher), fixtureDB{pool: pool, cipher: cipher} tenant := uuid.NewString() - input := store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/a", Data: []byte("first")}, {Type: "inline", Path: "/workspace/b", Data: []byte("second")}}} + input := sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/a", Data: []byte("first")}, {Type: "inline", Path: "/workspace/b", Data: []byte("second")}}} if setupOnly { input.InitialFiles = nil input.Initialization = environmentconfig.Setup{Env: map[string]string{"VALUE": "private"}, Packages: v1.EnvironmentPackages{NPM: []string{"is-number@7.0.0"}}, Commands: []environmentconfig.SetupCommand{{Command: "touch first"}, {Command: "test -f first"}}} @@ -72,10 +73,10 @@ func TestEnvironmentInitializationCompletionUnknownAndRestart(t *testing.T) { } p := &initializingProvider{lifecycleProvider: lifecycleProvider{resources: map[string]sandbox.Info{}}, initializationPeer: initializationPeer{deferred: true}} p.apply = func(_ proto.RuntimePreparePayload, _ []byte) proto.RuntimePrepareResultPayload { - if _, err := s.GetSessionDevice(t.Context(), tenant, session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := s.GetSessionDevice(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Error("premature file access", err) } - if _, err := s.GetSessionExecutionBinding(t.Context(), tenant, session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := s.GetSessionExecutionBinding(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Error("premature execution", err) } if mode == "uncertain" { @@ -156,7 +157,7 @@ func TestManagedRuntimePreparationAllOperationsUsePeer(t *testing.T) { } tenant := uuid.NewString() fileBody := bytes.Repeat([]byte("bounded bytes"), 12000) - session, environment := hostedFailureSession(t, s, tenant, store.CreateSessionInput{ + session, environment := hostedFailureSession(t, s, tenant, sessions.CreateSession{ InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/first", Data: fileBody}}, Initialization: environmentconfig.Setup{Skills: []environmentconfig.Skill{hostedFailureSkill(t)}, Plugins: []environmentconfig.Plugin{{Metadata: agentplugin.Metadata{Type: "inline", Name: "plugin", Description: "A plugin."}, Archive: archive.Bytes()}}, Packages: v1.EnvironmentPackages{NPM: []string{"is-number@7.0.0"}, Python: []string{"packaging==24.2"}}, Commands: []environmentconfig.SetupCommand{{Command: "read installed bundles and create directory"}}, CapabilityDirectories: []string{"/workspace/generated"}}, }) diff --git a/services/core/internal/store/runtime_input_admission_test.go b/services/core/internal/store/runtime_input_admission_test.go index 972450d40..6f093aa19 100644 --- a/services/core/internal/store/runtime_input_admission_test.go +++ b/services/core/internal/store/runtime_input_admission_test.go @@ -8,13 +8,14 @@ import ( "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestManagedRuntimeMaintenancePreservesCancelAndRetry(t *testing.T) { s, db := newManagedTestStoreDB(t) tenant, session, _ := managedSession(t, s) - inputs := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"accepted work"}`)}} + inputs := []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"accepted work"}`)}} accepted, err := s.SubmitInputs(t.Context(), tenant, session.ID, "work", inputs) if err != nil { t.Fatal(err) @@ -22,10 +23,10 @@ func TestManagedRuntimeMaintenancePreservesCancelAndRetry(t *testing.T) { p := &lifecycleProvider{resources: map[string]sandbox.Info{}} w, stop := managedWorkerMode(t, s, db, uuid.NewString(), p, true) defer stop() - if _, err := w.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: session.Configuration}); !errors.Is(err, store.ErrEnvironmentUnavailable) { + if _, err := w.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: session.Configuration}); !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Fatal("maintenance accepted new hosted Session", err) } - cancel := []store.Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}} + cancel := []sessions.Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}} first, err := w.SubmitInputs(t.Context(), tenant, session.ID, "cancel", cancel) if err != nil || len(first) != 1 { t.Fatal("existing work cannot be cancelled", first, err) @@ -38,7 +39,7 @@ func TestManagedRuntimeMaintenancePreservesCancelAndRetry(t *testing.T) { if err != nil || len(retry) != 1 || !retry[0].Replayed || retry[0].Sequence != accepted[0].Sequence || retry[0].TurnID != accepted[0].TurnID { t.Fatal("matching input retry lost its accepted outcome", retry, err) } - if _, err := w.SubmitInputs(t.Context(), uuid.NewString(), session.ID, "cancel", cancel); !errors.Is(err, store.ErrNotFound) { + if _, err := w.SubmitInputs(t.Context(), uuid.NewString(), session.ID, "cancel", cancel); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("maintenance weakened tenant isolation", err) } if p.creates != 0 { diff --git a/services/core/internal/store/runtime_lifecycle_nodes.go b/services/core/internal/store/runtime_lifecycle_nodes.go index 53090d591..c931cdf05 100644 --- a/services/core/internal/store/runtime_lifecycle_nodes.go +++ b/services/core/internal/store/runtime_lifecycle_nodes.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -106,7 +107,7 @@ func (s *Store) ResolveRuntimeLifecycleNode(ctx context.Context, tenant, environ } row, err := s.queries.GetRuntimeLifecyclePlacement(ctx, sqlc.GetRuntimeLifecyclePlacementParams{TenantID: lookup.TenantID, ID: lookup.ID}) if errors.Is(err, pgx.ErrNoRows) { - return "", ErrNotFound + return "", sessions.ErrNotFound } if err != nil { return "", err diff --git a/services/core/internal/store/runtime_lifecycle_nodes_test.go b/services/core/internal/store/runtime_lifecycle_nodes_test.go index a67b061d7..858b50258 100644 --- a/services/core/internal/store/runtime_lifecycle_nodes_test.go +++ b/services/core/internal/store/runtime_lifecycle_nodes_test.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -26,7 +27,7 @@ func lifecycleTestNode(t *testing.T, s *Store) string { onlineManagerNode(t, s, id) return id } -func lifecycleTestSession(t *testing.T, s *Store, node string) (string, Session) { +func lifecycleTestSession(t *testing.T, s *Store, node string) (string, sessions.Session) { t.Helper() tenant := uuid.NewString() session, err := createSessionOnNode(t, s, tenant, managerSessionInput(uuid.NewString()), node) @@ -134,7 +135,7 @@ func TestRuntimeLifecycleNodeInventoryAndRouting(t *testing.T) { } } checkRoute(other, nil) // Pending has no allocation yet. - if _, err := w.ResolveRuntimeLifecycleNode(t.Context(), uuid.NewString(), environment); !errors.Is(err, ErrNotFound) { + if _, err := w.ResolveRuntimeLifecycleNode(t.Context(), uuid.NewString(), environment); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("tenant boundary", err) } owner, err := w.ReserveRuntimeAllocation(t.Context(), tenant, environment, d.InstallationID, runtimedevice.HashCredential("runtime")) diff --git a/services/core/internal/store/runtime_lifecycle_test.go b/services/core/internal/store/runtime_lifecycle_test.go index 42d0abdf8..8a6af2daf 100644 --- a/services/core/internal/store/runtime_lifecycle_test.go +++ b/services/core/internal/store/runtime_lifecycle_test.go @@ -16,6 +16,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -112,10 +113,10 @@ func managedWorkerMode(t *testing.T, s *store.Store, db fixtureDB, key string, p return w, stop } -func managedSession(t *testing.T, s *store.Store) (string, store.Session, store.Environment) { +func managedSession(t *testing.T, s *store.Store) (string, sessions.Session, sessions.Environment) { t.Helper() tenant := uuid.NewString() - v, e := s.CreateSession(t.Context(), tenant, store.WithFixtureModelProvider(store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"enabled"}}}`)})) + v, e := s.CreateSession(t.Context(), tenant, store.WithFixtureModelProvider(sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"enabled"}}}`)})) if e != nil { t.Fatal(e) } diff --git a/services/core/internal/store/runtime_node_generations_test.go b/services/core/internal/store/runtime_node_generations_test.go index f0e2d8e00..0ea57bc7c 100644 --- a/services/core/internal/store/runtime_node_generations_test.go +++ b/services/core/internal/store/runtime_node_generations_test.go @@ -7,6 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -33,7 +34,7 @@ func generationHeartbeat(t *testing.T, s *Store, node deployment.Enrollment, con } } -func placedGeneration(t *testing.T, s *Store, session Session) (string, int64) { +func placedGeneration(t *testing.T, s *Store, session sessions.Session) (string, int64) { t.Helper() var node string var generation int64 diff --git a/services/core/internal/store/runtime_node_lifecycle_fixture_test.go b/services/core/internal/store/runtime_node_lifecycle_fixture_test.go index 66c3acae0..da9e9c8d1 100644 --- a/services/core/internal/store/runtime_node_lifecycle_fixture_test.go +++ b/services/core/internal/store/runtime_node_lifecycle_fixture_test.go @@ -21,6 +21,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -165,10 +166,10 @@ func (f *nodeIsolationFixture) online(id string) { f.t.Fatal(err) } } -func (f *nodeIsolationFixture) session(node string, initialize bool) (string, store.Session, store.Environment) { +func (f *nodeIsolationFixture) session(node string, initialize bool) (string, sessions.Session, sessions.Environment) { f.t.Helper() tenant := uuid.NewString() - input := store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage("{\"agent\":{\"model\":\"test\"},\"environment\":{\"type\":\"openai_hosted\"}}")} + input := sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage("{\"agent\":{\"model\":\"test\"},\"environment\":{\"type\":\"openai_hosted\"}}")} if initialize { input.InitialFiles = []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/seed", Data: []byte("retained")}} } @@ -219,7 +220,7 @@ func (f *nodeIsolationFixture) session(node string, initialize bool) (string, st } return tenant, session, env } -func (f *nodeIsolationFixture) provision(tenant string, env store.Environment) store.RuntimeAllocation { +func (f *nodeIsolationFixture) provision(tenant string, env sessions.Environment) store.RuntimeAllocation { f.t.Helper() ctx, cancel := context.WithTimeout(f.t.Context(), 3*time.Second) defer cancel() diff --git a/services/core/internal/store/runtime_nodes.go b/services/core/internal/store/runtime_nodes.go index 605d2c833..6e42de4b1 100644 --- a/services/core/internal/store/runtime_nodes.go +++ b/services/core/internal/store/runtime_nodes.go @@ -8,6 +8,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) type RuntimeNodeAllocation struct { @@ -39,7 +41,7 @@ func (s *Store) ListNodeRuntimeAllocations(ctx context.Context, nodeID string) ( return nil, err } if _, err := s.queries.GetRuntimeNode(ctx, id); errors.Is(err, pgx.ErrNoRows) { - return nil, ErrNotFound + return nil, sessions.ErrNotFound } else if err != nil { return nil, err } diff --git a/services/core/internal/store/runtime_nodes_test.go b/services/core/internal/store/runtime_nodes_test.go index 28c129cc3..e075cdbb3 100644 --- a/services/core/internal/store/runtime_nodes_test.go +++ b/services/core/internal/store/runtime_nodes_test.go @@ -11,6 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) @@ -41,13 +42,13 @@ func onlineManagerNode(t *testing.T, s *Store, id string) string { } return connection } -func managerSessionInput(key string) CreateSessionInput { +func managerSessionInput(key string) sessions.CreateSession { return environmentInput(key, "openai_hosted", "/workspace") } // createSessionOnNode steers automatic placement in multi-node tests: only node // stays provider-ready while the Session is created. -func createSessionOnNode(t *testing.T, s *Store, tenant string, input CreateSessionInput, node string) (Session, error) { +func createSessionOnNode(t *testing.T, s *Store, tenant string, input sessions.CreateSession, node string) (sessions.Session, error) { t.Helper() // Use the same authenticated readiness observations as the scheduler. The // compatibility provider_ready column alone is not admission authority. @@ -100,7 +101,7 @@ func sessionRuntimePlacement(ctx context.Context, s *Store, tenant, session stri return sessionPlacement{}, err } if value.Environment == nil { - return sessionPlacement{}, ErrNotFound + return sessionPlacement{}, sessions.ErrNotFound } id, err := parseID(value.Environment.ID) if err != nil { @@ -108,7 +109,7 @@ func sessionRuntimePlacement(ctx context.Context, s *Store, tenant, session stri } p, err := s.queries.GetRuntimePlacement(ctx, id) if errors.Is(err, pgx.ErrNoRows) { - return sessionPlacement{}, ErrNotFound + return sessionPlacement{}, sessions.ErrNotFound } return sessionPlacement{NodeID: runtimeUUID(p.NodeID), Available: p.Available && !p.ReleasedAt.Valid}, err } @@ -116,7 +117,7 @@ func TestRuntimeNodesAtomicPlacementAndRetry(t *testing.T) { s, _, d := managerFixture(t, 1, 4) tenant := uuid.NewString() var wg sync.WaitGroup - results := make(chan Session, 16) + results := make(chan sessions.Session, 16) failures := make(chan error, 16) for range 16 { wg.Add(1) @@ -141,7 +142,7 @@ func TestRuntimeNodesAtomicPlacementAndRetry(t *testing.T) { if successes != 1 { t.Fatal("overbooked node", successes) } - var retained Session + var retained sessions.Session for session := range results { if session.ID != "" { retained = session @@ -170,7 +171,7 @@ func TestRuntimeNodesAtomicPlacementAndRetry(t *testing.T) { if err != nil || replay.ID != first.ID { t.Fatal("offline retry changed Session", replay, err) } - if _, err := sessionRuntimePlacement(t.Context(), s, uuid.NewString(), first.ID); !errors.Is(err, ErrNotFound) { + if _, err := sessionRuntimePlacement(t.Context(), s, uuid.NewString(), first.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign placement leaked", err) } placement, err := sessionRuntimePlacement(t.Context(), s, tenant, first.ID) diff --git a/services/core/internal/store/runtime_observation.go b/services/core/internal/store/runtime_observation.go index 6cbcce463..6c0db28ed 100644 --- a/services/core/internal/store/runtime_observation.go +++ b/services/core/internal/store/runtime_observation.go @@ -4,6 +4,7 @@ import ( "context" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5/pgtype" ) @@ -13,7 +14,7 @@ func (s *Store) RecordRuntimeObservation(ctx context.Context, owner RuntimeAlloc switch diagnostic { case "", "node_unavailable", "resource_missing", "compute_unconfirmed", "ownership_mismatch", "provider_unavailable": default: - return ErrInvalidInput + return sessions.ErrInvalidInput } if owner.NodeID == "" { return nil diff --git a/services/core/internal/store/runtime_observation_test.go b/services/core/internal/store/runtime_observation_test.go index 9ec0a46fb..b072b1464 100644 --- a/services/core/internal/store/runtime_observation_test.go +++ b/services/core/internal/store/runtime_observation_test.go @@ -6,6 +6,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -55,7 +56,7 @@ func TestRuntimeNodeObservationRetainsResourcesAndFencesStaleResults(t *testing. if err != nil || recovered.ObservationError != "" || recovered.ID != owner.ID { t.Fatal("stale error replaced recovered observation", recovered, err) } - if err := w.RecordRuntimeObservation(t.Context(), current, "secret provider exception"); !errors.Is(err, ErrInvalidInput) { + if err := w.RecordRuntimeObservation(t.Context(), current, "secret provider exception"); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("raw diagnostics accepted", err) } } diff --git a/services/core/internal/store/runtime_pending_test.go b/services/core/internal/store/runtime_pending_test.go index 96fc3f6d2..0aa82eca0 100644 --- a/services/core/internal/store/runtime_pending_test.go +++ b/services/core/internal/store/runtime_pending_test.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -15,7 +16,7 @@ import ( func TestManagedRuntimeAutomaticBootstrapRecoversCommittedSessions(t *testing.T) { s, db := newManagedTestStoreDB(t) tenant, idle, idleEnvironment := managedSession(t, s) - initial, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted"}}`), InitialInputs: []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"hello"}`)}}}) + initial, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted"}}`), InitialInputs: []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"hello"}`)}}}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/runtime_suspension.go b/services/core/internal/store/runtime_suspension.go index 2ff0a10c3..90d4afff3 100644 --- a/services/core/internal/store/runtime_suspension.go +++ b/services/core/internal/store/runtime_suspension.go @@ -7,6 +7,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -31,18 +32,18 @@ func runtimeActivity(row sqlc.GetRuntimeActivityRow) RuntimeActivity { // Revision and the existing Session lock fence a stale lifecycle observation. func (s *Store) SetRuntimeCompute(ctx context.Context, owner RuntimeAllocation, phase string, state json.RawMessage, retainedUntil *time.Time, idleTimeout time.Duration) (RuntimeAllocation, error) { if !runtimeComputeTransition(owner.ComputePhase, phase) || !json.Valid(state) || (owner.ComputePhase == "running" && phase == "quiescing" && idleTimeout <= 0) { - return RuntimeAllocation{}, ErrInvalidInput + return RuntimeAllocation{}, sessions.ErrInvalidInput } if phase != "running" && (retainedUntil == nil || retainedUntil.IsZero()) { - return RuntimeAllocation{}, ErrInvalidInput + return RuntimeAllocation{}, sessions.ErrInvalidInput } var object map[string]json.RawMessage if json.Unmarshal(state, &object) != nil || object == nil { - return RuntimeAllocation{}, ErrInvalidInput + return RuntimeAllocation{}, sessions.ErrInvalidInput } return s.mutateRuntimeAllocation(ctx, owner, true, func(ctx context.Context, q *sqlc.Queries, row sqlc.RuntimeAllocation) (sqlc.RuntimeAllocation, error) { if row.ComputeRevision != owner.ComputeRevision || row.ComputePhase != owner.ComputePhase { - return sqlc.RuntimeAllocation{}, ErrTurnConflict + return sqlc.RuntimeAllocation{}, sessions.ErrTurnConflict } if (phase == "quiescing" && row.ComputePhase == "running") || (phase == "suspending" && row.ComputePhase == "quiescing") { activity, err := q.GetRuntimeActivity(ctx, row.ID) @@ -50,10 +51,10 @@ func (s *Store) SetRuntimeCompute(ctx context.Context, owner RuntimeAllocation, return sqlc.RuntimeAllocation{}, err } if activity.Busy || activity.ComputeWakeRequested { - return sqlc.RuntimeAllocation{}, ErrTurnConflict + return sqlc.RuntimeAllocation{}, sessions.ErrTurnConflict } if phase == "quiescing" && (!runtimeActivity(activity).ReadyToSuspend(idleTimeout) || row.ComputeActivityAt.Time.After(owner.ComputeActivityAt)) { - return sqlc.RuntimeAllocation{}, ErrTurnConflict + return sqlc.RuntimeAllocation{}, sessions.ErrTurnConflict } } if row.ComputePhase == "suspended" && phase == "restoring" { @@ -104,7 +105,7 @@ func (s *Store) RuntimeActivity(ctx context.Context, owner RuntimeAllocation) (R } row, err := s.queries.GetRuntimeActivity(ctx, id) if errors.Is(err, pgx.ErrNoRows) { - return RuntimeActivity{}, ErrNotFound + return RuntimeActivity{}, sessions.ErrNotFound } if err != nil { return RuntimeActivity{}, err @@ -126,13 +127,13 @@ func (s *Store) TouchRuntimeActivity(ctx context.Context, tenant, environment st return s.withPublicSession(ctx, tenant, owned.SessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { current, err := q.GetEnvironment(ctx, sqlc.GetEnvironmentParams{TenantID: lookup.TenantID, ID: lookup.ID}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } if current.Environment.SessionID != session { - return ErrNotFound + return sessions.ErrNotFound } return q.TouchRuntimeActivity(ctx, sqlc.TouchRuntimeActivityParams{TenantID: lookup.TenantID, EnvironmentID: lookup.ID}) }) @@ -176,7 +177,7 @@ func (s *Store) CountRuntimeRetainedAllocations(ctx context.Context, provider st func checkRuntimeComputeAdmission(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { blocked, err := q.RuntimeComputeBlocksAdmission(ctx, session) if err == nil && blocked { - return ErrTurnConflict + return sessions.ErrTurnConflict } return err } diff --git a/services/core/internal/store/runtime_suspension_concurrency_test.go b/services/core/internal/store/runtime_suspension_concurrency_test.go index cdf8020ba..1e32c700b 100644 --- a/services/core/internal/store/runtime_suspension_concurrency_test.go +++ b/services/core/internal/store/runtime_suspension_concurrency_test.go @@ -59,7 +59,7 @@ func TestRuntimeSuspensionClaimReadsPhaseAfterSessionLock(t *testing.T) { ctx, tx, blocker := runtimeSuspensionLockedSession(t, pool, owner.SessionID) done := make(chan error, 1) go func() { - _, err := w.TransitionTurn(ctx, owner.TenantID, owner.SessionID, turn, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err := w.TransitionTurn(ctx, owner.TenantID, owner.SessionID, turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) done <- err }() runtimeSuspensionWaitBlocked(t, ctx, pool, blocker, done) @@ -72,7 +72,7 @@ func TestRuntimeSuspensionClaimReadsPhaseAfterSessionLock(t *testing.T) { } err := <-done blocked := phase != "disabled" && phase != "running" - if blocked && !errors.Is(err, ErrTurnConflict) || !blocked && err != nil { + if blocked && !errors.Is(err, sessions.ErrTurnConflict) || !blocked && err != nil { t.Fatal("incorrect claim outcome", phase, err) } got, err := s.GetTurn(ctx, owner.TenantID, owner.SessionID, turn) @@ -97,17 +97,17 @@ func TestRuntimeSuspensionPromotionRetainsPendingInput(t *testing.T) { s, w, pool, owner := runtimeSuspensionFixture(t) runtimeSuspensionSQL(t, pool, `UPDATE runtime_allocations SET compute_phase='waking',compute_retained_until=clock_timestamp()+interval '1 hour' WHERE id=$1`, owner.ID) pending := reserveEnvironmentInput(t, s, owner.TenantID, owner.SessionID, "during-wake") - if _, err := w.PromoteEnvironmentInput(t.Context(), owner.TenantID, owner.SessionID, pending.ID); !errors.Is(err, ErrTurnConflict) { + if _, err := w.PromoteEnvironmentInput(t.Context(), owner.TenantID, owner.SessionID, pending.ID); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("waking allocation promoted model input", err) } got, err := s.GetEnvironmentInputReservation(t.Context(), owner.TenantID, owner.SessionID, pending.ID) - if err != nil || got.State != EnvironmentInputPending || got.SettledAt != nil || len(got.Receipts) != 0 { + if err != nil || got.State != sessions.EnvironmentInputPending || got.SettledAt != nil || len(got.Receipts) != 0 { t.Fatal("blocked promotion partially committed", got, err) } environmentInputHistory(t, pool, owner.SessionID, 0, 0) runtimeSuspensionSQL(t, pool, `UPDATE runtime_allocations SET compute_phase='running',compute_retained_until=NULL WHERE id=$1`, owner.ID) got, err = w.PromoteEnvironmentInput(t.Context(), owner.TenantID, owner.SessionID, pending.ID) - if err != nil || got.State != EnvironmentInputAdmitted || len(got.Receipts) != 2 { + if err != nil || got.State != sessions.EnvironmentInputAdmitted || len(got.Receipts) != 2 { t.Fatal("pending request could not resume once running", got, err) } } @@ -143,7 +143,7 @@ func TestRuntimeSuspensionCaptureRechecksNewPendingWork(t *testing.T) { if err := tx.Commit(ctx); err != nil { t.Fatal(err) } - if err := <-done; !errors.Is(err, ErrTurnConflict) { + if err := <-done; !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("capture ignored work admitted after quiesce", err) } got, err := s.GetRuntimeAllocation(ctx, owner.TenantID, owner.EnvironmentID) @@ -173,7 +173,7 @@ func TestRuntimeSuspensionWakeUsesSessionLock(t *testing.T) { t.Fatal(err) } err := <-done - if deleted && !errors.Is(err, ErrNotFound) || !deleted && err != nil { + if deleted && !errors.Is(err, sessions.ErrNotFound) || !deleted && err != nil { t.Fatal("wake did not observe locked state", err) } got, err := s.GetRuntimeAllocation(ctx, owner.TenantID, owner.EnvironmentID) @@ -203,13 +203,13 @@ func TestRuntimeSuspensionQuiesceCannotOvertakeClaim(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := transitionTurn(ctx, w.queries.WithTx(tx), params, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress, Outcome: json.RawMessage(`{}`)}); err != nil { + if _, err := transitionTurn(ctx, w.queries.WithTx(tx), params, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress, Outcome: json.RawMessage(`{}`)}); err != nil { t.Fatal(err) } if err := tx.Commit(ctx); err != nil { t.Fatal(err) } - if err := <-done; !errors.Is(err, ErrTurnConflict) { + if err := <-done; !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("quiesce overtook an admitted Turn", err) } got, err := s.GetRuntimeAllocation(ctx, owner.TenantID, owner.EnvironmentID) diff --git a/services/core/internal/store/runtime_suspension_test.go b/services/core/internal/store/runtime_suspension_test.go index 03abe1367..df515534e 100644 --- a/services/core/internal/store/runtime_suspension_test.go +++ b/services/core/internal/store/runtime_suspension_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) @@ -97,7 +98,7 @@ func TestRuntimeSuspensionRequiresCompletedIdleAndNoPendingWork(t *testing.T) { if err != nil { t.Fatal(err) } - } else if !errors.Is(err, ErrTurnConflict) { + } else if !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("unsafe quiesce admitted: %v", err) } }) @@ -124,7 +125,7 @@ func TestRuntimeSuspensionCASAndActivityFence(t *testing.T) { for err := range results { if err == nil { winners++ - } else if errors.Is(err, ErrTurnConflict) { + } else if errors.Is(err, sessions.ErrTurnConflict) { conflicts++ } else { t.Fatal(err) @@ -140,7 +141,7 @@ func TestRuntimeSuspensionCASAndActivityFence(t *testing.T) { if current.ComputeRevision != owner.ComputeRevision+1 || current.ComputePhase != "quiescing" { t.Fatal("operation intent not durable", current) } - if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{"stale":true}`), &until, time.Nanosecond); !errors.Is(err, ErrTurnConflict) { + if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{"stale":true}`), &until, time.Nanosecond); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("stale phase overwrite", err) } current = runtimeSuspensionStep(t, w, current, "running", nil) @@ -156,15 +157,15 @@ func TestRuntimeSuspensionCASAndActivityFence(t *testing.T) { if err := w.ClearRuntimeWake(t.Context(), latest, latest.ComputeActivityAt); err != nil { t.Fatal(err) } - if _, err := w.SetRuntimeCompute(t.Context(), observed, "quiescing", json.RawMessage(`{}`), &until, time.Nanosecond); !errors.Is(err, ErrTurnConflict) { + if _, err := w.SetRuntimeCompute(t.Context(), observed, "quiescing", json.RawMessage(`{}`), &until, time.Nanosecond); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("newer activity was swallowed", err) } for _, invalid := range []json.RawMessage{json.RawMessage(`[]`), json.RawMessage(`null`), json.RawMessage(`false`), json.RawMessage(`{`)} { - if _, err := w.SetRuntimeCompute(t.Context(), latest, "running", invalid, nil, 0); !errors.Is(err, ErrInvalidInput) { + if _, err := w.SetRuntimeCompute(t.Context(), latest, "running", invalid, nil, 0); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("non-object compute state accepted", string(invalid), err) } } - if _, err := w.SetRuntimeCompute(t.Context(), latest, "suspended", json.RawMessage(`{}`), &until, 0); !errors.Is(err, ErrInvalidInput) { + if _, err := w.SetRuntimeCompute(t.Context(), latest, "suspended", json.RawMessage(`{}`), &until, 0); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("running skipped snapshot protocol", err) } } @@ -186,7 +187,7 @@ func TestRuntimeSuspensionWakeDoesNotLoseNewerWork(t *testing.T) { if err != nil || quiet.WakeRequested || !quiet.LastActivity.Equal(before.LastActivity) { t.Fatal("heartbeat or history read touched compute activity", quiet, err) } - if err := s.TouchRuntimeActivity(t.Context(), uuid.NewString(), owner.EnvironmentID); !errors.Is(err, ErrNotFound) { + if err := s.TouchRuntimeActivity(t.Context(), uuid.NewString(), owner.EnvironmentID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } foreign, err := w.RuntimeActivity(t.Context(), owner) @@ -241,20 +242,20 @@ func TestRuntimeSuspensionRetentionAndDeletedSession(t *testing.T) { t.Fatal("snapshot retention expiry not observed", expired, err) } // Use the earlier unexpired observation to exercise expiry at the database CAS. - if _, err := w.SetRuntimeCompute(t.Context(), retained, "restoring", json.RawMessage(`{}`), &until, 0); !errors.Is(err, ErrTurnConflict) { + if _, err := w.SetRuntimeCompute(t.Context(), retained, "restoring", json.RawMessage(`{}`), &until, 0); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("expired snapshot restored from stale observation", err) } if err := s.DeleteSession(t.Context(), owner.TenantID, owner.SessionID); err != nil { t.Fatal(err) } - if err := s.TouchRuntimeActivity(t.Context(), owner.TenantID, owner.EnvironmentID); !errors.Is(err, ErrNotFound) { + if err := s.TouchRuntimeActivity(t.Context(), owner.TenantID, owner.EnvironmentID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } deleted, err := s.GetRuntimeAllocation(t.Context(), owner.TenantID, owner.EnvironmentID) if err != nil || !deleted.SessionDeleted || deleted.ComputeWakeRequested { t.Fatal("deleted session was woken", deleted, err) } - if _, err := w.SetRuntimeCompute(t.Context(), deleted, "restoring", json.RawMessage(`{}`), &until, 0); !errors.Is(err, ErrNotFound) { + if _, err := w.SetRuntimeCompute(t.Context(), deleted, "restoring", json.RawMessage(`{}`), &until, 0); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted session restored", err) } } @@ -356,7 +357,7 @@ func TestRuntimeSuspensionExpiredRunningAndLostWriterAreFenced(t *testing.T) { runtimeSuspensionCompleted(t, pool, owner) until := time.Now().Add(time.Hour) runtimeSuspensionSQL(t, pool, `UPDATE runtime_allocations SET kept_at=clock_timestamp()-interval '2 hours' WHERE id=$1`, owner.ID) - if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, time.Nanosecond); !errors.Is(err, ErrTurnConflict) { + if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, time.Nanosecond); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("expired running allocation entered checkpoint", err) } if err := w.lease.Close(t.Context()); err != nil { @@ -406,14 +407,14 @@ func TestRuntimeSuspensionRechecksCompletionAgainstIdleTimeout(t *testing.T) { runtimeSuspensionSQL(t, pool, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256,state,created_at,settled_at) VALUES($1,$2,$3,$4,$5,clock_timestamp()-interval '10 minutes',clock_timestamp())`, uuid.NewString(), owner.EnvironmentID, owner.DeviceID, strings.Repeat("a", 64), strings.TrimPrefix(kind, "file_")) } until := time.Now().Add(time.Hour) - if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, idleTimeout); !errors.Is(err, ErrTurnConflict) { + if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, idleTimeout); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("completion after idle observation did not fence quiesce", err) } activity, err := w.RuntimeActivity(t.Context(), owner) if err != nil || activity.Busy || activity.WakeRequested || activity.ReadyToSuspend(idleTimeout) { t.Fatal("last completion did not restart idle interval", activity, err) } - if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, 0); !errors.Is(err, ErrInvalidInput) { + if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, 0); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("missing idle timeout accepted", err) } if _, err := w.SetRuntimeCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, time.Nanosecond); err != nil { diff --git a/services/core/internal/store/runtime_wake_hint_integration_test.go b/services/core/internal/store/runtime_wake_hint_integration_test.go index 5240d78e7..1b61291f6 100644 --- a/services/core/internal/store/runtime_wake_hint_integration_test.go +++ b/services/core/internal/store/runtime_wake_hint_integration_test.go @@ -11,6 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -39,8 +40,8 @@ func (p *wakeHintScanProvider) GetCompute(ctx context.Context, reference sandbox type wakeHintIntegrationTarget struct { tenant string - session store.Session - environment store.Environment + session sessions.Session + environment sessions.Environment owner store.RuntimeAllocation } @@ -111,12 +112,12 @@ func newWakeHintIntegration(t *testing.T) *wakeHintIntegration { return result } -func wakeHintInput(text string) []store.Input { +func wakeHintInput(text string) []sessions.Input { payload, _ := json.Marshal(map[string]string{"text": text}) - return []store.Input{{Kind: "message", Payload: payload}} + return []sessions.Input{{Kind: "message", Payload: payload}} } -func (f *wakeHintIntegration) pending(t *testing.T, target wakeHintIntegrationTarget, key string) store.EnvironmentInputReservation { +func (f *wakeHintIntegration) pending(t *testing.T, target wakeHintIntegrationTarget, key string) sessions.EnvironmentInputReservation { t.Helper() var id string awaitDaemonRemoteCondition(t, t.Context(), 2*time.Second, "committed wake input", func() bool { @@ -125,7 +126,7 @@ func (f *wakeHintIntegration) pending(t *testing.T, target wakeHintIntegrationTa target.session.ID, key).Scan(&id) == nil }) pending, err := f.fixture.store.GetEnvironmentInputReservation(t.Context(), target.tenant, target.session.ID, id) - if err != nil || pending.State != store.EnvironmentInputPending { + if err != nil || pending.State != sessions.EnvironmentInputPending { t.Fatal("input was not durably pending", pending.State, err) } return pending @@ -189,9 +190,9 @@ func TestRuntimeWakeHintRejectedSubmitDoesNotAccelerateScan(t *testing.T) { for _, name := range []string{"invalid", "idempotency conflict", "competing batch"} { t.Run(name, func(t *testing.T) { f := newWakeHintIntegration(t) - key, inputs, want := "wake", wakeHintInput("next turn"), store.ErrInvalidInput + key, inputs, want := "wake", wakeHintInput("next turn"), sessions.ErrInvalidInput if name == "invalid" { - inputs = []store.Input{{Kind: "unsupported", Payload: json.RawMessage("{}")}} + inputs = []sessions.Input{{Kind: "unsupported", Payload: json.RawMessage("{}")}} } else { // Persist directly while the sentinel is blocked. Only the failing // Worker submission could emit a hint; Store persistence cannot. @@ -199,9 +200,9 @@ func TestRuntimeWakeHintRejectedSubmitDoesNotAccelerateScan(t *testing.T) { t.Fatal(err) } if name == "idempotency conflict" { - inputs, want = wakeHintInput("different input"), store.ErrIdempotencyConflict + inputs, want = wakeHintInput("different input"), sessions.ErrIdempotencyConflict } else { - key, want = "different-key", store.ErrTurnConflict + key, want = "different-key", sessions.ErrTurnConflict } } if _, err := f.worker.SubmitInputs(t.Context(), f.target.tenant, f.target.session.ID, key, inputs); !errors.Is(err, want) { diff --git a/services/core/internal/store/runtime_worker_recovery_test.go b/services/core/internal/store/runtime_worker_recovery_test.go index 0c3327982..8b6030a0d 100644 --- a/services/core/internal/store/runtime_worker_recovery_test.go +++ b/services/core/internal/store/runtime_worker_recovery_test.go @@ -37,7 +37,7 @@ func TestPreparedDispatchKeepsPendingReservationAfterComputeConflict(t *testing. h, _ := runtimeWorkerHarness(t) owner := executionOwner(t, h.db, h.s) h.d.Store, h.lease = owner.Store, owner.Lease - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "pending", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "pending", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) if err != nil { t.Fatal(err) } @@ -46,7 +46,7 @@ func TestPreparedDispatchKeepsPendingReservationAfterComputeConflict(t *testing. handle := acknowledgePreparation(h, frame.ID) h.write(frame.ID, proto.TypePreparationStatus, proto.PreparationStatusPayload{Handle: handle, Revision: 2, State: "ready"}) got := awaitPreparedDispatch(t, result) - if !errors.Is(got.err, store.ErrTurnConflict) || got.run.Reservation.ID != pending.ID || got.run.Reservation.State != store.EnvironmentInputPending { + if !errors.Is(got.err, sessions.ErrTurnConflict) || got.run.Reservation.ID != pending.ID || got.run.Reservation.State != sessions.EnvironmentInputPending { t.Fatal("rejected promotion lost its pending owner", got) } assertPreparationReleased(t, h, frame.ID, handle) @@ -58,7 +58,7 @@ func TestPreparedDispatchKeepsPendingReservationAfterComputeConflict(t *testing. func TestWorkerWaitsForComputeAndSurvivesPromotionConflict(t *testing.T) { h, pool := runtimeWorkerHarness(t) - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "pending", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "pending", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) if err != nil { t.Fatal(err) } @@ -106,7 +106,7 @@ func TestWorkerWaitsForComputeAndSurvivesPromotionConflict(t *testing.T) { t.Fatal("conflicted preparation was not released") } stored, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, h.session.ID, pending.ID) - if err != nil || stored.State != store.EnvironmentInputPending || len(stored.Receipts) != 0 { + if err != nil || stored.State != sessions.EnvironmentInputPending || len(stored.Receipts) != 0 { t.Fatal("conflict consumed queued input", stored, err) } setPhase("running") diff --git a/services/core/internal/store/sandbox_deployment_switch_test.go b/services/core/internal/store/sandbox_deployment_switch_test.go index 6bac6e3c7..5d35a2f71 100644 --- a/services/core/internal/store/sandbox_deployment_switch_test.go +++ b/services/core/internal/store/sandbox_deployment_switch_test.go @@ -139,7 +139,7 @@ func TestSandboxDirectDeploymentOwnershipAndCleanSwitch(t *testing.T) { if _, err := w.RequestRuntimeCleanup(t.Context(), owner); err != nil { t.Fatal(err) } - if _, err := w.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, ErrTurnConflict) { + if _, err := w.ReleaseRuntimeAllocation(t.Context(), owner); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal("unsettled create released", err) } if _, err := w.SettleRuntimeCreation(t.Context(), owner); err != nil { @@ -306,7 +306,7 @@ func TestSandboxSwitchPreservesReleasedAllocationAndItemHistory(t *testing.T) { t.Fatal(err) } // A separate completed Session supplies public Items without calling a model. - history, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString()}) + history, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString()}) if err != nil { t.Fatal(err) } @@ -314,13 +314,13 @@ func TestSandboxSwitchPreservesReleasedAllocationAndItemHistory(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := w.TransitionTurn(t.Context(), tenant, history.ID, input.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := w.TransitionTurn(t.Context(), tenant, history.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - if err := w.AppendTurnEvents(t.Context(), tenant, history.ID, input.TurnID, 1, []ExecutionEvent{{Kind: "output_message", Payload: json.RawMessage(`{"id":"answer","status":"completed","text":"retained answer"}`)}}); err != nil { + if err := w.AppendTurnEvents(t.Context(), tenant, history.ID, input.TurnID, 1, []sessions.ExecutionEvent{{Kind: "output_message", Payload: json.RawMessage(`{"id":"answer","status":"completed","text":"retained answer"}`)}}); err != nil { t.Fatal(err) } - if _, err := w.TransitionTurn(t.Context(), tenant, history.ID, input.TurnID, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted}); err != nil { + if _, err := w.TransitionTurn(t.Context(), tenant, history.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted}); err != nil { t.Fatal(err) } items, err := s.ListItems(t.Context(), tenant, history.ID, "", 100, true) @@ -404,7 +404,7 @@ func TestUnspecifiedNodeDeploymentRejectedWithoutMutation(t *testing.T) { if _, err := nodes.NodeConfiguration(t.Context(), node.NodeID, node.Credential, 0); !errors.Is(err, deployment.ErrSpecificationMismatch) { t.Fatal("node configuration served without a specification", err) } - if _, err := s.CreateSession(t.Context(), uuid.NewString(), managerSessionInput(uuid.NewString())); !errors.Is(err, ErrEnvironmentUnavailable) { + if _, err := s.CreateSession(t.Context(), uuid.NewString(), managerSessionInput(uuid.NewString())); !errors.Is(err, sessions.ErrEnvironmentUnavailable) { t.Fatal("unspecified deployment admitted a fresh sandbox", err) } if _, err := nodes.CreateEnrollment(t.Context(), deployment.Capacity{MaxActive: 2, MaxRetained: 4}); !errors.Is(err, deployment.ErrConflict) { diff --git a/services/core/internal/store/sandbox_deployment_worker_test.go b/services/core/internal/store/sandbox_deployment_worker_test.go index 47f176bd9..b797c6274 100644 --- a/services/core/internal/store/sandbox_deployment_worker_test.go +++ b/services/core/internal/store/sandbox_deployment_worker_test.go @@ -12,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -42,7 +43,7 @@ func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { return w, stop } w, stop := start() - input := store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`)} + input := sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`)} if _, err := w.CreateSession(t.Context(), uuid.NewString(), input); !errors.Is(err, execution.ErrExecutionUnavailable) { t.Fatal("uninitialized worker admitted hosted Session", err) } diff --git a/services/core/internal/store/sandbox_reset.go b/services/core/internal/store/sandbox_reset.go index c7bf36874..fb6884e54 100644 --- a/services/core/internal/store/sandbox_reset.go +++ b/services/core/internal/store/sandbox_reset.go @@ -12,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -60,12 +61,12 @@ func (s *Store) resetTransaction(ctx context.Context, apply func(context.Context // caller reads the deployment after commit. func (s *Store) StartSandboxReset(ctx context.Context, installation string, input SandboxResetRequest) error { if input.Clear != "auto" && input.Clear != "force" { - return ErrInvalidInput + return sessions.ErrInvalidInput } deadline := int32(3600) if input.DeadlineSeconds != nil { if input.Clear != "auto" || *input.DeadlineSeconds < 300 || *input.DeadlineSeconds > 86400 { - return ErrInvalidInput + return sessions.ErrInvalidInput } deadline = *input.DeadlineSeconds } @@ -91,7 +92,7 @@ func (s *Store) StartSandboxReset(ctx context.Context, installation string, inpu } else { source, ok := adminaudit.FromContext(ctx) if !ok { - return ErrInvalidInput + return sessions.ErrInvalidInput } // The audit insert validates the provenance before this transaction // can commit. Persist only the same non-secret typed source. @@ -146,7 +147,7 @@ func (s *Store) AdvanceSandboxResetDeadline(ctx context.Context) error { func sandboxResetAudit(d sqlc.RuntimeDeployment) (adminaudit.Source, error) { var source adminaudit.Source if !d.ResetClear.Valid || json.Unmarshal(d.ResetAudit, &source) != nil { - return source, ErrInvalidInput + return source, sessions.ErrInvalidInput } return source, nil } diff --git a/services/core/internal/store/sandbox_reset_test.go b/services/core/internal/store/sandbox_reset_test.go index d6f077649..d734977e1 100644 --- a/services/core/internal/store/sandbox_reset_test.go +++ b/services/core/internal/store/sandbox_reset_test.go @@ -353,7 +353,7 @@ func TestSandboxResetPaginationSkipsBusyPrefixAndPreservesSelfHosted(t *testing. if err != nil || len(second) != 2 { t.Fatal(second, err) } - if _, err := w.ArchiveSandboxResetSession(t.Context(), selfTenant, self.ID, 1, reset.Reset.RequestedAt); !errors.Is(err, ErrInvalidInput) { + if _, err := w.ArchiveSandboxResetSession(t.Context(), selfTenant, self.ID, 1, reset.Reset.RequestedAt); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("self-hosted reset archive", err) } view, err := s.GetSession(t.Context(), selfTenant, self.ID) diff --git a/services/core/internal/store/sandbox_specification_store_test.go b/services/core/internal/store/sandbox_specification_store_test.go index 2e5f3e047..829a92b92 100644 --- a/services/core/internal/store/sandbox_specification_store_test.go +++ b/services/core/internal/store/sandbox_specification_store_test.go @@ -12,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -244,23 +245,23 @@ func TestSandboxSpecificationChangesPreserveEveryRetainedResource(t *testing.T) func TestSandboxSpecificationAllocationRaceWithMaintenance(t *testing.T) { s, w, view, input := webSpecificationFixture(t, "e2b") tenant := uuid.NewString() - var sessions []Session + var created []sessions.Session for range 12 { session, err := s.CreateSession(t.Context(), tenant, managerSessionInput(uuid.NewString())) if err != nil { t.Fatal(err) } - sessions = append(sessions, session) + created = append(created, session) } type result struct { - session Session + session sessions.Session owner RuntimeAllocation err error } start := make(chan struct{}) - results := make(chan result, len(sessions)) + results := make(chan result, len(created)) maintenance := make(chan error, 1) - for _, session := range sessions { + for _, session := range created { go func() { <-start owner, err := w.ReserveRuntimeAllocation(t.Context(), tenant, session.Environment.ID, view.InstallationID, runtimedevice.HashCredential(uuid.NewString())) @@ -276,7 +277,7 @@ func TestSandboxSpecificationAllocationRaceWithMaintenance(t *testing.T) { t.Fatal(err) } var allocated int64 - for range sessions { + for range created { result := <-results if result.err == nil { allocated++ @@ -294,7 +295,7 @@ func TestSandboxSpecificationAllocationRaceWithMaintenance(t *testing.T) { } } after, err := deploymentService(t, s).View(t.Context()) - if err != nil || after.Reset == nil || after.Generation != view.Generation || after.Resources.Allocations != allocated || after.Resources.Pending != int64(len(sessions))-allocated { + if err != nil || after.Reset == nil || after.Generation != view.Generation || after.Resources.Allocations != allocated || after.Resources.Pending != int64(len(created))-allocated { t.Fatal("concurrent maintenance lost resource accounting", after.Resources, err) } input.Resources.CPUs++ diff --git a/services/core/internal/store/scheduling.go b/services/core/internal/store/scheduling.go index e1f632beb..863e527c4 100644 --- a/services/core/internal/store/scheduling.go +++ b/services/core/internal/store/scheduling.go @@ -6,15 +6,12 @@ import ( "fmt" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -type ExecutionWork struct{ TenantID, SessionID, TurnID, Status string } - -type EnvironmentInputWork struct{ TenantID, SessionID, ReservationID string } - func executionWorkCursor(after string, connectedDevices []string) (pgtype.UUID, []pgtype.UUID, error) { id := pgtype.UUID{Valid: true} var err error @@ -35,7 +32,7 @@ func executionWorkCursor(after string, connectedDevices []string) (pgtype.UUID, return id, devices, nil } -func (s *Store) ListEnvironmentInputWork(ctx context.Context, after string, connectedDevices []string) ([]EnvironmentInputWork, error) { +func (s *Store) ListEnvironmentInputWork(ctx context.Context, after string, connectedDevices []string) ([]sessions.EnvironmentInputWork, error) { id, devices, err := executionWorkCursor(after, connectedDevices) if err != nil { return nil, err @@ -44,14 +41,14 @@ func (s *Store) ListEnvironmentInputWork(ctx context.Context, after string, conn if err != nil { return nil, err } - work := make([]EnvironmentInputWork, 0, len(rows)) + work := make([]sessions.EnvironmentInputWork, 0, len(rows)) for _, row := range rows { - work = append(work, EnvironmentInputWork{TenantID: uuid.UUID(row.TenantID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), ReservationID: uuid.UUID(row.ID.Bytes).String()}) + work = append(work, sessions.EnvironmentInputWork{TenantID: uuid.UUID(row.TenantID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), ReservationID: uuid.UUID(row.ID.Bytes).String()}) } return work, nil } -func (s *Store) ListExecutionWork(ctx context.Context, after string, statuses []string, connectedDevices []string) ([]ExecutionWork, error) { +func (s *Store) ListExecutionWork(ctx context.Context, after string, statuses []string, connectedDevices []string) ([]sessions.ExecutionWork, error) { id, devices, err := executionWorkCursor(after, connectedDevices) if err != nil { return nil, err @@ -60,14 +57,14 @@ func (s *Store) ListExecutionWork(ctx context.Context, after string, statuses [] if err != nil { return nil, err } - work := make([]ExecutionWork, 0, len(rows)) + work := make([]sessions.ExecutionWork, 0, len(rows)) for _, row := range rows { - work = append(work, ExecutionWork{TenantID: uuid.UUID(row.TenantID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), TurnID: uuid.UUID(row.ID.Bytes).String(), Status: row.Status}) + work = append(work, sessions.ExecutionWork{TenantID: uuid.UUID(row.TenantID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), TurnID: uuid.UUID(row.ID.Bytes).String(), Status: row.Status}) } return work, nil } -func (s *Store) ListExecutionDevices(ctx context.Context, tenantID string) ([]ExecutionDevice, error) { +func (s *Store) ListExecutionDevices(ctx context.Context, tenantID string) ([]sessions.ExecutionDevice, error) { tenant, err := parseID(tenantID) if err != nil { return nil, err @@ -76,16 +73,16 @@ func (s *Store) ListExecutionDevices(ctx context.Context, tenantID string) ([]Ex if err != nil { return nil, err } - devices := make([]ExecutionDevice, 0, len(rows)) + devices := make([]sessions.ExecutionDevice, 0, len(rows)) for _, row := range rows { - devices = append(devices, ExecutionDevice{ID: uuid.UUID(row.ID.Bytes).String(), Name: row.Name}) + devices = append(devices, sessions.ExecutionDevice{ID: uuid.UUID(row.ID.Bytes).String(), Name: row.Name}) } return devices, nil } -func (s *Store) sessionActivity(ctx context.Context, session Session, err error) (Session, error) { +func (s *Store) sessionActivity(ctx context.Context, session sessions.Session, err error) (sessions.Session, error) { if err != nil { - return Session{}, err + return sessions.Session{}, err } err = s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { var err error @@ -97,16 +94,16 @@ func (s *Store) sessionActivity(ctx context.Context, session Session, err error) // SessionStreamSnapshot reads the projection that GetSession returns and the // committed Session event cursor from one database snapshot. -func (s *Store) SessionStreamSnapshot(ctx context.Context, tenantID, sessionID string) (Session, int64, error) { +func (s *Store) SessionStreamSnapshot(ctx context.Context, tenantID, sessionID string) (sessions.Session, int64, error) { tenant, err := parseID(tenantID) if err != nil { - return Session{}, 0, err + return sessions.Session{}, 0, err } id, err := parseID(sessionID) if err != nil { - return Session{}, 0, err + return sessions.Session{}, 0, err } - var session Session + var session sessions.Session var cursor int64 err = s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { q := s.queries.WithTx(tx) @@ -122,17 +119,17 @@ func (s *Store) SessionStreamSnapshot(ctx context.Context, tenantID, sessionID s return err }) if errors.Is(err, pgx.ErrNoRows) { - return Session{}, 0, ErrNotFound + return sessions.Session{}, 0, sessions.ErrNotFound } if err != nil { - return Session{}, 0, fmt.Errorf("read session stream snapshot: %w", err) + return sessions.Session{}, 0, fmt.Errorf("read session stream snapshot: %w", err) } return session, cursor, nil } // readSessionActivity adds the Environment, reservation activity and latest Turn // projection within the caller's snapshot. -func readSessionActivity(ctx context.Context, q *sqlc.Queries, session Session) (Session, error) { +func readSessionActivity(ctx context.Context, q *sqlc.Queries, session sessions.Session) (sessions.Session, error) { id, _ := parseID(session.ID) tenant, _ := parseID(session.TenantID) environment, err := q.GetSessionEnvironment(ctx, sqlc.GetSessionEnvironmentParams{TenantID: tenant, ID: id}) diff --git a/services/core/internal/store/self_hosted_cancel_public_test.go b/services/core/internal/store/self_hosted_cancel_public_test.go index 9cd9897a8..e03b3062c 100644 --- a/services/core/internal/store/self_hosted_cancel_public_test.go +++ b/services/core/internal/store/self_hosted_cancel_public_test.go @@ -13,7 +13,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -74,7 +73,7 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) { t.Fatal("missing public cancellation fixture identities", err) } settings["accepted"] = accepted - receipts := func(key, target string) []store.InputReceipt { + receipts := func(key, target string) []sessions.InputReceipt { t.Helper() rows, err := db.pool.Query(t.Context(), `SELECT sequence, COALESCE(turn_id::text,'') FROM turn_inputs WHERE session_id=$1 AND idempotency_key=$2 ORDER BY batch_position`, created.ID, key) @@ -82,9 +81,9 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) { t.Fatal(err) } defer rows.Close() - var result []store.InputReceipt + var result []sessions.InputReceipt for rows.Next() { - var receipt store.InputReceipt + var receipt sessions.InputReceipt if err := rows.Scan(&receipt.Sequence, &receipt.TurnID); err != nil { t.Fatal(err) } @@ -114,14 +113,14 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) { return value } idleReceipts := receipts(created.IdleKey, "") - later, err := s.ReserveEnvironmentInput(t.Context(), tenant, created.LaterID, "controlled-later-input", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"Retain pending input."}`)}}) - if err != nil || later.State != store.EnvironmentInputPending || later.IsInitial { + later, err := s.ReserveEnvironmentInput(t.Context(), tenant, created.LaterID, "controlled-later-input", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"Retain pending input."}`)}}) + if err != nil || later.State != sessions.EnvironmentInputPending || later.IsInitial { t.Fatal("could not establish controlled later reservation", err) } pending := map[string]string{created.InitialID: snapshot(created.InitialID), created.LaterID: snapshot(created.LaterID)} transition := func(id, from, to string) { t.Helper() - if _, err := s.TransitionTurn(t.Context(), tenant, created.ID, id, store.TurnTransition{ExpectedStatus: from, Status: to}); err != nil { + if _, err := s.TransitionTurn(t.Context(), tenant, created.ID, id, sessions.TurnTransition{ExpectedStatus: from, Status: to}); err != nil { t.Fatal(err) } } @@ -137,7 +136,7 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) { return input.TurnID } first := start() - if err := s.AppendTurnEvents(t.Context(), tenant, created.ID, first, 1, []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"controlled-partial","delta":"Retained partial output."}`)}}); err != nil { + if err := s.AppendTurnEvents(t.Context(), tenant, created.ID, first, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"controlled-partial","delta":"Retained partial output."}`)}}); err != nil { t.Fatal(err) } before := snapshot(created.ID) diff --git a/services/core/internal/store/self_hosted_initial_public_test.go b/services/core/internal/store/self_hosted_initial_public_test.go index b8776e539..4d2537a1f 100644 --- a/services/core/internal/store/self_hosted_initial_public_test.go +++ b/services/core/internal/store/self_hosted_initial_public_test.go @@ -19,6 +19,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -46,7 +47,13 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) { } else { // Without a Worker, Core keeps its executor URL but admits nothing. enabled = append(enabled, func(d *api.Dependencies) { - d.Execution = &api.Execution{ExecutorURL: origin, Admission: unavailableAdmission{}, SessionArchive: strictStandIn{t}, Workspaces: strictStandIn{t}} + d.Execution = &api.Execution{ + ExecutorURL: origin, + SessionAdmission: unavailableAdmission{}, + InputAdmission: unavailableAdmission{}, + SessionArchive: strictStandIn{t}, + Workspaces: strictStandIn{t}, + } }) } handler, err := publicHandler(t, s, db, auth, "codex", enabled...) @@ -91,9 +98,9 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) { if err := json.Unmarshal(accepted, &created); err != nil || len(created.Cases) != 4 { t.Fatal("missing public initial creation cases", err) } - reservations := func(s *store.Store, pool *pgxpool.Pool) map[string]store.EnvironmentInputReservation { + reservations := func(s *store.Store, pool *pgxpool.Pool) map[string]sessions.EnvironmentInputReservation { t.Helper() - result := make(map[string]store.EnvironmentInputReservation) + result := make(map[string]sessions.EnvironmentInputReservation) for _, item := range created.Cases { var id string if err := pool.QueryRow(t.Context(), "SELECT id FROM environment_input_reservations WHERE session_id=$1 AND is_initial", item.ID).Scan(&id); err != nil { @@ -139,7 +146,7 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) { } before := reservations(s, db.pool) for _, reservation := range before { - if reservation.State != store.EnvironmentInputPending || reservation.Deadline.Sub(reservation.CreatedAt) != 5*time.Minute { + if reservation.State != sessions.EnvironmentInputPending || reservation.Deadline.Sub(reservation.CreatedAt) != 5*time.Minute { t.Fatal("public initial creation did not retain its database deadline") } } @@ -175,7 +182,7 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) { after := reservations(reopened, reopenedDB.pool) for id, reservation := range after { if id == failureID { - if reservation.ID != before[id].ID || reservation.State != store.EnvironmentInputExpired || reservation.SettledAt == nil { + if reservation.ID != before[id].ID || reservation.State != sessions.EnvironmentInputExpired || reservation.SettledAt == nil { t.Fatal("Worker did not settle the original public initial reservation") } } else if !reflect.DeepEqual(reservation, before[id]) { @@ -229,17 +236,18 @@ func publicInitialWorker(t *testing.T, s *store.Store, db fixtureDB) (*execution return worker, stop } -// unavailableAdmission admits nothing, as a Core without a running Worker. +// unavailableAdmission admits no Session creation or input, as a Core without +// a running Worker. type unavailableAdmission struct{} -func (unavailableAdmission) CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) { - return store.Session{}, execution.ErrExecutionUnavailable +func (unavailableAdmission) CreateSession(context.Context, string, sessions.CreateSession) (sessions.Session, error) { + return sessions.Session{}, execution.ErrExecutionUnavailable } -func (unavailableAdmission) CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) { - return store.SessionCreation{}, execution.ErrExecutionUnavailable +func (unavailableAdmission) CreateSessionStream(context.Context, string, sessions.CreateSession) (sessions.Creation, error) { + return sessions.Creation{}, execution.ErrExecutionUnavailable } -func (unavailableAdmission) SubmitInputs(context.Context, string, string, string, []store.Input) ([]store.InputReceipt, error) { +func (unavailableAdmission) SubmitInputs(context.Context, string, string, string, []sessions.Input) ([]sessions.InputReceipt, error) { return nil, execution.ErrExecutionUnavailable } diff --git a/services/core/internal/store/session_agent_filter_test.go b/services/core/internal/store/session_agent_filter_test.go index 841dfaa21..959b6e2c1 100644 --- a/services/core/internal/store/session_agent_filter_test.go +++ b/services/core/internal/store/session_agent_filter_test.go @@ -9,6 +9,8 @@ import ( "time" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestSessionAgentFilterPaginationAndIsolation(t *testing.T) { @@ -16,9 +18,9 @@ func TestSessionAgentFilterPaginationAndIsolation(t *testing.T) { tenant, foreign := uuid.NewString(), uuid.NewString() root := "agent_inline-root" var expected []string - create := func(tenant, key, agent string) Session { + create := func(tenant, key, agent string) sessions.Session { configuration, _ := json.Marshal(map[string]any{"agent": map[string]string{"id": agent, "model": "test-model"}, "environment": map[string]string{"type": "none"}}) - value, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: configuration}) + value, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: configuration}) if err != nil { t.Fatal(err) } @@ -83,7 +85,7 @@ func TestSessionAgentFilterPaginationAndIsolation(t *testing.T) { t.Fatal(page, err) } } - if _, err := s.ListSessions(t.Context(), tenant, other.ID, 2, true, &root); !errors.Is(err, ErrNotFound) { + if _, err := s.ListSessions(t.Context(), tenant, other.ID, 2, true, &root); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign cursor", err) } page, err := s.ListSessions(t.Context(), foreign, "", 100, false, &root) diff --git a/services/core/internal/store/session_artifacts.go b/services/core/internal/store/session_artifacts.go index 888b83bac..6bacd5e86 100644 --- a/services/core/internal/store/session_artifacts.go +++ b/services/core/internal/store/session_artifacts.go @@ -4,49 +4,34 @@ import ( "context" "errors" "io" - "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -type SessionArtifact struct { - ID string - SessionID string - TurnID string - EnvironmentID string - Path string - SizeBytes int64 - CreatedAt time.Time -} - -type ArtifactPage struct { - Artifacts []SessionArtifact - NextCursor string -} - -func (s *Store) GetSessionArtifact(ctx context.Context, tenantID, sessionID, artifactID string) (SessionArtifact, error) { +func (s *Store) GetSessionArtifact(ctx context.Context, tenantID, sessionID, artifactID string) (sessions.Artifact, error) { lookup, err := artifactLookup(tenantID, sessionID, artifactID) if err != nil { - return SessionArtifact{}, err + return sessions.Artifact{}, err } row, err := s.queries.GetSessionArtifact(ctx, lookup) if errors.Is(err, pgx.ErrNoRows) { - return SessionArtifact{}, ErrNotFound + return sessions.Artifact{}, sessions.ErrNotFound } return artifactFromRow(row), err } -func (s *Store) ListSessionArtifacts(ctx context.Context, tenantID, sessionID, environmentID, cursor string, limit int, ascending bool) (ArtifactPage, error) { +func (s *Store) ListSessionArtifacts(ctx context.Context, tenantID, sessionID, environmentID, cursor string, limit int, ascending bool) (sessions.ArtifactPage, error) { if limit < 1 || limit > 100 { - return ArtifactPage{}, ErrInvalidInput + return sessions.ArtifactPage{}, sessions.ErrInvalidInput } if _, err := s.GetSession(ctx, tenantID, sessionID); err != nil { - return ArtifactPage{}, err + return sessions.ArtifactPage{}, err } tenant, _ := parseID(tenantID) session, _ := parseID(sessionID) @@ -59,26 +44,26 @@ func (s *Store) ListSessionArtifacts(ctx context.Context, tenantID, sessionID, e // Any cursor that is not an Artifact of this Session, including a // malformed one, is an invalid cursor rather than a missing resource. after, err := s.GetSessionArtifact(ctx, tenantID, sessionID, cursor) - if errors.Is(err, ErrNotFound) { + if errors.Is(err, sessions.ErrNotFound) { // The Session lookup above is a separate statement: a Session deleted // since then stays not found. Deleted Sessions never reappear, so an // existing one here also existed when the cursor was read. if _, err := s.GetSession(ctx, tenantID, sessionID); err != nil { - return ArtifactPage{}, err + return sessions.ArtifactPage{}, err } - return ArtifactPage{}, errArtifactCursor + return sessions.ArtifactPage{}, sessions.ErrArtifactCursor } if err != nil { - return ArtifactPage{}, err + return sessions.ArtifactPage{}, err } params.AfterCreated = pgtype.Timestamptz{Time: after.CreatedAt, Valid: true} params.AfterID, _ = parseID(after.ID) } rows, err := s.queries.ListSessionArtifacts(ctx, params) if err != nil { - return ArtifactPage{}, err + return sessions.ArtifactPage{}, err } - page := ArtifactPage{Artifacts: make([]SessionArtifact, 0, min(limit, len(rows)))} + page := sessions.ArtifactPage{Artifacts: make([]sessions.Artifact, 0, min(limit, len(rows)))} if len(rows) > limit { page.NextCursor = uuid.UUID(rows[limit-1].ID.Bytes).String() rows = rows[:limit] @@ -90,18 +75,18 @@ func (s *Store) ListSessionArtifacts(ctx context.Context, tenantID, sessionID, e } // ReadSessionArtifact keeps an admitted snapshot available across concurrent deletion. -func (s *Store) ReadSessionArtifact(ctx context.Context, tenantID, sessionID, artifactID string, consume func(SessionArtifact, io.Reader) error) error { +func (s *Store) ReadSessionArtifact(ctx context.Context, tenantID, sessionID, artifactID string, consume func(sessions.Artifact, io.Reader) error) error { lookup, err := artifactLookup(tenantID, sessionID, artifactID) if err != nil { return err } if consume == nil { - return ErrInvalidInput + return sessions.ErrInvalidInput } return s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { row, err := s.queries.WithTx(tx).GetSessionArtifact(ctx, lookup) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -128,14 +113,14 @@ func (s *Store) DeleteSessionArtifact(ctx context.Context, tenantID, sessionID, // Use the same lock order as whole-Session deletion and Turn publication. locked, err := q.LockSession(ctx, sqlc.LockSessionParams{TenantID: lookup.TenantID, ID: lookup.SessionID}) if errors.Is(err, pgx.ErrNoRows) || err == nil && locked.DeletedAt.Valid { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err } oid, err := q.DeleteSessionArtifact(ctx, sqlc.DeleteSessionArtifactParams(lookup)) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -153,8 +138,8 @@ func artifactLookup(tenantID, sessionID, artifactID string) (sqlc.GetSessionArti return sqlc.GetSessionArtifactParams{TenantID: ids.TenantID, SessionID: ids.SessionID, ID: ids.ID}, err } -func artifactFromRow(row sqlc.SessionArtifact) SessionArtifact { - return SessionArtifact{ID: uuid.UUID(row.ID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), +func artifactFromRow(row sqlc.SessionArtifact) sessions.Artifact { + return sessions.Artifact{ID: uuid.UUID(row.ID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), TurnID: uuid.UUID(row.TurnID.Bytes).String(), EnvironmentID: uuid.UUID(row.EnvironmentID.Bytes).String(), Path: row.Path, SizeBytes: row.SizeBytes, CreatedAt: row.CreatedAt.Time} } diff --git a/services/core/internal/store/session_artifacts_public_test.go b/services/core/internal/store/session_artifacts_public_test.go index 96935da81..9249b2f9f 100644 --- a/services/core/internal/store/session_artifacts_public_test.go +++ b/services/core/internal/store/session_artifacts_public_test.go @@ -25,7 +25,7 @@ import ( // hostedArtifactSession creates an openai_hosted Session without Turns. func hostedArtifactSession(t *testing.T, s *store.Store, tenant, key string) (session, environment string) { t.Helper() - created, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: key, + created, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: json.RawMessage(`{"agent":{"model":"artifact-model"},"environment":{"type":"openai_hosted","workspace_directory":"/workspace","capability_directories":[]}}`)}) if err != nil || created.Environment == nil { t.Fatal("fixture Session", err) @@ -43,7 +43,7 @@ func completeArtifactTurn(t *testing.T, s *store.Store, tenant, session, environ } transition := func(from, to string) { t.Helper() - if _, err := s.TransitionTurn(t.Context(), tenant, session, receipt.TurnID, store.TurnTransition{ExpectedStatus: from, Status: to}); err != nil { + if _, err := s.TransitionTurn(t.Context(), tenant, session, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: from, Status: to}); err != nil { t.Fatal(err) } } diff --git a/services/core/internal/store/session_artifacts_test.go b/services/core/internal/store/session_artifacts_test.go index 908e136b7..f7cec36fd 100644 --- a/services/core/internal/store/session_artifacts_test.go +++ b/services/core/internal/store/session_artifacts_test.go @@ -92,23 +92,23 @@ func testSessionArtifactsPublishVersionScopeAndLifetime(t *testing.T, kind strin t.Fatalf("publication metadata: %+v", a) } foreign := uuid.NewString() - if _, err := s.GetSessionArtifact(t.Context(), foreign, session, a.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionArtifact(t.Context(), foreign, session, a.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign metadata: %v", err) } - if _, err := s.GetSessionArtifact(t.Context(), tenant, uuid.NewString(), a.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionArtifact(t.Context(), tenant, uuid.NewString(), a.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("wrong session metadata: %v", err) } - if err := s.DeleteSessionArtifact(t.Context(), foreign, session, a.ID); !errors.Is(err, ErrNotFound) { + if err := s.DeleteSessionArtifact(t.Context(), foreign, session, a.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign delete: %v", err) } - if err := s.ReadSessionArtifact(t.Context(), foreign, session, a.ID, func(SessionArtifact, io.Reader) error { + if err := s.ReadSessionArtifact(t.Context(), foreign, session, a.ID, func(sessions.Artifact, io.Reader) error { t.Error("foreign read reached content") return nil - }); !errors.Is(err, ErrNotFound) { + }); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign read: %v", err) } } - if _, err := s.ListSessionArtifacts(t.Context(), uuid.NewString(), session, "", "", 100, false); !errors.Is(err, ErrNotFound) { + if _, err := s.ListSessionArtifacts(t.Context(), uuid.NewString(), session, "", "", 100, false); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign list: %v", err) } if empty, err := s.ListSessionArtifacts(t.Context(), tenant, session, uuid.NewString(), "", 100, false); err != nil || len(empty.Artifacts) != 0 { @@ -126,7 +126,7 @@ func testSessionArtifactsPublishVersionScopeAndLifetime(t *testing.T, kind strin t.Fatal(all, err) } for _, asc := range []bool{true, false} { - var got []SessionArtifact + var got []sessions.Artifact cursor := "" for { part, err := s.ListSessionArtifacts(t.Context(), tenant, session, environment, cursor, 1, asc) @@ -142,7 +142,7 @@ func testSessionArtifactsPublishVersionScopeAndLifetime(t *testing.T, kind strin } cursor = part.NextCursor } - want := append([]SessionArtifact(nil), all.Artifacts...) + want := append([]sessions.Artifact(nil), all.Artifacts...) if !asc { want[0], want[2] = want[2], want[0] } @@ -155,7 +155,7 @@ func testSessionArtifactsPublishVersionScopeAndLifetime(t *testing.T, kind strin t.Fatal(err) } for _, a := range page.Artifacts { - if err := New(pool).ReadSessionArtifact(t.Context(), tenant, session, a.ID, func(meta SessionArtifact, r io.Reader) error { + if err := New(pool).ReadSessionArtifact(t.Context(), tenant, session, a.ID, func(meta sessions.Artifact, r io.Reader) error { if err := s.DeleteSessionArtifact(t.Context(), tenant, session, a.ID); err != nil { return err } @@ -171,7 +171,7 @@ func testSessionArtifactsPublishVersionScopeAndLifetime(t *testing.T, kind strin }); err != nil { t.Fatal(err) } - if _, err := s.GetSessionArtifact(t.Context(), tenant, session, a.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionArtifact(t.Context(), tenant, session, a.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("deleted metadata retained: %v", err) } } @@ -215,7 +215,7 @@ func testSessionArtifactsRejectIncompleteAndUnownedCapture(t *testing.T, kind st }) } for _, ids := range [][4]string{{uuid.NewString(), session, turn, environment}, {tenant, session, turn, uuid.NewString()}} { - if err := s.StageTurnArtifacts(t.Context(), ids[0], ids[1], ids[2], ids[3], artifactReadError{}); !errors.Is(err, ErrNotFound) { + if err := s.StageTurnArtifacts(t.Context(), ids[0], ids[1], ids[2], ids[3], artifactReadError{}); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("unauthorized capture reached reader: %v", err) } } @@ -235,7 +235,7 @@ func TestSessionArtifactsDiscardTerminalPrivateCapture(t *testing.T) { if count := largeObjectCount(t, pool); count != before { t.Fatalf("terminal capture leaked objects: %d -> %d", before, count) } - if err := s.StageTurnArtifacts(t.Context(), tenant, session, turn, environment, bytes.NewReader(body)); !errors.Is(err, ErrTurnConflict) { + if err := s.StageTurnArtifacts(t.Context(), tenant, session, turn, environment, bytes.NewReader(body)); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("late capture accepted: %v", err) } if count := largeObjectCount(t, pool); count != before { @@ -262,10 +262,10 @@ func TestSessionArtifactTransferDoesNotBlockDeletionOrCancellation(t *testing.T) } ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() - want := ErrNotFound + want := sessions.ErrNotFound if operation == "delete" { // The idle-only decision itself is not blocked by the transfer. - if err := s.DeleteSession(ctx, tenant, session); !errors.Is(err, ErrSessionNotIdle) { + if err := s.DeleteSession(ctx, tenant, session); !errors.Is(err, sessions.ErrNotIdle) { t.Fatalf("transfer blocked or bypassed the deletion rule: %v", err) } if err := s.commitLegacyDeletion(ctx, tenant, session); err != nil { @@ -275,7 +275,7 @@ func TestSessionArtifactTransferDoesNotBlockDeletionOrCancellation(t *testing.T) if _, err := s.RequestCancel(ctx, tenant, session, "cancel-capture"); err != nil { t.Fatalf("transfer blocked cancellation: %v", err) } - want = ErrTurnConflict + want = sessions.ErrTurnConflict } writer.Close() if err := <-result; !errors.Is(err, want) { @@ -309,13 +309,13 @@ func stageArtifactOutputs(t *testing.T, s *Store, tenant, session, environment, } // publishedByTurn returns the Artifacts one Turn published, keyed by outputs-relative path. -func publishedByTurn(t *testing.T, s *Store, tenant, session, turn string) map[string]SessionArtifact { +func publishedByTurn(t *testing.T, s *Store, tenant, session, turn string) map[string]sessions.Artifact { t.Helper() page, err := s.ListSessionArtifacts(t.Context(), tenant, session, "", "", 100, true) if err != nil || page.NextCursor != "" { t.Fatalf("list: %+v %v", page, err) } - got := make(map[string]SessionArtifact) + got := make(map[string]sessions.Artifact) for _, artifact := range page.Artifacts { if artifact.TurnID == turn { got[strings.TrimPrefix(artifact.Path, "/workspace/outputs/")] = artifact @@ -327,7 +327,7 @@ func publishedByTurn(t *testing.T, s *Store, tenant, session, turn string) map[s func artifactBytes(t *testing.T, s *Store, tenant, session, id string) string { t.Helper() var body []byte - if err := s.ReadSessionArtifact(t.Context(), tenant, session, id, func(_ SessionArtifact, r io.Reader) error { + if err := s.ReadSessionArtifact(t.Context(), tenant, session, id, func(_ sessions.Artifact, r io.Reader) error { var err error body, err = io.ReadAll(r) return err @@ -337,7 +337,7 @@ func artifactBytes(t *testing.T, s *Store, tenant, session, id string) string { return string(body) } -func publishedPaths(published map[string]SessionArtifact) []string { +func publishedPaths(published map[string]sessions.Artifact) []string { paths := make([]string, 0, len(published)) for path := range published { paths = append(paths, path) @@ -353,7 +353,7 @@ func TestSessionArtifactsRepublishOnlyNewChangedOrDeletedPaths(t *testing.T) { tenant, session, environment, first := artifactTurn(t, s, "openai_hosted") before := largeObjectCount(t, pool) turnNumber := 1 - run := func(files map[string]string, want ...string) map[string]SessionArtifact { + run := func(files map[string]string, want ...string) map[string]sessions.Artifact { t.Helper() turn := first if turnNumber > 1 { @@ -374,7 +374,7 @@ func TestSessionArtifactsRepublishOnlyNewChangedOrDeletedPaths(t *testing.T) { } return published } - unchanged := func(artifacts ...SessionArtifact) { + unchanged := func(artifacts ...sessions.Artifact) { t.Helper() for _, artifact := range artifacts { if got, err := s.GetSessionArtifact(t.Context(), tenant, session, artifact.ID); err != nil || got != artifact { @@ -490,7 +490,7 @@ func TestSessionArtifactsNewestVersionFollowsTurnOrder(t *testing.T) { t.Fatal(err) } one := publishedByTurn(t, s, tenant, session, first.TurnID)["b.txt"] - run := func(key, body string) map[string]SessionArtifact { + run := func(key, body string) map[string]sessions.Artifact { t.Helper() turn := startArtifactTurn(t, s, tenant, session, key) stageArtifactOutputs(t, s, tenant, session, env.ID, turn, map[string]string{"b.txt": body}) @@ -546,7 +546,7 @@ func TestSessionArtifactsCompletionWaitsForConcurrentDeletion(t *testing.T) { } done := make(chan error, 1) go func() { - _, err := s.TransitionTurn(t.Context(), tenant, session, turn, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted}) + _, err := s.TransitionTurn(t.Context(), tenant, session, turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted}) done <- err }() // Completion must be blocked on the Session lock before the deletion commits. @@ -582,7 +582,7 @@ func TestSessionArtifactsCompletionWaitsForConcurrentDeletion(t *testing.T) { if got := publishedPaths(published); strings.Join(got, ",") != "a.txt" || artifactBytes(t, s, tenant, session, published["a.txt"].ID) != "alpha" { t.Fatalf("concurrent deletion was not republished: %v", got) } - if _, err := s.GetSessionArtifact(t.Context(), tenant, session, newest.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionArtifact(t.Context(), tenant, session, newest.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("deleted Artifact remains: %v", err) } if count := largeObjectCount(t, pool); count != before+1 { diff --git a/services/core/internal/store/session_configuration_test.go b/services/core/internal/store/session_configuration_test.go index 9e0f925b1..d57de119b 100644 --- a/services/core/internal/store/session_configuration_test.go +++ b/services/core/internal/store/session_configuration_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestConfigurationSizeLimitSurvivesJSONBRoundTrip(t *testing.T) { @@ -15,7 +17,7 @@ func TestConfigurationSizeLimitSurvivesJSONBRoundTrip(t *testing.T) { tenant := uuid.NewString() empty := `{"agent":{"model":"example","instructions":""},"environment":{"type":"none"}}` raw := strings.Replace(empty, `"instructions":""`, `"instructions":"`+strings.Repeat("x", 512*1024-len(empty))+`"`, 1) - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "size-limit", Configuration: []byte(raw)} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "size-limit", Configuration: []byte(raw)} first, err := s.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) @@ -29,7 +31,7 @@ func TestConfigurationSizeLimitSurvivesJSONBRoundTrip(t *testing.T) { t.Fatalf("configuration broke listing: %v", err) } input.Configuration = append(input.Configuration, ' ') - if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, ErrInvalidInput) { + if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("oversized request accepted: %v", err) } } @@ -38,7 +40,7 @@ func TestConfigurationIsPartOfSessionIdentity(t *testing.T) { s, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "configured", + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "configured", Configuration: []byte(`{"agent":{"model":"example","instructions":"First"},"environment":{"type":"none"}}`)} first, err := s.CreateSession(ctx, tenant, input) if err != nil { @@ -55,7 +57,7 @@ func TestConfigurationIsPartOfSessionIdentity(t *testing.T) { `{"agent":{"model":"example","instructions":"First"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`, } { input.Configuration = []byte(configuration) - if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatalf("changed snapshot was accepted: %v", err) } } @@ -68,7 +70,7 @@ func TestConfigurationIsPartOfSessionIdentity(t *testing.T) { func TestEmptyConfigurationCanonicalizationPreservesCreator(t *testing.T) { s, _ := testStore(t) tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "empty-configuration"} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "empty-configuration"} first, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -81,7 +83,7 @@ func TestEmptyConfigurationCanonicalizationPreservesCreator(t *testing.T) { } } input.Creator.ID += "-other" - if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("another creator claimed the same request", err) } } diff --git a/services/core/internal/store/session_creation_identity.go b/services/core/internal/store/session_creation_identity.go index 2a0080410..225728b2c 100644 --- a/services/core/internal/store/session_creation_identity.go +++ b/services/core/internal/store/session_creation_identity.go @@ -15,6 +15,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -45,7 +46,7 @@ func (s *Store) fingerprintedProvider(provider *v1.ModelProviderInput) (*v1.Mode func (s *Store) withoutProviderKey(raw json.RawMessage) (json.RawMessage, error) { var request map[string]json.RawMessage if json.Unmarshal(raw, &request) != nil || request == nil { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } extensionRaw, present := request["x_agents_core"] if !present || jsonNull(extensionRaw) { @@ -53,7 +54,7 @@ func (s *Store) withoutProviderKey(raw json.RawMessage) (json.RawMessage, error) } var extension map[string]json.RawMessage if json.Unmarshal(extensionRaw, &extension) != nil || extension == nil { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } providerRaw, present := extension["model_provider"] if !present || jsonNull(providerRaw) { @@ -61,7 +62,7 @@ func (s *Store) withoutProviderKey(raw json.RawMessage) (json.RawMessage, error) } var provider v1.ModelProviderInput if json.Unmarshal(providerRaw, &provider) != nil { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } fingerprinted, err := s.fingerprintedProvider(&provider) if err != nil { @@ -85,7 +86,7 @@ func (s *Store) creationRequestHash(raw json.RawMessage) (pgtype.Text, error) { return pgtype.Text{}, nil } if len(raw) > 16<<20 { - return pgtype.Text{}, ErrInvalidInput + return pgtype.Text{}, sessions.ErrInvalidInput } raw, err := s.withoutProviderKey(raw) if err != nil { @@ -93,39 +94,39 @@ func (s *Store) creationRequestHash(raw json.RawMessage) (pgtype.Text, error) { } canonical, err := jsonobject.Normalize(raw) if err != nil { - return pgtype.Text{}, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return pgtype.Text{}, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } hash := sha256.Sum256(canonical) return pgtype.Text{String: hex.EncodeToString(hash[:]), Valid: true}, nil } // FindSessionCreation recovers recorded caller intent without resolving a mutable source. -func (s *Store) FindSessionCreation(ctx context.Context, tenantID, key string, request json.RawMessage, creator identity.Subject) (SessionCreation, error) { +func (s *Store) FindSessionCreation(ctx context.Context, tenantID, key string, request json.RawMessage, creator identity.Subject) (sessions.Creation, error) { if err := creator.Validate(); err != nil { - return SessionCreation{}, fmt.Errorf("%w: %v", ErrInvalidInput, err) + return sessions.Creation{}, fmt.Errorf("%w: %v", sessions.ErrInvalidInput, err) } tenant, err := parseID(tenantID) if err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } if strings.TrimSpace(key) == "" || len(key) > 128 { - return SessionCreation{}, ErrInvalidInput + return sessions.Creation{}, sessions.ErrInvalidInput } hash, err := s.creationRequestHash(request) if errors.Is(err, credentialcrypto.ErrUnavailable) { // Without the credential key no Session with a provider bundle can have // been committed or can be created; creation reports the missing key // after request validation. - return SessionCreation{}, ErrNotFound + return sessions.Creation{}, sessions.ErrNotFound } if err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } if !hash.Valid { - return SessionCreation{}, ErrInvalidInput + return sessions.Creation{}, sessions.ErrInvalidInput } var row sqlc.Session - var environment *Environment + var environment *sessions.Environment err = s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { q := s.queries.WithTx(tx) var err error @@ -137,24 +138,24 @@ func (s *Store) FindSessionCreation(ctx context.Context, tenantID, key string, r return err }) if errors.Is(err, pgx.ErrNoRows) { - return SessionCreation{}, ErrNotFound + return sessions.Creation{}, sessions.ErrNotFound } if err != nil { - return SessionCreation{}, fmt.Errorf("find session creation: %w", err) + return sessions.Creation{}, fmt.Errorf("find session creation: %w", err) } if row.DeletedAt.Valid || !row.CreatorKind.Valid || !row.CreatorID.Valid || row.CreatorKind.String != creator.Kind || row.CreatorID.String != creator.ID { - return SessionCreation{}, ErrIdempotencyConflict + return sessions.Creation{}, sessions.ErrIdempotencyConflict } // Missing request intent does not imply missing ownership. Known creators may // still fall back to the original resolved-request equivalence at the upsert. if !row.CreationRequestHash.Valid { - return SessionCreation{}, ErrNotFound + return sessions.Creation{}, sessions.ErrNotFound } if row.CreationRequestHash.String != hash.String { - return SessionCreation{}, ErrIdempotencyConflict + return sessions.Creation{}, sessions.ErrIdempotencyConflict } session, err := sessionFromRow(row) session.Environment = environment // The row and cursor share one committed snapshot; later events remain observable. - return SessionCreation{Session: session, Cursor: row.EventSequence}, err + return sessions.Creation{Session: session, Cursor: row.EventSequence}, err } diff --git a/services/core/internal/store/session_creation_identity_test.go b/services/core/internal/store/session_creation_identity_test.go index 3f31b416d..77983a3ff 100644 --- a/services/core/internal/store/session_creation_identity_test.go +++ b/services/core/internal/store/session_creation_identity_test.go @@ -26,14 +26,14 @@ func TestSessionCreationIdentityConvergesOnFrozenSnapshot(t *testing.T) { tenant := uuid.NewString() request := json.RawMessage(`{"agent_id":"source","agent":{"tools":[{"parameters":{"const":9007199254740993}}]}}`) const count = 8 - results := make(chan SessionCreation, count) + results := make(chan sessions.Creation, count) errs := make(chan error, count) var wg sync.WaitGroup for i := range count { wg.Add(1) go func() { defer wg.Done() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "same", CreationRequest: request, Configuration: json.RawMessage(fmt.Sprintf(`{"resolved":%d}`, i)), InitialInputs: []Input{messageInput("one")}} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "same", CreationRequest: request, Configuration: json.RawMessage(fmt.Sprintf(`{"resolved":%d}`, i)), InitialInputs: []sessions.Input{messageInput("one")}} result, err := s.CreateSessionStream(ctx, tenant, input) results <- result errs <- err @@ -47,7 +47,7 @@ func TestSessionCreationIdentityConvergesOnFrozenSnapshot(t *testing.T) { t.Fatal(err) } } - var first SessionCreation + var first sessions.Creation created := 0 for result := range results { if first.Session.ID == "" { @@ -78,13 +78,13 @@ func TestSessionCreationIdentityConvergesOnFrozenSnapshot(t *testing.T) { if err != nil || retry.Created || retry.Session.ID != first.Session.ID || retry.Cursor == 0 { t.Fatal(retry, err) } - if _, err := restarted.FindSessionCreation(ctx, tenant, "same", json.RawMessage(`{"agent_id":"source","agent":{"tools":[{"parameters":{"const":9007199254740992}}]}}`), FixtureCreator()); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := restarted.FindSessionCreation(ctx, tenant, "same", json.RawMessage(`{"agent_id":"source","agent":{"tools":[{"parameters":{"const":9007199254740992}}]}}`), FixtureCreator()); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("numeric identity collapsed", err) } - if _, err := restarted.FindSessionCreation(ctx, uuid.NewString(), "same", request, FixtureCreator()); !errors.Is(err, ErrNotFound) { + if _, err := restarted.FindSessionCreation(ctx, uuid.NewString(), "same", request, FixtureCreator()); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign lookup", err) } - if _, err := restarted.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "same", CreationRequest: json.RawMessage(`{"agent_id":"changed"}`), Configuration: first.Session.Configuration}); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := restarted.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "same", CreationRequest: json.RawMessage(`{"agent_id":"changed"}`), Configuration: first.Session.Configuration}); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed caller admitted", err) } } @@ -93,13 +93,13 @@ func TestSessionCreationIdentityDoesNotInventHistoricalIntent(t *testing.T) { s, pool := testStore(t) ctx := context.Background() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "historical", Configuration: json.RawMessage(`{"agent":{"id":"source","instructions":"original"}}`)} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "historical", Configuration: json.RawMessage(`{"agent":{"id":"source","instructions":"original"}}`)} first, err := s.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) } input.CreationRequest = json.RawMessage(`{"agent_id":"source"}`) - if _, err := s.FindSessionCreation(ctx, tenant, input.IdempotencyKey, input.CreationRequest, FixtureCreator()); !errors.Is(err, ErrNotFound) { + if _, err := s.FindSessionCreation(ctx, tenant, input.IdempotencyKey, input.CreationRequest, FixtureCreator()); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } retry, err := s.CreateSession(ctx, tenant, input) @@ -111,7 +111,7 @@ func TestSessionCreationIdentityDoesNotInventHistoricalIntent(t *testing.T) { t.Fatal("historical intent was manufactured", hash, err) } input.Configuration = json.RawMessage(`{"agent":{"id":"source","instructions":"changed"}}`) - if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(ctx, tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("historical retry rules changed", err) } } @@ -126,7 +126,7 @@ func TestProviderKeyEntersRetryHashesOnlyAsKeyedFingerprint(t *testing.T) { if err != nil { t.Fatal(err) } - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "same-request", ModelProvider: provider, ModelProviderSource: v1.ModelProviderSourceSession, + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "same-request", ModelProvider: provider, ModelProviderSource: v1.ModelProviderSourceSession, Configuration: []byte(`{"agent":{"model":"m"},"environment":{"type":"openai_hosted"}}`), CreationRequest: intent} var hashes [2][2]string for index, seed := range []byte{71, 72} { @@ -148,7 +148,7 @@ func TestProviderKeyEntersRetryHashesOnlyAsKeyedFingerprint(t *testing.T) { cipher, _ := credentialcrypto.New(bytes.Repeat([]byte{71}, 32)) unreadable := input unreadable.IdempotencyKey, unreadable.CreationRequest = "unreadable", json.RawMessage(`{"x_agents_core":{"model_provider":"hash-key-canary"}}`) - if _, err := NewWithCredentialCipher(pool, cipher).CreateSession(t.Context(), uuid.NewString(), unreadable); !errors.Is(err, ErrInvalidInput) { + if _, err := NewWithCredentialCipher(pool, cipher).CreateSession(t.Context(), uuid.NewString(), unreadable); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatal("unreadable provider intent was hashed", err) } } diff --git a/services/core/internal/store/session_creation_stream.go b/services/core/internal/store/session_creation_stream.go index dc447988f..1319353a1 100644 --- a/services/core/internal/store/session_creation_stream.go +++ b/services/core/internal/store/session_creation_stream.go @@ -1,23 +1,15 @@ package store -import "context" +import ( + "context" -// SessionCreation starts observation at the Session upsert. For a new creation, -// CreateSessionStream returns the committed Session projection that -// CreateSession returns, read after the creation commits; Cursor still precedes -// the initial inputs, so their events remain observable exactly once. Retries -// and FindSessionCreation return only the resource row and its cursor. Only a new creation emits a created snapshot and -// streams from Cursor; a stream retry of an existing creation sends no events. -type SessionCreation struct { - Session Session - Created bool - Cursor int64 -} + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" +) // CreateSessionStream shares admission and retry identity with ordinary creation. // The upsert returns its event cursor while holding the Session write lock, before // initial inputs commit. No post-commit cursor lookup may skip those inputs. -func (s *Store) CreateSessionStream(ctx context.Context, tenant string, input CreateSessionInput) (SessionCreation, error) { +func (s *Store) CreateSessionStream(ctx context.Context, tenant string, input sessions.CreateSession) (sessions.Creation, error) { result, err := s.createSession(ctx, tenant, input) if err != nil || !result.Created { // A stream retry sends no events, so it needs no projection read. @@ -25,7 +17,7 @@ func (s *Store) CreateSessionStream(ctx context.Context, tenant string, input Cr } result.Session, err = s.sessionActivity(ctx, result.Session, nil) if err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } return result, nil } diff --git a/services/core/internal/store/session_creation_stream_test.go b/services/core/internal/store/session_creation_stream_test.go index 1db14f55b..6c63da92e 100644 --- a/services/core/internal/store/session_creation_stream_test.go +++ b/services/core/internal/store/session_creation_stream_test.go @@ -16,9 +16,9 @@ func TestCreationStreamStartsBeforeOwnInputsAndRetriesAtUpsertCursor(t *testing. other, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "stream", InitialInputs: []Input{messageInput("first")}} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "stream", InitialInputs: []sessions.Input{messageInput("first")}} var wg sync.WaitGroup - results := make(chan SessionCreation, 8) + results := make(chan sessions.Creation, 8) for i := range 8 { wg.Go(func() { st := s @@ -35,8 +35,8 @@ func TestCreationStreamStartsBeforeOwnInputsAndRetriesAtUpsertCursor(t *testing. } wg.Wait() close(results) - var created SessionCreation - var retries []SessionCreation + var created sessions.Creation + var retries []sessions.Creation for result := range results { if result.Created { if created.Created { @@ -65,7 +65,7 @@ func TestCreationStreamStartsBeforeOwnInputsAndRetriesAtUpsertCursor(t *testing. if initial[1].Event.Item == nil || initial[1].Event.Item.Role != "user" || initial[2].Turn == nil || initial[2].Turn.Status != sessions.TurnQueued { t.Fatal("invalid initial input or activity snapshot", initial) } - encoded := func(value Session) string { + encoded := func(value sessions.Session) string { raw, err := json.Marshal(value) if err != nil { t.Fatal(err) @@ -114,14 +114,14 @@ func TestCreationStreamStartsBeforeOwnInputsAndRetriesAtUpsertCursor(t *testing. if _, cursor, err := s.SessionStreamSnapshot(ctx, tenant, id); err != nil || cursor != all[len(all)-1].Sequence { t.Fatal("stream snapshot cursor", cursor, err) } - if _, _, err := s.SessionStreamSnapshot(ctx, uuid.NewString(), id); !errors.Is(err, ErrNotFound) { + if _, _, err := s.SessionStreamSnapshot(ctx, uuid.NewString(), id); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign stream snapshot", err) } late, err := s.CreateSessionStream(ctx, tenant, input) if err != nil || late.Created || late.Cursor != all[len(all)-1].Sequence { t.Fatal(late, err) } - next, err := s.SubmitInputs(ctx, tenant, id, "next", []Input{messageInput("later")}) + next, err := s.SubmitInputs(ctx, tenant, id, "next", []sessions.Input{messageInput("later")}) if err != nil { t.Fatal(err) } @@ -151,12 +151,12 @@ func TestCreationStreamIdleAndNonstreamRetry(t *testing.T) { s, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "idle"} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "idle"} first, err := s.CreateSessionStream(ctx, tenant, input) if err != nil || !first.Created || first.Cursor != 0 || first.Session.LastTurn != nil { t.Fatal(first, err) } - if _, err := s.SubmitInputs(ctx, tenant, first.Session.ID, "message", []Input{messageInput("later")}); err != nil { + if _, err := s.SubmitInputs(ctx, tenant, first.Session.ID, "message", []sessions.Input{messageInput("later")}); err != nil { t.Fatal(err) } retry, err := s.CreateSessionStream(ctx, tenant, input) diff --git a/services/core/internal/store/session_creator_test.go b/services/core/internal/store/session_creator_test.go index a74b0618c..b380d1947 100644 --- a/services/core/internal/store/session_creator_test.go +++ b/services/core/internal/store/session_creator_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -15,10 +16,10 @@ func TestSessionCreatorIsRequiredBeforeCreation(t *testing.T) { s, _ := testStore(t) tenant := uuid.NewString() input := environmentInput("creator-required", "self_hosted", "/workspace") - input.InitialInputs = []Input{messageInput("initial")} + input.InitialInputs = []sessions.Input{messageInput("initial")} for _, invalid := range []identity.Subject{{}, {Kind: "user"}, {ID: "someone"}, {Kind: "workspace", ID: "someone"}} { input.Creator = invalid - if _, err := s.CreateSessionStream(t.Context(), tenant, input); !errors.Is(err, ErrInvalidInput) { + if _, err := s.CreateSessionStream(t.Context(), tenant, input); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("invalid creator accepted: %v", err) } } @@ -39,7 +40,7 @@ func TestConcurrentSessionCreatorsCannotShareCreationRetry(t *testing.T) { request := json.RawMessage(`{"agent_id":"source"}`) creators := []identity.Subject{{Kind: "user", ID: "same-id"}, {Kind: "service_account", ID: "same-id"}} type outcome struct { - result SessionCreation + result sessions.Creation err error } results := make(chan outcome, 8) @@ -50,7 +51,7 @@ func TestConcurrentSessionCreatorsCannotShareCreationRetry(t *testing.T) { go func() { input := environmentInput("shared-retry", "self_hosted", "/workspace") input.Creator = creators[i%2] - input.CreationRequest, input.InitialInputs = request, []Input{messageInput("once")} + input.CreationRequest, input.InitialInputs = request, []sessions.Input{messageInput("once")} ready.Done() <-start result, err := s.CreateSessionStream(t.Context(), tenant, input) @@ -59,11 +60,11 @@ func TestConcurrentSessionCreatorsCannotShareCreationRetry(t *testing.T) { } ready.Wait() close(start) - var winner Session + var winner sessions.Session succeeded, conflicted, created := 0, 0, 0 for range 8 { got := <-results - if errors.Is(got.err, ErrIdempotencyConflict) { + if errors.Is(got.err, sessions.ErrIdempotencyConflict) { conflicted++ continue } @@ -101,11 +102,11 @@ func TestConcurrentSessionCreatorsCannotShareCreationRetry(t *testing.T) { if err != nil || found.Session.ID != winner.ID || found.Session.Creator == nil || *found.Session.Creator != creator { t.Fatal("creator retry did not survive restart", found, err) } - } else if !errors.Is(err, ErrIdempotencyConflict) { + } else if !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("early recovery ignored creator kind", err) } } - if _, err := restarted.GetSession(t.Context(), uuid.NewString(), winner.ID); !errors.Is(err, ErrNotFound) { + if _, err := restarted.GetSession(t.Context(), uuid.NewString(), winner.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("creator bypassed project isolation", err) } } @@ -139,10 +140,10 @@ func TestHistoricalUnknownCreatorCannotBeClaimedByRetry(t *testing.T) { if err != nil || len(page.Sessions) != 1 || page.Sessions[0].Creator != nil { t.Fatal("historical project reads changed", page, err) } - if _, err := s.FindSessionCreation(ctx, tenant, input.IdempotencyKey, input.CreationRequest, input.Creator); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.FindSessionCreation(ctx, tenant, input.IdempotencyKey, input.CreationRequest, input.Creator); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("early retry claimed historical ownership", err) } - if _, err := s.CreateSessionStream(ctx, tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSessionStream(ctx, tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("upsert claimed historical ownership", err) } for _, statement := range []string{ diff --git a/services/core/internal/store/session_deletion.go b/services/core/internal/store/session_deletion.go index 72dc16ea8..bc074f526 100644 --- a/services/core/internal/store/session_deletion.go +++ b/services/core/internal/store/session_deletion.go @@ -13,10 +13,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -// ErrSessionNotIdle rejects deletion of a Session that still has work or input -// pending. Callers cancel first and delete after the Session settles. -var ErrSessionNotIdle = errors.New("session must be durably idle or failed without required actions before deletion") - // DeleteSession removes public access to a durably idle or failed Session while // retaining state needed to settle execution. The decision is taken under the // Session lock that also orders Turn and input admission, so a concurrent @@ -53,7 +49,7 @@ func (s *Store) DeleteSession(ctx context.Context, tenantID, sessionID string) e // hosted initial input while provisioning. Terminal idle and failed Sessions pass. func requireSessionSettled(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { if _, err := q.GetActiveTurn(ctx, session); err == nil { - return ErrSessionNotIdle + return sessions.ErrNotIdle } else if !errors.Is(err, pgx.ErrNoRows) { return err } @@ -62,7 +58,7 @@ func requireSessionSettled(ctx context.Context, q *sqlc.Queries, session pgtype. return err } if pending { - return ErrSessionNotIdle + return sessions.ErrNotIdle } return nil } diff --git a/services/core/internal/store/session_deletion_execution_test.go b/services/core/internal/store/session_deletion_execution_test.go index da47e5d7b..6dbdc1c03 100644 --- a/services/core/internal/store/session_deletion_execution_test.go +++ b/services/core/internal/store/session_deletion_execution_test.go @@ -9,7 +9,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestDeletedSessionWaitingTurnSettlesWithoutStoppingWorker(t *testing.T) { @@ -31,7 +30,7 @@ func TestDeletedSessionWaitingTurnSettlesWithoutStoppingWorker(t *testing.T) { h.read(testExecutionRequest) h.write(input.TurnID, proto.TypeFunctionCall, proto.FunctionCallPayload{CallID: "pending", Name: "lookup_ticket", Arguments: json.RawMessage(`{}`)}) state := functionState(t, h, 1) - if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrSessionNotIdle) { + if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("waiting Session deleted", err) } // A marker committed by an earlier release still cancels and settles work. @@ -42,8 +41,8 @@ func TestDeletedSessionWaitingTurnSettlesWithoutStoppingWorker(t *testing.T) { if err := h.read(proto.TypePromptCancel).DecodePayload(&request); err != nil { t.Fatal(err) } - raw, _ := json.Marshal(store.FunctionResultInput{TurnID: input.TurnID, CallID: state.RequiredActions[0].CallID, Result: json.RawMessage(`{"success":true,"output":"late"}`)}) - if _, err := worker.SubmitInputs(ctx, h.tenant, h.session.ID, "late", []store.Input{{Kind: "tool_result", Payload: raw}}); !errors.Is(err, store.ErrNotFound) { + raw, _ := json.Marshal(sessions.FunctionResultInput{TurnID: input.TurnID, CallID: state.RequiredActions[0].CallID, Result: json.RawMessage(`{"success":true,"output":"late"}`)}) + if _, err := worker.SubmitInputs(ctx, h.tenant, h.session.ID, "late", []sessions.Input{{Kind: "tool_result", Payload: raw}}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } h.write(input.TurnID, proto.TypeInteractionDecisionAck, proto.InteractionDecisionAckPayload{DeliveryID: request.DeliveryID, Applied: true, Outcome: &proto.DonePayload{Metadata: map[string]any{proto.DoneMetaAgentSessionID: "deleted-native"}}}) @@ -52,10 +51,10 @@ func TestDeletedSessionWaitingTurnSettlesWithoutStoppingWorker(t *testing.T) { if err != nil || bound.NativeSessionID != "deleted-native" { t.Fatal(bound, err) } - if _, err := h.s.GetSession(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSession(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := h.d.Run(ctx, h.tenant, h.session.ID, input.TurnID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.d.Run(ctx, h.tenant, h.session.ID, input.TurnID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted Session must be unavailable to new dispatch", err) } h.session = publicSession(t, h, "unrelated") @@ -69,10 +68,10 @@ func TestDeletedSessionRestartStillReconcilesHiddenClaim(t *testing.T) { h := newDispatchHarness(t) input := h.message("interrupted", "Run") ctx := t.Context() - if _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrSessionNotIdle) { + if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("running Session deleted", err) } if err := h.s.CommitLegacyDeletion(ctx, h.tenant, h.session.ID); err != nil { @@ -88,7 +87,7 @@ func TestDeletedSessionRestartStillReconcilesHiddenClaim(t *testing.T) { if err != nil || turn.Status != sessions.TurnFailed { t.Fatal(turn, err) } - if _, err := h.s.GetSession(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSession(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } @@ -114,7 +113,7 @@ func TestWaitingSessionCancelsThenDeletesThroughWorker(t *testing.T) { h.read(testExecutionRequest) h.write(input.TurnID, proto.TypeFunctionCall, proto.FunctionCallPayload{CallID: "pending", Name: "lookup_ticket", Arguments: json.RawMessage(`{}`)}) state := functionState(t, h, 1) - if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrSessionNotIdle) { + if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("waiting Session deleted", err) } if again := functionState(t, h, 1); again.LastTurn == nil || again.LastTurn.Status != sessions.TurnWaiting || !again.LastTurn.CancelRequestedAt.IsZero() { @@ -128,7 +127,7 @@ func TestWaitingSessionCancelsThenDeletesThroughWorker(t *testing.T) { if err := h.read(proto.TypePromptCancel).DecodePayload(&request); err != nil { t.Fatal(err) } - if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrSessionNotIdle) { + if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("Session deleted before cancellation settled", err) } h.write(input.TurnID, proto.TypeInteractionDecisionAck, proto.InteractionDecisionAckPayload{DeliveryID: request.DeliveryID, Applied: true, Outcome: &proto.DonePayload{Metadata: map[string]any{proto.DoneMetaAgentSessionID: "cancelled-native"}}}) @@ -139,7 +138,7 @@ func TestWaitingSessionCancelsThenDeletesThroughWorker(t *testing.T) { if err == nil { break } - if !errors.Is(err, store.ErrSessionNotIdle) || time.Now().After(deadline) { + if !errors.Is(err, sessions.ErrNotIdle) || time.Now().After(deadline) { t.Fatal("settled Session not deleted", err) } time.Sleep(20 * time.Millisecond) @@ -147,11 +146,11 @@ func TestWaitingSessionCancelsThenDeletesThroughWorker(t *testing.T) { if err := h.s.DeleteSession(ctx, h.tenant, h.session.ID); err != nil { t.Fatal("repeated deletion", err) } - raw, _ := json.Marshal(store.FunctionResultInput{TurnID: input.TurnID, CallID: state.RequiredActions[0].CallID, Result: json.RawMessage(`{"success":true,"output":"late"}`)}) - if _, err := worker.SubmitInputs(ctx, h.tenant, h.session.ID, "late", []store.Input{{Kind: "tool_result", Payload: raw}}); !errors.Is(err, store.ErrNotFound) { + raw, _ := json.Marshal(sessions.FunctionResultInput{TurnID: input.TurnID, CallID: state.RequiredActions[0].CallID, Result: json.RawMessage(`{"success":true,"output":"late"}`)}) + if _, err := worker.SubmitInputs(ctx, h.tenant, h.session.ID, "late", []sessions.Input{{Kind: "tool_result", Payload: raw}}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := h.s.GetSession(ctx, h.tenant, h.session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err := h.s.GetSession(ctx, h.tenant, h.session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } h.session = publicSession(t, h, "unrelated") diff --git a/services/core/internal/store/session_deletion_lifecycle_public_test.go b/services/core/internal/store/session_deletion_lifecycle_public_test.go index ccce7ddee..34b01200d 100644 --- a/services/core/internal/store/session_deletion_lifecycle_public_test.go +++ b/services/core/internal/store/session_deletion_lifecycle_public_test.go @@ -41,12 +41,12 @@ func TestSessionDeletionLifecyclePostgres(t *testing.T) { client := pathIDClient{t: t, server: server} writer := executionOwner(t, db, s).Store - create := func(environment string, initial bool) store.Session { + create := func(environment string, initial bool) sessions.Session { t.Helper() - input := store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), + input := sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{` + deletionAgent + `,"environment":` + environment + `}`)} if initial { - input.InitialInputs = []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"reserved"}`)}} + input.InitialInputs = []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"reserved"}`)}} } session, err := s.CreateSession(ctx, tenant, input) if err != nil { @@ -70,11 +70,11 @@ func TestSessionDeletionLifecyclePostgres(t *testing.T) { case "cancel": _, err = s.RequestCancel(ctx, tenant, session.ID, "cancel") case "function": - err = s.RecordFunctionCall(ctx, tenant, session.ID, receipt.TurnID, store.FunctionCall{CallID: "pending", ExecutorCallID: "native-pending", Name: "lookup", Arguments: json.RawMessage(`{}`)}) + err = s.RecordFunctionCall(ctx, tenant, session.ID, receipt.TurnID, sessions.FunctionCall{CallID: "pending", ExecutorCallID: "native-pending", Name: "lookup", Arguments: json.RawMessage(`{}`)}) case sessions.TurnCompleted, sessions.TurnFailed: _, err = s.CompleteExecution(ctx, tenant, session.ID, receipt.TurnID, status, nil, "", receipt.Sequence) default: - _, err = s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, store.TurnTransition{ExpectedStatus: from, Status: status}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: from, Status: status}) from = status } if err != nil { @@ -83,15 +83,15 @@ func TestSessionDeletionLifecyclePostgres(t *testing.T) { } return session.ID } - reserve := func(session store.Session) store.EnvironmentInputReservation { + reserve := func(session sessions.Session) sessions.EnvironmentInputReservation { t.Helper() - reservation, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "later", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}) - if err != nil || reservation.State != store.EnvironmentInputPending { + reservation, err := s.ReserveEnvironmentInput(ctx, tenant, session.ID, "later", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"later"}`)}}) + if err != nil || reservation.State != sessions.EnvironmentInputPending { t.Fatal(reservation, err) } return reservation } - connect := func(session store.Session) { + connect := func(session sessions.Session) { t.Helper() generation := uuid.NewString() if err := writer.ReplaceEnvironmentConnection(ctx, tenant, session.Environment.ID, generation); err != nil { diff --git a/services/core/internal/store/session_deletion_test.go b/services/core/internal/store/session_deletion_test.go index 4d8edad55..28d25bb6d 100644 --- a/services/core/internal/store/session_deletion_test.go +++ b/services/core/internal/store/session_deletion_test.go @@ -52,7 +52,7 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) { tenant := uuid.NewString() for _, status := range []string{sessions.TurnQueued, sessions.TurnInProgress, sessions.TurnCompleted, sessions.TurnFailed} { t.Run(status, func(t *testing.T) { - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: status} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: status} session, err := s.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) @@ -62,7 +62,7 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) { t.Fatal(err) } if status != sessions.TurnQueued { - _, err = s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } @@ -72,7 +72,7 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) { t.Fatal(err) } } - if err := s.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, ErrNotFound) { + if err := s.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } if status == sessions.TurnQueued || status == sessions.TurnInProgress { @@ -81,7 +81,7 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) { if err != nil { t.Fatal(err) } - if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, ErrSessionNotIdle) { + if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("active Session deleted", err) } turn, err := s.GetTurn(ctx, tenant, session.ID, receipt.TurnID) @@ -100,7 +100,7 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) { t.Fatal(err) } if status == sessions.TurnInProgress { - if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, ErrSessionNotIdle) { + if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("cancelling Session deleted", err) } if _, err := s.CompleteExecution(ctx, tenant, session.ID, receipt.TurnID, sessions.TurnCancelled, nil, "", receipt.Sequence); err != nil { @@ -118,29 +118,29 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) { if err := repeat.DeleteSession(ctx, tenant, session.ID); err != nil { t.Fatal("repeated deletion", err) } - if err := repeat.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, ErrNotFound) { + if err := repeat.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign deleted Session", err) } } if again := sessionDeletedAt(t, pool, session.ID); again != marker { t.Fatal("repeated deletion rewrote the marker", marker, again) } - if _, err := fresh.GetSession(ctx, tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := fresh.GetSession(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := fresh.CreateSession(ctx, tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := fresh.CreateSession(ctx, tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal(err) } - if _, err := fresh.CreateSessionStream(ctx, tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := fresh.CreateSessionStream(ctx, tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal(err) } - if _, err := fresh.SubmitMessage(ctx, tenant, session.ID, "input", json.RawMessage(`{"text":"retained"}`)); !errors.Is(err, ErrNotFound) { + if _, err := fresh.SubmitMessage(ctx, tenant, session.ID, "input", json.RawMessage(`{"text":"retained"}`)); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := fresh.RequestCancel(ctx, tenant, session.ID, "late-cancel"); !errors.Is(err, ErrNotFound) { + if _, err := fresh.RequestCancel(ctx, tenant, session.ID, "late-cancel"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := fresh.ListItems(ctx, tenant, session.ID, "", 20, true); !errors.Is(err, ErrNotFound) { + if _, err := fresh.ListItems(ctx, tenant, session.ID, "", 20, true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } turn, err := fresh.GetTurn(ctx, tenant, session.ID, receipt.TurnID) @@ -154,17 +154,17 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) { if turn.Status != want { t.Fatal(turn) } - if _, err := fresh.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); !errors.Is(err, ErrTurnConflict) { + if _, err := fresh.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } inputs, err := fresh.ListTurnInputs(ctx, tenant, session.ID, receipt.TurnID, 0, 20) if err != nil || len(inputs) == 0 || inputs[0].Sequence != receipt.Sequence { t.Fatal(inputs, err) } - if _, err := fresh.SessionEventCursor(ctx, tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := fresh.SessionEventCursor(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := fresh.ListSessionEvents(ctx, tenant, session.ID, 0); !errors.Is(err, ErrNotFound) { + if _, err := fresh.ListSessionEvents(ctx, tenant, session.ID, 0); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } }) @@ -175,7 +175,7 @@ func TestSessionDeletionSerializesAdmissionBeforeRetryLookup(t *testing.T) { s, pool := testStore(t) ctx := t.Context() tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "creation"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "creation"}) if err != nil { t.Fatal(err) } @@ -195,7 +195,7 @@ func TestSessionDeletionSerializesAdmissionBeforeRetryLookup(t *testing.T) { if err := tx.Commit(ctx); err != nil { t.Fatal(err) } - if err := <-done; !errors.Is(err, ErrNotFound) { + if err := <-done; !errors.Is(err, sessions.ErrNotFound) { t.Fatal("retry admitted after deletion", err) } } @@ -264,7 +264,7 @@ func TestSessionDeletionRacesAdmissionUnderSessionLock(t *testing.T) { tenant, session := environmentInputSession(t, s) return tenant, session.ID }, func(ctx context.Context, s *Store, tenant, session string) error { - _, err := s.ReserveEnvironmentInput(ctx, tenant, session, "racing", []Input{messageInput("racing")}) + _, err := s.ReserveEnvironmentInput(ctx, tenant, session, "racing", []sessions.Input{messageInput("racing")}) return err }}, } @@ -291,7 +291,7 @@ func TestSessionDeletionRacesAdmissionUnderSessionLock(t *testing.T) { if err := kind.admit(t.Context(), s, tenant, session); err != nil { t.Fatal(err) } - if err := <-deleted; !errors.Is(err, ErrSessionNotIdle) { + if err := <-deleted; !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("deletion ignored committed admission", err) } if sessionDeletedAt(t, pool, session).Valid { @@ -318,7 +318,7 @@ func TestSessionDeletionRacesAdmissionUnderSessionLock(t *testing.T) { if err := s.DeleteSession(t.Context(), tenant, session); err != nil { t.Fatal(err) } - if err := <-admitted; !errors.Is(err, ErrNotFound) { + if err := <-admitted; !errors.Is(err, sessions.ErrNotFound) { t.Fatal("admission after deletion", err) } var turns, reservations int @@ -351,7 +351,7 @@ func TestSessionDeletionRacesAdmissionUnderSessionLock(t *testing.T) { for _, err := range []error{first, second} { if err != nil { failures++ - if !errors.Is(err, ErrSessionNotIdle) && !errors.Is(err, ErrNotFound) { + if !errors.Is(err, sessions.ErrNotIdle) && !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } @@ -373,7 +373,7 @@ func TestSessionDeletionKeepsProvisioningInputPlacementUntilSettled(t *testing.T ctx := t.Context() tenant := uuid.NewString() input := managerSessionInput("reserved-input") - input.InitialInputs = []Input{messageInput("reserved")} + input.InitialInputs = []sessions.Input{messageInput("reserved")} session, err := s.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) @@ -401,10 +401,10 @@ func TestSessionDeletionKeepsProvisioningInputPlacementUntilSettled(t *testing.T if before.deleted.Valid || before.released.Valid || before.retained != 1 || before.reserved != 1 { t.Fatal("unexpected reserved placement", before) } - if err := s.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, ErrNotFound) { + if err := s.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign deletion", err) } - if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, ErrSessionNotIdle) { + if err := s.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("provisioning input deleted", err) } if after := read(); after != before { @@ -429,7 +429,7 @@ func TestSessionDeletionKeepsProvisioningInputPlacementUntilSettled(t *testing.T if again := read(); again != deleted { t.Fatal("repeated deletion changed timestamps", deleted, again) } - if err := s.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, ErrNotFound) { + if err := s.DeleteSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign deletion of a deleted Session", err) } } diff --git a/services/core/internal/store/session_diagnostics.go b/services/core/internal/store/session_diagnostics.go index b9ab3d181..1b435c539 100644 --- a/services/core/internal/store/session_diagnostics.go +++ b/services/core/internal/store/session_diagnostics.go @@ -3,7 +3,6 @@ package store import ( "context" "errors" - "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" @@ -12,29 +11,14 @@ import ( "github.com/jackc/pgx/v5" ) -// ItemDiagnosticTiming records Core database receipt and settlement, never native -// execution duration. Historical terminal Items can have unknown settlement. -type ItemDiagnosticTiming struct { - ItemID string - StartedAt time.Time - CompletedAt *time.Time -} - -type TurnDiagnosticsSnapshot struct { - Session Session - Turn sessions.Turn - Items []ItemDiagnosticTiming - ItemsTruncated bool -} - // GetSessionDiagnosticsSnapshot keeps all existing Session projections in one // read-only snapshot, including the failure precedence used by the public API. -func (s *Store) GetSessionDiagnosticsSnapshot(ctx context.Context, tenantID, sessionID string) (Session, error) { +func (s *Store) GetSessionDiagnosticsSnapshot(ctx context.Context, tenantID, sessionID string) (sessions.Session, error) { tenant, err := parseID(tenantID) if err != nil { - return Session{}, err + return sessions.Session{}, err } - var session Session + var session sessions.Session err = s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { q := s.queries.WithTx(tx) row, err := q.GetSession(ctx, sqlc.GetSessionParams{TenantID: tenant, ID: pgunit.PathID(sessionID)}) @@ -49,19 +33,19 @@ func (s *Store) GetSessionDiagnosticsSnapshot(ctx context.Context, tenantID, ses return err }) if errors.Is(err, pgx.ErrNoRows) { - return Session{}, ErrNotFound + return sessions.Session{}, sessions.ErrNotFound } return session, err } // GetTurnDiagnosticsSnapshot reads only root Turns and their root Items. Its // bounded query and public projection share the same committed snapshot. -func (s *Store) GetTurnDiagnosticsSnapshot(ctx context.Context, tenantID, sessionID, turnID string) (TurnDiagnosticsSnapshot, error) { +func (s *Store) GetTurnDiagnosticsSnapshot(ctx context.Context, tenantID, sessionID, turnID string) (sessions.TurnDiagnosticsSnapshot, error) { params, err := publicTurnLookup(tenantID, sessionID, turnID) if err != nil { - return TurnDiagnosticsSnapshot{}, err + return sessions.TurnDiagnosticsSnapshot{}, err } - result := TurnDiagnosticsSnapshot{Items: []ItemDiagnosticTiming{}} + result := sessions.TurnDiagnosticsSnapshot{Items: []sessions.ItemDiagnosticTiming{}} err = s.pooled.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { q := s.queries.WithTx(tx) turn, err := q.GetTurn(ctx, params) @@ -86,7 +70,7 @@ func (s *Store) GetTurnDiagnosticsSnapshot(ctx context.Context, tenantID, sessio rows = rows[:1000] } for _, row := range rows { - item := ItemDiagnosticTiming{ItemID: uuid.UUID(row.ID.Bytes).String(), StartedAt: row.CreatedAt.Time} + item := sessions.ItemDiagnosticTiming{ItemID: uuid.UUID(row.ID.Bytes).String(), StartedAt: row.CreatedAt.Time} if row.SettledAt.Valid { at := row.SettledAt.Time item.CompletedAt = &at @@ -96,7 +80,7 @@ func (s *Store) GetTurnDiagnosticsSnapshot(ctx context.Context, tenantID, sessio return nil }) if errors.Is(err, pgx.ErrNoRows) { - return TurnDiagnosticsSnapshot{}, ErrNotFound + return sessions.TurnDiagnosticsSnapshot{}, sessions.ErrNotFound } return result, err } diff --git a/services/core/internal/store/session_diagnostics_test.go b/services/core/internal/store/session_diagnostics_test.go index 28d0b6f24..20949ddf1 100644 --- a/services/core/internal/store/session_diagnostics_test.go +++ b/services/core/internal/store/session_diagnostics_test.go @@ -16,8 +16,8 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func diagnosticToolEvent(id, stage, status string) ExecutionEvent { - return ExecutionEvent{Kind: "tool_call", Payload: json.RawMessage(fmt.Sprintf(`{"id":%q,"stage":%q,"observation":{"kind":"command","command":"private-command-canary","status":%q}}`, id, stage, status))} +func diagnosticToolEvent(id, stage, status string) sessions.ExecutionEvent { + return sessions.ExecutionEvent{Kind: "tool_call", Payload: json.RawMessage(fmt.Sprintf(`{"id":%q,"stage":%q,"observation":{"kind":"command","command":"private-command-canary","status":%q}}`, id, stage, status))} } func TestDiagnosticItemReceiptSettlementAndReplay(t *testing.T) { @@ -27,8 +27,8 @@ func TestDiagnosticItemReceiptSettlementAndReplay(t *testing.T) { transition(t, s, tenant, session.ID, receipt.TurnID, sessions.TurnQueued, sessions.TurnInProgress) before := diagnosticToolEvent("cmd", "before", "in_progress") after := diagnosticToolEvent("cmd", "after", "failed") - for i, event := range []ExecutionEvent{before, after} { - if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, int32(i+1), []ExecutionEvent{event}); err != nil { + for i, event := range []sessions.ExecutionEvent{before, after} { + if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, int32(i+1), []sessions.ExecutionEvent{event}); err != nil { t.Fatal(err) } } @@ -47,12 +47,12 @@ func TestDiagnosticItemReceiptSettlementAndReplay(t *testing.T) { if item.CompletedAt == nil || !item.CompletedAt.Equal(received) || item.CompletedAt.Before(item.StartedAt) { t.Fatal("Item did not use terminal receipt", item, received) } - if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, 2, []ExecutionEvent{after}); err != nil { + if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, 2, []sessions.ExecutionEvent{after}); err != nil { t.Fatal(err) } // A terminal legacy Item with unknown settlement must remain unknown even on a repeated upsert. runtimeSuspensionSQL(t, s.pool, "UPDATE session_items SET settled_at=NULL WHERE id=$1", item.ItemID) - if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, 3, []ExecutionEvent{after}); err != nil { + if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, 3, []sessions.ExecutionEvent{after}); err != nil { t.Fatal(err) } transition(t, s, tenant, session.ID, receipt.TurnID, sessions.TurnInProgress, sessions.TurnFailed) @@ -68,7 +68,7 @@ func TestDiagnosticForceSettlementIgnoresNativeClock(t *testing.T) { s, w, owner := managedIdleClockFixture(t) turn := uuid.NewString() runtimeSuspensionSQL(t, s.pool, "INSERT INTO turns(id,session_id,status,started_at) VALUES($1,$2,'in_progress',clock_timestamp())", turn, owner.SessionID) - events := []ExecutionEvent{diagnosticToolEvent("first", "before", "in_progress"), diagnosticToolEvent("second", "before", "in_progress")} + events := []sessions.ExecutionEvent{diagnosticToolEvent("first", "before", "in_progress"), diagnosticToolEvent("second", "before", "in_progress")} if err := w.AppendTurnEvents(t.Context(), owner.TenantID, owner.SessionID, turn, 1, events); err != nil { t.Fatal(err) } @@ -110,7 +110,7 @@ func TestDiagnosticSettlementWaitsForSessionLock(t *testing.T) { } done := make(chan error, 1) go func() { - _, err := s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed}) + _, err := s.TransitionTurn(t.Context(), tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed}) done <- err }() // Wait for the actual competing transaction to block, not a scheduler delay. @@ -182,15 +182,15 @@ func TestDiagnosticTimingBoundOrderAndIsolation(t *testing.T) { t.Fatal("exact limit falsely truncated", len(exact.Items), exact.ItemsTruncated, err) } for _, ids := range [][3]string{{uuid.NewString(), session.ID, receipt.TurnID}, {tenant, "malformed", receipt.TurnID}, {tenant, session.ID, uuid.NewString()}} { - if _, err := s.GetTurnDiagnosticsSnapshot(t.Context(), ids[0], ids[1], ids[2]); !errors.Is(err, ErrNotFound) { + if _, err := s.GetTurnDiagnosticsSnapshot(t.Context(), ids[0], ids[1], ids[2]); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("scope leaked", ids, err) } } runtimeSuspensionSQL(t, pool, "UPDATE sessions SET deleted_at=clock_timestamp() WHERE id=$1", session.ID) - if _, err := s.GetTurnDiagnosticsSnapshot(t.Context(), tenant, session.ID, receipt.TurnID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetTurnDiagnosticsSnapshot(t.Context(), tenant, session.ID, receipt.TurnID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted root visible", err) } - if _, err := s.GetSessionDiagnosticsSnapshot(t.Context(), tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSessionDiagnosticsSnapshot(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted Session visible", err) } } @@ -199,7 +199,7 @@ func TestDiagnosticProvisioningDetailAtomicAndPrivate(t *testing.T) { s, pool := testStore(t) tenant := uuid.NewString() input := environmentInput("safe-detail", "openai_hosted", "/workspace") - input.InitialInputs = []Input{messageInput("initial")} + input.InitialInputs = []sessions.Input{messageInput("initial")} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -217,9 +217,9 @@ func TestDiagnosticProvisioningDetailAtomicAndPrivate(t *testing.T) { t.Cleanup(func() { _, _ = pool.Exec(context.Background(), "ALTER TABLE session_events DROP CONSTRAINT IF EXISTS "+constraint) }) - failure := ProvisioningFailure{Step: ProvisioningSetupCommand, Index: 2, ExitCode: 7} + failure := sessions.ProvisioningFailure{Step: sessions.ProvisioningSetupCommand, Index: 2, ExitCode: 7} runtimeSuspensionSQL(t, pool, "UPDATE environments SET initialization='running' WHERE id=$1", owner.EnvironmentID) - preparation := EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: owner.DeviceID} + preparation := sessions.EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: owner.DeviceID} if err = writer.FailEnvironmentInitialization(t.Context(), preparation, failure); err == nil { t.Fatal("failure committed without events") } @@ -280,7 +280,7 @@ func TestDiagnosticFirstSettlementSurvivesStoredStatusRegression(t *testing.T) { tenant, session := newTurnSession(t, s) receipt := submitMessage(t, s, tenant, session.ID, "start") transition(t, s, tenant, session.ID, receipt.TurnID, sessions.TurnQueued, sessions.TurnInProgress) - events := []ExecutionEvent{diagnosticToolEvent("cmd", "before", "in_progress"), diagnosticToolEvent("cmd", "after", "completed")} + events := []sessions.ExecutionEvent{diagnosticToolEvent("cmd", "before", "in_progress"), diagnosticToolEvent("cmd", "after", "completed")} if err := s.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, 1, events); err != nil { t.Fatal(err) } @@ -315,7 +315,7 @@ func TestDiagnosticRootReadRejectsActualChildTurn(t *testing.T) { runtimeSuspensionSQL(t, s.pool, `INSERT INTO turn_events(session_id,turn_id,ordinal,kind,payload) VALUES($1,$2,1,'subagent','{}')`, owner.SessionID, root) runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, owner.SessionID, owner.DeviceID, root) runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_turns(id,session_id,subagent_id,native_id,status,created_at) VALUES($1,$2,$3,'child-turn','in_progress',clock_timestamp())`, turn, owner.SessionID, child) - if _, err := w.GetTurnDiagnosticsSnapshot(t.Context(), owner.TenantID, owner.SessionID, turn); !errors.Is(err, ErrNotFound) { + if _, err := w.GetTurnDiagnosticsSnapshot(t.Context(), owner.TenantID, owner.SessionID, turn); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("child Turn became root diagnostics", err) } } diff --git a/services/core/internal/store/session_environment_snapshot.go b/services/core/internal/store/session_environment_snapshot.go index 43f42ad92..334aebaaf 100644 --- a/services/core/internal/store/session_environment_snapshot.go +++ b/services/core/internal/store/session_environment_snapshot.go @@ -5,10 +5,11 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" ) -func sessionEnvironmentSnapshot(ctx context.Context, q *sqlc.Queries, session sqlc.Session) (*Environment, error) { +func sessionEnvironmentSnapshot(ctx context.Context, q *sqlc.Queries, session sqlc.Session) (*sessions.Environment, error) { row, err := q.GetSessionEnvironment(ctx, sqlc.GetSessionEnvironmentParams{TenantID: session.TenantID, ID: session.ID}) if errors.Is(err, pgx.ErrNoRows) { return nil, nil diff --git a/services/core/internal/store/session_environment_snapshot_test.go b/services/core/internal/store/session_environment_snapshot_test.go index 400a0e3d6..39d54f2d2 100644 --- a/services/core/internal/store/session_environment_snapshot_test.go +++ b/services/core/internal/store/session_environment_snapshot_test.go @@ -6,12 +6,14 @@ import ( "testing" "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestSelfHostedCreationSnapshotRetainsEnvironmentAndCursor(t *testing.T) { s, _ := testStore(t) tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "environment-snapshot", + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "environment-snapshot", Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), CreationRequest: json.RawMessage(`{"agent_id":"saved-agent","environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), } @@ -23,8 +25,8 @@ func TestSelfHostedCreationSnapshotRetainsEnvironmentAndCursor(t *testing.T) { if environment.ID == "" || environment.SessionID != created.Session.ID || environment.TenantID != tenant || environment.Status != "pending" { t.Fatal("incorrect creation association", environment) } - pending, err := s.ReserveEnvironmentInput(t.Context(), tenant, created.Session.ID, "later", []Input{messageInput("later")}) - if err != nil || pending.State != EnvironmentInputPending { + pending, err := s.ReserveEnvironmentInput(t.Context(), tenant, created.Session.ID, "later", []sessions.Input{messageInput("later")}) + if err != nil || pending.State != sessions.EnvironmentInputPending { t.Fatal(pending, err) } events, err := s.ListSessionEvents(t.Context(), tenant, created.Session.ID, created.Cursor) @@ -39,7 +41,7 @@ func TestSelfHostedCreationSnapshotRetainsEnvironmentAndCursor(t *testing.T) { if err != nil { t.Fatal(err) } - for _, value := range []SessionCreation{retry, recovered} { + for _, value := range []sessions.Creation{retry, recovered} { snapshot := value.Session if value.Created || value.Cursor != events[0].Sequence || snapshot.Environment == nil || snapshot.Environment.ID != environment.ID || string(snapshot.Environment.Configuration) != string(environment.Configuration) { t.Fatal("retry changed Environment or cursor", value) @@ -47,14 +49,14 @@ func TestSelfHostedCreationSnapshotRetainsEnvironmentAndCursor(t *testing.T) { } // Recorded-intent lookup and upsert retries return only the row; they read // no projection because a stream retry sends no events. - for _, snapshot := range []Session{recovered.Session, retry.Session} { + for _, snapshot := range []sessions.Session{recovered.Session, retry.Session} { if snapshot.LastTurn != nil || snapshot.EnvironmentInputActivity != nil || snapshot.Usage != nil { t.Fatal("creation retry borrowed later activity", snapshot) } } changedCreator := input.Creator changedCreator.ID = "another-creator" - if _, err := s.FindSessionCreation(t.Context(), tenant, input.IdempotencyKey, input.CreationRequest, changedCreator); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.FindSessionCreation(t.Context(), tenant, input.IdempotencyKey, input.CreationRequest, changedCreator); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("retry creator isolation", err) } current, err := s.GetSession(t.Context(), tenant, created.Session.ID) diff --git a/services/core/internal/store/session_events.go b/services/core/internal/store/session_events.go index f32b51430..81c48d111 100644 --- a/services/core/internal/store/session_events.go +++ b/services/core/internal/store/session_events.go @@ -12,8 +12,6 @@ import ( "github.com/jackc/pgx/v5" ) -var ErrStreamGap = errors.New("live event buffer exceeded; recover through Session and Items reads") - func (s *Store) SessionEventCursor(ctx context.Context, tenantID, sessionID string) (int64, error) { tenant, err := parseID(tenantID) if err != nil { @@ -22,14 +20,14 @@ func (s *Store) SessionEventCursor(ctx context.Context, tenantID, sessionID stri id := pgunit.PathID(sessionID) cursor, err := s.queries.SessionEventCursor(ctx, sqlc.SessionEventCursorParams{TenantID: tenant, ID: id}) if errors.Is(err, pgx.ErrNoRows) { - return 0, ErrNotFound + return 0, sessions.ErrNotFound } return cursor, err } func (s *Store) ListSessionEvents(ctx context.Context, tenantID, sessionID string, after int64) ([]sessions.SessionChange, error) { if after < 0 { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } latest, err := s.SessionEventCursor(ctx, tenantID, sessionID) if err != nil { @@ -49,11 +47,11 @@ func (s *Store) ListSessionEvents(ctx context.Context, tenantID, sessionID strin } changes := make([]sessions.SessionChange, 0, len(rows)) if len(rows) == 0 && latest > after { - return nil, ErrStreamGap + return nil, sessions.ErrStreamGap } for _, row := range rows { if row.Sequence != after+1 { - return nil, ErrStreamGap + return nil, sessions.ErrStreamGap } var change sessions.SessionChange decoder := json.NewDecoder(bytes.NewReader(row.Payload)) diff --git a/services/core/internal/store/session_events_test.go b/services/core/internal/store/session_events_test.go index fe40c827f..179b84102 100644 --- a/services/core/internal/store/session_events_test.go +++ b/services/core/internal/store/session_events_test.go @@ -18,7 +18,7 @@ func TestSessionEventsAreVisibleOnlyAfterCommit(t *testing.T) { ctx := context.Background() s, pool := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "commit"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "commit"}) if err != nil { t.Fatal(err) } @@ -52,7 +52,7 @@ func TestSessionEventsCommitSnapshotsRetriesAndIsolation(t *testing.T) { ctx := context.Background() s, pool := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "stream"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "stream"}) if err != nil { t.Fatal(err) } @@ -71,10 +71,10 @@ func TestSessionEventsCommitSnapshotsRetriesAndIsolation(t *testing.T) { if before != after { t.Fatal("input retry published duplicate events") } - if _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - batch := []store.ExecutionEvent{ + batch := []sessions.ExecutionEvent{ {Kind: "delta", Payload: json.RawMessage(`{"item_id":"first","delta":"partial"}`)}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"first","delta":" partial"}`)}, {Kind: "delta", Payload: json.RawMessage(`{"item_id":"first","delta":" partial"}`)}, @@ -86,7 +86,7 @@ func TestSessionEventsCommitSnapshotsRetriesAndIsolation(t *testing.T) { } } before, _ = s.SessionEventCursor(ctx, tenant, session.ID) - if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 5, []store.ExecutionEvent{ + if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 5, []sessions.ExecutionEvent{ {Kind: "delta", Payload: json.RawMessage(`{"item_id":"discarded","delta":"rollback"}`)}, {Kind: "output_message", Payload: json.RawMessage(`{"status":"invalid"}`)}, }); err == nil { @@ -138,7 +138,7 @@ func TestSessionEventsCommitSnapshotsRetriesAndIsolation(t *testing.T) { if counts["agent.session.turn.output_text.delta"] != 3 || counts["agent.session.turn.item.done"] != 2 { t.Fatal(counts) } - if _, err = s.ListSessionEvents(ctx, uuid.NewString(), session.ID, 0); !errors.Is(err, store.ErrNotFound) { + if _, err = s.ListSessionEvents(ctx, uuid.NewString(), session.ID, 0); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign event access", err) } before, _ = s.SessionEventCursor(ctx, tenant, session.ID) @@ -155,7 +155,7 @@ func TestSessionEventsRetentionAndQueuedCancellation(t *testing.T) { ctx := context.Background() s, pool := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "retention"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "retention"}) if err != nil { t.Fatal(err) } @@ -164,9 +164,9 @@ func TestSessionEventsRetentionAndQueuedCancellation(t *testing.T) { t.Error(err) } }) - inputs := make([]store.Input, 64) + inputs := make([]sessions.Input, 64) for i := range inputs { - inputs[i] = store.Input{Kind: "message", Payload: json.RawMessage(`{"text":"input"}`)} + inputs[i] = sessions.Input{Kind: "message", Payload: json.RawMessage(`{"text":"input"}`)} } for range 5 { if _, err = s.SubmitInputs(ctx, tenant, session.ID, uuid.NewString(), inputs); err != nil { @@ -177,7 +177,7 @@ func TestSessionEventsRetentionAndQueuedCancellation(t *testing.T) { if err = pool.QueryRow(ctx, "SELECT count(*) FROM session_events WHERE session_id=$1", session.ID).Scan(&count); err != nil || count != 256 { t.Fatal(count, err) } - if _, err = s.ListSessionEvents(ctx, tenant, session.ID, 0); !errors.Is(err, store.ErrStreamGap) { + if _, err = s.ListSessionEvents(ctx, tenant, session.ID, 0); !errors.Is(err, sessions.ErrStreamGap) { t.Fatal("lagging reader did not detect missing events", err) } cursor, err := s.SessionEventCursor(ctx, tenant, session.ID) diff --git a/services/core/internal/store/session_execution_configuration.go b/services/core/internal/store/session_execution_configuration.go index dd74455b3..da0cc42fc 100644 --- a/services/core/internal/store/session_execution_configuration.go +++ b/services/core/internal/store/session_execution_configuration.go @@ -9,6 +9,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -28,12 +29,12 @@ func saveSessionExecutionConfiguration(ctx context.Context, q *sqlc.Queries, ses } if !sameExecutionValue(frozen.Model.Value, model) || frozen.Harness.Value == nil || *frozen.Harness.Value != session.Engine || !validExecutionSource(frozen.Model.Source) || !validExecutionSource(frozen.Harness.Source) { - return fmt.Errorf("%w: execution projection does not match Session configuration", ErrInvalidInput) + return fmt.Errorf("%w: execution projection does not match Session configuration", sessions.ErrInvalidInput) } switch frozen.ModelProvider.Source { case "deployment": if provider == nil { - return fmt.Errorf("%w: execution projection has no model provider", ErrInvalidInput) + return fmt.Errorf("%w: execution projection has no model provider", sessions.ErrInvalidInput) } // The deployment default is readable with the same Core key, so the // safe view is recorded from the frozen bundle itself. Native options @@ -45,13 +46,13 @@ func saveSessionExecutionConfiguration(ctx context.Context, q *sqlc.Queries, ses } case "session", "agent": if provider == nil || frozen.ModelProvider.Status != "available" || frozen.ModelProvider.Configuration == nil || *frozen.ModelProvider.Configuration != *provider.SafeView() { - return fmt.Errorf("%w: execution projection does not match model provider", ErrInvalidInput) + return fmt.Errorf("%w: execution projection does not match model provider", sessions.ErrInvalidInput) } case "unknown": frozen.ModelProvider.Status = "unavailable" frozen.ModelProvider.Configuration = nil default: - return fmt.Errorf("%w: invalid execution projection source", ErrInvalidInput) + return fmt.Errorf("%w: invalid execution projection source", sessions.ErrInvalidInput) } normalizeExecutionProjection(&frozen, uuid.UUID(session.ID.Bytes).String()) raw, err := json.Marshal(frozen) @@ -70,7 +71,7 @@ func (s *Store) GetSessionExecutionConfiguration(ctx context.Context, tenantID, } row, err := s.queries.GetSessionExecutionConfiguration(ctx, sqlc.GetSessionExecutionConfigurationParams{TenantID: tenant, SessionID: pgunit.PathID(sessionID)}) if errors.Is(err, pgx.ErrNoRows) { - return v1.SessionExecutionConfiguration{}, ErrNotFound + return v1.SessionExecutionConfiguration{}, sessions.ErrNotFound } if err != nil { return v1.SessionExecutionConfiguration{}, fmt.Errorf("get session execution configuration: %w", err) diff --git a/services/core/internal/store/session_execution_configuration_test.go b/services/core/internal/store/session_execution_configuration_test.go index c6c5cc662..0f80f4d0b 100644 --- a/services/core/internal/store/session_execution_configuration_test.go +++ b/services/core/internal/store/session_execution_configuration_test.go @@ -13,12 +13,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) -func executionProjectionInput(source string) CreateSessionInput { +func executionProjectionInput(source string) sessions.CreateSession { model, harness := "frozen-model", "codex" - return CreateSessionInput{ + return sessions.CreateSession{ Creator: FixtureCreator(), Engine: harness, IdempotencyKey: uuid.NewString(), Configuration: []byte(`{"agent":{"model":"frozen-model"},"environment":{"type":"openai_hosted"}}`), ExecutionConfiguration: &v1.SessionExecutionConfiguration{ @@ -46,7 +47,7 @@ func TestSessionExecutionConfigurationFrozenAcrossCreationPathsAndRetry(t *testi input.ExecutionConfiguration.Object = "untrusted-object" input.ExecutionConfiguration.SchemaVersion = 99 input.ExecutionConfiguration.SessionID = "untrusted-session" - var session Session + var session sessions.Session var err error if stream { created, e := s.CreateSessionStream(t.Context(), tenant, input) @@ -108,14 +109,14 @@ func TestSessionExecutionConfigurationFrozenAcrossCreationPathsAndRetry(t *testi } } for _, lookup := range []struct{ tenant, id string }{{uuid.NewString(), session.ID}, {tenant, uuid.NewString()}, {tenant, "malformed"}} { - if _, err := reader.GetSessionExecutionConfiguration(t.Context(), lookup.tenant, lookup.id); !errors.Is(err, ErrNotFound) { + if _, err := reader.GetSessionExecutionConfiguration(t.Context(), lookup.tenant, lookup.id); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign/missing projection read differed", err) } } if err := s.DeleteSession(t.Context(), tenant, session.ID); err != nil { t.Fatal(err) } - if _, err := reader.GetSessionExecutionConfiguration(t.Context(), tenant, session.ID); !errors.Is(err, ErrNotFound) { + if _, err := reader.GetSessionExecutionConfiguration(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted Session projection remained public", err) } }) @@ -127,7 +128,7 @@ func TestSessionExecutionConfigurationHistoricalProvenance(t *testing.T) { s, pool := testStore(t) tenant := uuid.NewString() for _, configuration := range []string{`{}`, `{"agent":{"model":null}}`, `{"agent":{"model":"historical-model"},"model_provider_configured":true}`} { - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: []byte(configuration)} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: []byte(configuration)} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) @@ -185,7 +186,7 @@ func TestSessionExecutionConfigurationRollbackAndValidation(t *testing.T) { case "post_projection_failure": input.InitialFiles = []environmentconfig.InitialFile{{Type: "inline", Path: "invalid-path"}} } - if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, ErrInvalidInput) { + if _, err := s.CreateSession(t.Context(), tenant, input); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("%s: invalid projection/creation accepted: %v", kind, err) } } diff --git a/services/core/internal/store/session_initial_input.go b/services/core/internal/store/session_initial_input.go index 0d344acc8..878fb7ea2 100644 --- a/services/core/internal/store/session_initial_input.go +++ b/services/core/internal/store/session_initial_input.go @@ -14,13 +14,14 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" ) -func validateInitialInputs(inputs []Input) ([]Input, json.RawMessage, error) { +func validateInitialInputs(inputs []sessions.Input) ([]sessions.Input, json.RawMessage, error) { for _, input := range inputs { if input.Kind != "message" { - return nil, nil, ErrInvalidInput + return nil, nil, sessions.ErrInvalidInput } } return validateInputs(inputs) @@ -28,9 +29,9 @@ func validateInitialInputs(inputs []Input) ([]Input, json.RawMessage, error) { // The Session upsert locks retries. Only the new row reserves or admits work, so a // retry after completion or later Turns cannot submit the original input again. -func (s *Store) createSessionResources(ctx context.Context, tenant string, params sqlc.CreateSessionParams, inputs []Input, encodedInput json.RawMessage, files []environmentconfig.InitialFile, setup environmentconfig.Setup, provider *v1.ModelProviderInput, executionConfiguration *v1.SessionExecutionConfiguration, providerSource string, deploymentRevision uuid.UUID) (sqlc.Session, *Environment, error) { +func (s *Store) createSessionResources(ctx context.Context, tenant string, params sqlc.CreateSessionParams, inputs []sessions.Input, encodedInput json.RawMessage, files []environmentconfig.InitialFile, setup environmentconfig.Setup, provider *v1.ModelProviderInput, executionConfiguration *v1.SessionExecutionConfiguration, providerSource string, deploymentRevision uuid.UUID) (sqlc.Session, *sessions.Environment, error) { var row sqlc.Session - var environment *Environment + var environment *sessions.Environment err := s.pooled.Transaction(ctx, func(ctx context.Context, tx pgx.Tx) error { q := s.queries.WithTx(tx) var err error diff --git a/services/core/internal/store/session_initial_input_test.go b/services/core/internal/store/session_initial_input_test.go index 887c3deb5..2a66829b8 100644 --- a/services/core/internal/store/session_initial_input_test.go +++ b/services/core/internal/store/session_initial_input_test.go @@ -17,9 +17,9 @@ func TestInitialInputCreationRetriesAcrossConnectionsAndLaterTurns(t *testing.T) other, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "initial", InitialInputs: []Input{messageInput("first"), messageInput("second")}} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "initial", InitialInputs: []sessions.Input{messageInput("first"), messageInput("second")}} var wg sync.WaitGroup - results := make(chan Session, 8) + results := make(chan sessions.Session, 8) for i := range 8 { wg.Add(1) go func() { @@ -38,7 +38,7 @@ func TestInitialInputCreationRetriesAcrossConnectionsAndLaterTurns(t *testing.T) } wg.Wait() close(results) - var first Session + var first sessions.Session for session := range results { if first.ID == "" { first = session @@ -57,17 +57,17 @@ func TestInitialInputCreationRetriesAcrossConnectionsAndLaterTurns(t *testing.T) if !strings.Contains(string(inputs[0].Payload), "first") || !strings.Contains(string(inputs[1].Payload), "second") { t.Fatal(inputs) } - for _, changed := range [][]Input{nil, {messageInput("changed")}, {input.InitialInputs[1], input.InitialInputs[0]}} { + for _, changed := range [][]sessions.Input{nil, {messageInput("changed")}, {input.InitialInputs[1], input.InitialInputs[0]}} { request := input request.InitialInputs = changed - if _, err := s.CreateSession(ctx, tenant, request); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(ctx, tenant, request); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed initial input accepted", err) } } transition(t, s, tenant, first.ID, first.LastTurn.ID, sessions.TurnQueued, sessions.TurnInProgress) transition(t, s, tenant, first.ID, first.LastTurn.ID, sessions.TurnInProgress, sessions.TurnCompleted) // The same caller key at the events endpoint is an independent request. - next, err := s.SubmitInputs(ctx, tenant, first.ID, input.IdempotencyKey, []Input{messageInput("later")}) + next, err := s.SubmitInputs(ctx, tenant, first.ID, input.IdempotencyKey, []sessions.Input{messageInput("later")}) if err != nil || len(next) != 1 || next[0].TurnID == first.LastTurn.ID { t.Fatal(next, err) } @@ -108,7 +108,7 @@ func TestInitialInputFailureRollsBackSessionAndWork(t *testing.T) { t.Fatal(err) } t.Cleanup(func() { _, _ = pool.Exec(ctx, "ALTER TABLE turn_inputs DROP CONSTRAINT IF EXISTS "+constraint) }) - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "rollback", InitialInputs: []Input{messageInput("first"), messageInput(marker)}} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "rollback", InitialInputs: []sessions.Input{messageInput("first"), messageInput(marker)}} if got, err := s.CreateSession(ctx, tenant, input); err == nil || got.ID != "" { t.Fatal("partial creation succeeded", got, err) } diff --git a/services/core/internal/store/session_metadata.go b/services/core/internal/store/session_metadata.go index 08e8c63d4..f96a87e53 100644 --- a/services/core/internal/store/session_metadata.go +++ b/services/core/internal/store/session_metadata.go @@ -9,19 +9,20 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/metadata" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) -func (s *Store) UpdateSessionMetadata(ctx context.Context, tenantID, sessionID string, values map[string]string) (Session, error) { +func (s *Store) UpdateSessionMetadata(ctx context.Context, tenantID, sessionID string, values map[string]string) (sessions.Session, error) { tenant, err := parseID(tenantID) if err != nil { - return Session{}, err + return sessions.Session{}, err } id := pgunit.PathID(sessionID) encoded, err := metadata.Encode(values) if err != nil { - return Session{}, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return sessions.Session{}, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } var row sqlc.Session err = s.pooled.Transaction(ctx, func(ctx context.Context, tx pgx.Tx) error { @@ -34,10 +35,10 @@ func (s *Store) UpdateSessionMetadata(ctx context.Context, tenantID, sessionID s return auditpg.RecordWriteAudit(ctx, q, tenantID, "update", "session", uuid.UUID(row.ID.Bytes).String(), "") }) if errors.Is(err, pgx.ErrNoRows) { - return Session{}, ErrNotFound + return sessions.Session{}, sessions.ErrNotFound } if err != nil { - return Session{}, fmt.Errorf("update session metadata: %w", err) + return sessions.Session{}, fmt.Errorf("update session metadata: %w", err) } session, decodeErr := sessionFromRow(row) return s.sessionActivity(ctx, session, decodeErr) diff --git a/services/core/internal/store/session_metadata_test.go b/services/core/internal/store/session_metadata_test.go index 9235ffb66..771ef210d 100644 --- a/services/core/internal/store/session_metadata_test.go +++ b/services/core/internal/store/session_metadata_test.go @@ -17,7 +17,7 @@ func TestSessionMetadataPreservesCreationAndExecutionData(t *testing.T) { s, pool := testStore(t) ctx := context.Background() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "metadata-update", Metadata: map[string]string{"old": "value"}, Configuration: []byte(`{"agent":{"model":"test-model"},"environment":{"type":"none"}}`)} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "metadata-update", Metadata: map[string]string{"old": "value"}, Configuration: []byte(`{"agent":{"model":"test-model"},"environment":{"type":"none"}}`)} first, err := s.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) @@ -48,7 +48,7 @@ func TestSessionMetadataPreservesCreationAndExecutionData(t *testing.T) { } changed := input changed.Metadata = metadata - if _, err := s.CreateSession(ctx, tenant, changed); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(ctx, tenant, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatalf("changed creation request: %v", err) } } @@ -61,12 +61,12 @@ func TestSessionMetadataPreservesCreationAndExecutionData(t *testing.T) { metadata map[string]string want error }{ - {uuid.NewString(), first.ID, nil, ErrNotFound}, - {tenant, uuid.NewString(), nil, ErrNotFound}, - {"invalid", first.ID, nil, ErrInvalidInput}, + {uuid.NewString(), first.ID, nil, sessions.ErrNotFound}, + {tenant, uuid.NewString(), nil, sessions.ErrNotFound}, + {"invalid", first.ID, nil, sessions.ErrInvalidInput}, // A malformed path identifier is indistinguishable from a missing Session. - {tenant, "invalid", nil, ErrNotFound}, - {tenant, first.ID, map[string]string{"large": strings.Repeat("x", 64*1024)}, ErrInvalidInput}, + {tenant, "invalid", nil, sessions.ErrNotFound}, + {tenant, first.ID, map[string]string{"large": strings.Repeat("x", 64*1024)}, sessions.ErrInvalidInput}, } { if _, err := s.UpdateSessionMetadata(ctx, test.tenant, test.session, test.metadata); !errors.Is(err, test.want) { t.Fatalf("rejected update error = %v, want %v", err, test.want) @@ -82,7 +82,7 @@ func TestSessionMetadataConcurrentReplacement(t *testing.T) { s, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() - first, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "concurrent-metadata"}) + first, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "concurrent-metadata"}) if err != nil { t.Fatal(err) } @@ -109,7 +109,7 @@ func TestSessionMetadataPreservesTerminalActivity(t *testing.T) { s, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "terminal-metadata"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "terminal-metadata"}) if err != nil { t.Fatal(err) } @@ -118,10 +118,10 @@ func TestSessionMetadataPreservesTerminalActivity(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: status}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: status}); err != nil { t.Fatal(err) } before, err := s.GetSession(ctx, tenant, session.ID) diff --git a/services/core/internal/store/session_model_execution_test.go b/services/core/internal/store/session_model_execution_test.go index 5d26e43fb..ceaca5a57 100644 --- a/services/core/internal/store/session_model_execution_test.go +++ b/services/core/internal/store/session_model_execution_test.go @@ -8,6 +8,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -20,7 +21,7 @@ func TestSessionModelExecutionEncryptedAndBound(t *testing.T) { st := NewWithCredentialCipher(pool, cipher) ctx := t.Context() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "mcode", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"actual-model"},"environment":{"type":"openai_hosted"}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "anthropic", BaseURL: "https://example.com", APIKey: "private-model-canary", ContextWindow: 100000, MaxOutputTokens: 8000}} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "mcode", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"actual-model"},"environment":{"type":"openai_hosted"}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "anthropic", BaseURL: "https://example.com", APIKey: "private-model-canary", ContextWindow: 100000, MaxOutputTokens: 8000}} session, err := st.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) @@ -37,7 +38,7 @@ func TestSessionModelExecutionEncryptedAndBound(t *testing.T) { t.Fatal("retry changed snapshot", err) } input.ModelProvider.APIKey = "conflicting-key" - if _, err := st.CreateSession(ctx, tenant, input); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := st.CreateSession(ctx, tenant, input); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("changed credentials accepted", err) } restarted := NewWithCredentialCipher(pool, cipher) diff --git a/services/core/internal/store/session_model_options_test.go b/services/core/internal/store/session_model_options_test.go index cb9f6409c..c7e2bc261 100644 --- a/services/core/internal/store/session_model_options_test.go +++ b/services/core/internal/store/session_model_options_test.go @@ -7,6 +7,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -19,7 +20,7 @@ func TestSessionModelExecutionStoresOnlyProviderBundle(t *testing.T) { st := NewWithCredentialCipher(pool, cipher) ctx, tenant := t.Context(), uuid.NewString() provider := &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://example.com/v1", APIKey: "provider-key-canary"} - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: []byte(`{"agent":{"model":"actual-model"},"environment":{"type":"openai_hosted"}}`), ModelProvider: provider} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: []byte(`{"agent":{"model":"actual-model"},"environment":{"type":"openai_hosted"}}`), ModelProvider: provider} session, err := st.CreateSession(ctx, tenant, input) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/session_transaction.go b/services/core/internal/store/session_transaction.go index db8d1d53b..1f14909e2 100644 --- a/services/core/internal/store/session_transaction.go +++ b/services/core/internal/store/session_transaction.go @@ -10,6 +10,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // All Turn admission and lifecycle writes lock the tenant-scoped Session first. @@ -25,7 +26,7 @@ func (s *Store) withPublicSession(ctx context.Context, tenantID, sessionID strin func (s *Store) withSessionState(ctx context.Context, tenantID, sessionID string, public bool, apply func(context.Context, *sqlc.Queries, pgtype.UUID) error) error { return s.withLockedSession(ctx, tenantID, sessionID, public, func(ctx context.Context, q *sqlc.Queries, session sqlc.LockSessionRow) error { if public && session.DeletedAt.Valid { - return ErrNotFound + return sessions.ErrNotFound } return apply(ctx, q, session.ID) }) @@ -49,7 +50,7 @@ func (s *Store) withLockedSession(ctx context.Context, tenantID, sessionID strin q := s.queries.WithTx(tx) session, err := q.LockSession(ctx, sqlc.LockSessionParams{TenantID: tenant, ID: id}) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } else if err != nil { return err } diff --git a/services/core/internal/store/session_write_audit.go b/services/core/internal/store/session_write_audit.go index cf9dde78a..4ab76b688 100644 --- a/services/core/internal/store/session_write_audit.go +++ b/services/core/internal/store/session_write_audit.go @@ -5,6 +5,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) @@ -13,7 +14,7 @@ import ( // It cannot create ownership or admit execution work. func (s *Store) AuditSessionOperation(ctx context.Context, tenantID, sessionID, action string) error { if action != "create" && action != "send_events" { - return ErrInvalidInput + return sessions.ErrInvalidInput } return s.withPublicSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { return auditpg.RecordWriteAudit(ctx, q, tenantID, action, "session", uuid.UUID(session.Bytes).String(), "") diff --git a/services/core/internal/store/session_write_audit_test.go b/services/core/internal/store/session_write_audit_test.go index 78a2f7ab2..505214b98 100644 --- a/services/core/internal/store/session_write_audit_test.go +++ b/services/core/internal/store/session_write_audit_test.go @@ -8,6 +8,7 @@ import ( "sync" "testing" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" "github.com/google/uuid" ) @@ -87,7 +88,7 @@ func TestSessionWriteAuditCreationReplayNoopAndDeletion(t *testing.T) { if err := s.pool.QueryRow(t.Context(), "SELECT jsonb_agg(to_jsonb(o))::text FROM write_audit_operations o WHERE tenant_id=$1", tenant).Scan(&history); err != nil || strings.Contains(history, "not in audit") || strings.Contains(history, "/workspace") { t.Fatal("payload entered audit", err) } - if err := s.AuditSessionOperation(sessionAuditContext(t, tenant, "b"), tenant, created.ID, "send_events"); !errors.Is(err, ErrNotFound) { + if err := s.AuditSessionOperation(sessionAuditContext(t, tenant, "b"), tenant, created.ID, "send_events"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted no-op accepted", err) } sessionAuditCount(t, s, tenant, 7) @@ -142,11 +143,11 @@ func TestSessionWriteAuditRollback(t *testing.T) { case "delete": err = s.DeleteSession(ctx, tenant, created.ID) case "events": - _, err = s.SubmitInputs(ctx, tenant, created.ID, "events", []Input{messageInput("private")}) + _, err = s.SubmitInputs(ctx, tenant, created.ID, "events", []sessions.Input{messageInput("private")}) case "noop": err = s.AuditSessionOperation(ctx, tenant, created.ID, "send_events") case "reserve": - _, err = s.ReserveEnvironmentInput(ctx, tenant, created.ID, "reserve", []Input{messageInput("private")}) + _, err = s.ReserveEnvironmentInput(ctx, tenant, created.ID, "reserve", []sessions.Input{messageInput("private")}) } if err == nil { t.Fatal("mutation bypassed audit failure") @@ -176,10 +177,10 @@ func TestEventsWriteAuditAdmissionAndReplay(t *testing.T) { } submit := func(ctx context.Context) error { if prepared { - _, err := s.ReserveEnvironmentInput(ctx, tenant, created.ID, "batch", []Input{messageInput("private")}) + _, err := s.ReserveEnvironmentInput(ctx, tenant, created.ID, "batch", []sessions.Input{messageInput("private")}) return err } - _, err := s.SubmitInputs(ctx, tenant, created.ID, "batch", []Input{messageInput("private")}) + _, err := s.SubmitInputs(ctx, tenant, created.ID, "batch", []sessions.Input{messageInput("private")}) return err } first := sessionAuditContext(t, tenant, "a") @@ -203,7 +204,7 @@ func TestSessionWriteAuditInitialInputAndHistoricalReplay(t *testing.T) { s, _ := testStore(t) tenant := uuid.NewString() input := environmentInput("audit-initial", kind, "/workspace") - input.InitialInputs = []Input{messageInput("private initial message")} + input.InitialInputs = []sessions.Input{messageInput("private initial message")} created, err := s.CreateSessionStream(sessionAuditContext(t, tenant, "a"), tenant, input) if err != nil || !created.Created { t.Fatal(created, err) diff --git a/services/core/internal/store/sessions.go b/services/core/internal/store/sessions.go index c81dcc9af..ac1a97acb 100644 --- a/services/core/internal/store/sessions.go +++ b/services/core/internal/store/sessions.go @@ -8,9 +8,7 @@ import ( "encoding/json" "errors" "fmt" - "regexp" "strings" - "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -21,67 +19,12 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/metadata" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -var ( - ErrInvalidInput = errors.New("invalid session input") - ErrEnvironmentUnavailable = errors.New("environment is no longer available") - ErrNotFound = errors.New("session not found") - ErrIdempotencyConflict = errors.New("idempotency key was already used with different input") - enginePattern = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`) -) - -// Session is a durable execution context, separate from product conversations -// and from live daemon connections. Engine session IDs will be bound at execution. -type Session struct { - ID string - TenantID string - Creator *identity.Subject - Engine string - Metadata map[string]string - CreatedAt time.Time - Configuration json.RawMessage - LastTurn *sessions.Turn - Usage json.RawMessage - RequiredActions []v1.FunctionCallAction - Environment *Environment - EnvironmentInputActivity *sessions.EnvironmentInputActivity - // EnvironmentFailure is the recorded provisioning failure of a failed hosted - // Environment. It makes the Session failed and is terminal. - EnvironmentFailure *sessions.EnvironmentFailure - // PendingInput reports that the latest input reservation, read once no Turn - // is active or newer, can still start a Turn. It only supports settlement - // checks and is never rendered. - PendingInput bool -} - -type CreateSessionInput struct { - // DeploymentProviderRevision is private creation metadata, never retry identity. - DeploymentProviderRevision uuid.UUID `json:"-"` - ExecutionConfiguration *v1.SessionExecutionConfiguration - ModelProvider *v1.ModelProviderInput - ModelProviderSource string // session, agent or deployment; empty allows only openai_hosted - Initialization environmentconfig.Setup - InitialFiles []environmentconfig.InitialFile - Creator identity.Subject - CreationRequest json.RawMessage - Engine string - Metadata map[string]string - IdempotencyKey string - Configuration json.RawMessage - InitialInputs []Input -} - -type SessionPage struct { - Sessions []Session - NextCursor string -} - type Store struct { queries *sqlc.Queries // pool is the database the Store was built on; pooled runs every @@ -104,47 +47,45 @@ func New(pool *pgxpool.Pool) *Store { return &Store{queries: sqlc.New(pool), pool: pool, pooled: pooled, writer: pooled} } -func ValidEngine(engine string) bool { return enginePattern.MatchString(engine) } - // CreateSession uses a project-scoped key to make retries safe, including // concurrent submissions. Different input or creator with the same key conflicts. -func (s *Store) CreateSession(ctx context.Context, tenantID string, input CreateSessionInput) (Session, error) { +func (s *Store) CreateSession(ctx context.Context, tenantID string, input sessions.CreateSession) (sessions.Session, error) { result, err := s.createSession(ctx, tenantID, input) return s.sessionActivity(ctx, result.Session, err) } -func (s *Store) createSession(ctx context.Context, tenantID string, input CreateSessionInput) (SessionCreation, error) { +func (s *Store) createSession(ctx context.Context, tenantID string, input sessions.CreateSession) (sessions.Creation, error) { if err := input.Creator.Validate(); err != nil { - return SessionCreation{}, fmt.Errorf("%w: %v", ErrInvalidInput, err) + return sessions.Creation{}, fmt.Errorf("%w: %v", sessions.ErrInvalidInput, err) } tenant, err := parseID(tenantID) if err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } input.Engine = strings.TrimSpace(input.Engine) - if !ValidEngine(input.Engine) || strings.TrimSpace(input.IdempotencyKey) == "" || len(input.IdempotencyKey) > 128 { - return SessionCreation{}, fmt.Errorf("%w: engine and idempotency key are required", ErrInvalidInput) + if !sessions.ValidEngine(input.Engine) || strings.TrimSpace(input.IdempotencyKey) == "" || len(input.IdempotencyKey) > 128 { + return sessions.Creation{}, fmt.Errorf("%w: engine and idempotency key are required", sessions.ErrInvalidInput) } if input.Metadata == nil { input.Metadata = map[string]string{} } encodedMetadata, err := metadata.Encode(input.Metadata) if err != nil { - return SessionCreation{}, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return sessions.Creation{}, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } if len(input.Configuration) > 512*1024 { - return SessionCreation{}, fmt.Errorf("%w: configuration exceeds 512 KiB", ErrInvalidInput) + return sessions.Creation{}, fmt.Errorf("%w: configuration exceeds 512 KiB", sessions.ErrInvalidInput) } configuration, err := jsonobject.Normalize(input.Configuration) if err != nil { - return SessionCreation{}, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return sessions.Creation{}, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } - var batch []Input + var batch []sessions.Input var encodedInput json.RawMessage if len(input.InitialInputs) > 0 { batch, encodedInput, err = validateInitialInputs(input.InitialInputs) if err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } } // The retry identity covers the configuration as requested; the marker added @@ -152,21 +93,21 @@ func (s *Store) createSession(ctx context.Context, tenantID string, input Create requested := configuration if input.ModelProvider != nil { if err := input.ModelProvider.ValidateHarness(input.Engine); err != nil { - return SessionCreation{}, fmt.Errorf("%w: %s", ErrInvalidInput, err) + return sessions.Creation{}, fmt.Errorf("%w: %s", sessions.ErrInvalidInput, err) } var fields map[string]any if json.Unmarshal(configuration, &fields) != nil { - return SessionCreation{}, ErrInvalidInput + return sessions.Creation{}, sessions.ErrInvalidInput } environment, _ := fields["environment"].(map[string]any) environmentType, _ := environment["type"].(string) if !v1.ModelProviderAllowed(environmentType, input.ModelProviderSource) { - return SessionCreation{}, fmt.Errorf("%w: this model provider source is not supported for the Session environment", ErrInvalidInput) + return sessions.Creation{}, fmt.Errorf("%w: this model provider source is not supported for the Session environment", sessions.ErrInvalidInput) } fields["model_provider_configured"] = true configuration, err = json.Marshal(fields) if err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } } var initialization *environmentconfig.Setup @@ -182,7 +123,7 @@ func (s *Store) createSession(ctx context.Context, tenantID string, input Create if input.ModelProviderSource == v1.ModelProviderSourceDeployment { hashed = requested } else if fingerprinted, err = s.fingerprintedProvider(input.ModelProvider); err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } // JSON map keys are sorted by encoding/json, so key order does not affect retries. canonical, err := json.Marshal(struct { @@ -195,11 +136,11 @@ func (s *Store) createSession(ctx context.Context, tenantID string, input Create Initialization *environmentconfig.Setup `json:",omitempty"` }{fingerprinted, input.Engine, input.Metadata, hashed, encodedInput, input.InitialFiles, initialization}) if err != nil { - return SessionCreation{}, fmt.Errorf("%w: input: %v", ErrInvalidInput, err) + return sessions.Creation{}, fmt.Errorf("%w: input: %v", sessions.ErrInvalidInput, err) } creationHash, err := s.creationRequestHash(input.CreationRequest) if err != nil { - return SessionCreation{}, err + return sessions.Creation{}, err } hash := sha256.Sum256(canonical) params := sqlc.CreateSessionParams{ @@ -210,29 +151,29 @@ func (s *Store) createSession(ctx context.Context, tenantID string, input Create } row, environment, err := s.createSessionResources(ctx, tenantID, params, batch, encodedInput, input.InitialFiles, input.Initialization, input.ModelProvider, input.ExecutionConfiguration, input.ModelProviderSource, input.DeploymentProviderRevision) if errors.Is(err, pgx.ErrNoRows) { - return SessionCreation{}, ErrIdempotencyConflict + return sessions.Creation{}, sessions.ErrIdempotencyConflict } if err != nil { - return SessionCreation{}, fmt.Errorf("create session: %w", err) + return sessions.Creation{}, fmt.Errorf("create session: %w", err) } session, err := sessionFromRow(row) session.Environment = environment - return SessionCreation{Session: session, Created: row.ID == params.ID, Cursor: row.EventSequence}, err + return sessions.Creation{Session: session, Created: row.ID == params.ID, Cursor: row.EventSequence}, err } // GetSession always scopes lookup to the authenticated caller's tenant. -func (s *Store) GetSession(ctx context.Context, tenantID, sessionID string) (Session, error) { +func (s *Store) GetSession(ctx context.Context, tenantID, sessionID string) (sessions.Session, error) { tenant, err := parseID(tenantID) if err != nil { - return Session{}, err + return sessions.Session{}, err } id := pgunit.PathID(sessionID) row, err := s.queries.GetSession(ctx, sqlc.GetSessionParams{TenantID: tenant, ID: id}) if errors.Is(err, pgx.ErrNoRows) { - return Session{}, ErrNotFound + return sessions.Session{}, sessions.ErrNotFound } if err != nil { - return Session{}, fmt.Errorf("get session: %w", err) + return sessions.Session{}, fmt.Errorf("get session: %w", err) } session, decodeErr := sessionFromRow(row) return s.sessionActivity(ctx, session, decodeErr) @@ -240,13 +181,13 @@ func (s *Store) GetSession(ctx context.Context, tenantID, sessionID string) (Ses // ListSessions orders by creation time and ID. The cursor is the last returned // session ID and must belong to the same tenant; it grants no additional access. -func (s *Store) ListSessions(ctx context.Context, tenantID, cursor string, limit int, ascending bool, agentID *string) (SessionPage, error) { +func (s *Store) ListSessions(ctx context.Context, tenantID, cursor string, limit int, ascending bool, agentID *string) (sessions.Page, error) { tenant, err := parseID(tenantID) if err != nil { - return SessionPage{}, err + return sessions.Page{}, err } if limit < 1 || limit > 100 { - return SessionPage{}, fmt.Errorf("%w: page size must be 1..100", ErrInvalidInput) + return sessions.Page{}, fmt.Errorf("%w: page size must be 1..100", sessions.ErrInvalidInput) } params := sqlc.ListSessionsParams{TenantID: tenant, PageLimit: int32(limit + 1), AfterID: pgtype.UUID{Valid: true}, Ascending: ascending} if agentID != nil { @@ -255,16 +196,16 @@ func (s *Store) ListSessions(ctx context.Context, tenantID, cursor string, limit if cursor != "" { after, err := s.GetSession(ctx, tenantID, pgunit.LookupCursor(cursor)) if err != nil { - return SessionPage{}, err + return sessions.Page{}, err } params.AfterCreated = pgtype.Timestamptz{Time: after.CreatedAt, Valid: true} params.AfterID, _ = parseID(after.ID) } rows, err := s.queries.ListSessions(ctx, params) if err != nil { - return SessionPage{}, fmt.Errorf("list sessions: %w", err) + return sessions.Page{}, fmt.Errorf("list sessions: %w", err) } - page := SessionPage{Sessions: make([]Session, 0, min(limit, len(rows)))} + page := sessions.Page{Sessions: make([]sessions.Session, 0, min(limit, len(rows)))} if len(rows) > limit { page.NextCursor = uuid.UUID(rows[limit-1].ID.Bytes).String() rows = rows[:limit] @@ -273,7 +214,7 @@ func (s *Store) ListSessions(ctx context.Context, tenantID, cursor string, limit session, err := sessionFromRow(row) session, err = s.sessionActivity(ctx, session, err) if err != nil { - return SessionPage{}, err + return sessions.Page{}, err } page.Sessions = append(page.Sessions, session) } @@ -286,25 +227,25 @@ func (s *Store) ListSessions(ctx context.Context, tenantID, cursor string, limit func parseID(value string) (pgtype.UUID, error) { id, err := pgunit.ParseID(value) if err != nil { - return pgtype.UUID{}, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return pgtype.UUID{}, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } return id, nil } -func sessionFromRow(row sqlc.Session) (Session, error) { - session := Session{ID: uuid.UUID(row.ID.Bytes).String(), TenantID: uuid.UUID(row.TenantID.Bytes).String(), Engine: row.Engine, CreatedAt: row.CreatedAt.Time, RequiredActions: []v1.FunctionCallAction{}} +func sessionFromRow(row sqlc.Session) (sessions.Session, error) { + session := sessions.Session{ID: uuid.UUID(row.ID.Bytes).String(), TenantID: uuid.UUID(row.TenantID.Bytes).String(), Engine: row.Engine, CreatedAt: row.CreatedAt.Time, RequiredActions: []v1.FunctionCallAction{}} creator, err := sessionCreator(row.CreatorKind, row.CreatorID) if err != nil { - return Session{}, err + return sessions.Session{}, err } session.Creator = creator configuration, err := jsonobject.Normalize(row.Configuration) if err != nil { - return Session{}, fmt.Errorf("decode session configuration: %w: %w", ErrInvalidInput, err) + return sessions.Session{}, fmt.Errorf("decode session configuration: %w: %w", sessions.ErrInvalidInput, err) } session.Configuration = configuration if err := json.Unmarshal(row.Metadata, &session.Metadata); err != nil { - return Session{}, fmt.Errorf("decode session metadata: %w", err) + return sessions.Session{}, fmt.Errorf("decode session metadata: %w", err) } return session, nil } diff --git a/services/core/internal/store/sessions_test.go b/services/core/internal/store/sessions_test.go index 6dd3ca83b..7ae996b03 100644 --- a/services/core/internal/store/sessions_test.go +++ b/services/core/internal/store/sessions_test.go @@ -11,6 +11,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func testStore(t *testing.T) (*Store, *pgxpool.Pool) { @@ -23,7 +24,7 @@ func TestSessionsPersistAndStayTenantScoped(t *testing.T) { s, pool := testStore(t) ctx := context.Background() tenantA, tenantB := uuid.NewString(), uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", Metadata: map[string]string{"source": "standalone"}, IdempotencyKey: "first", + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", Metadata: map[string]string{"source": "standalone"}, IdempotencyKey: "first", Configuration: []byte(`{"agent":{"model":"test-model","instructions":"Keep the snapshot."},"environment":{"type":"none"}}`)} first, err := s.CreateSession(ctx, tenantA, input) if err != nil { @@ -36,10 +37,10 @@ func TestSessionsPersistAndStayTenantScoped(t *testing.T) { if first.ID == other.ID { t.Fatal("idempotency leaked across tenants") } - if _, err := s.GetSession(ctx, tenantB, first.ID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetSession(ctx, tenantB, first.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("cross-tenant read: %v", err) } - if _, err := s.ListSessions(ctx, tenantB, first.ID, 10, false, nil); !errors.Is(err, ErrNotFound) { + if _, err := s.ListSessions(ctx, tenantB, first.ID, 10, false, nil); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("cross-tenant cursor: %v", err) } for _, key := range []string{"second", "third"} { @@ -89,7 +90,7 @@ func TestConcurrentSessionCreationIsIdempotent(t *testing.T) { s, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "fake_alpha", Metadata: map[string]string{"b": "2", "a": "1"}, IdempotencyKey: "repeated"} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "fake_alpha", Metadata: map[string]string{"b": "2", "a": "1"}, IdempotencyKey: "repeated"} const count = 8 ids := make(chan string, count) errs := make(chan error, count) @@ -118,15 +119,15 @@ func TestConcurrentSessionCreationIsIdempotent(t *testing.T) { if len(unique) != 1 { t.Fatalf("duplicate sessions: %+v", unique) } - replay, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "fake_alpha", Metadata: map[string]string{"a": "1", "b": "2"}, IdempotencyKey: "repeated"}) + replay, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "fake_alpha", Metadata: map[string]string{"a": "1", "b": "2"}, IdempotencyKey: "repeated"}) if err != nil || !unique[replay.ID] { t.Fatalf("reordered metadata was not replayed: %+v %v", replay, err) } - for _, changed := range []CreateSessionInput{ + for _, changed := range []sessions.CreateSession{ {Creator: FixtureCreator(), Engine: "codex", Metadata: input.Metadata, IdempotencyKey: input.IdempotencyKey}, {Creator: FixtureCreator(), Engine: input.Engine, Metadata: map[string]string{"a": "changed"}, IdempotencyKey: input.IdempotencyKey}, } { - if _, err := s.CreateSession(ctx, tenant, changed); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.CreateSession(ctx, tenant, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatalf("changed request = %v", err) } } diff --git a/services/core/internal/store/skill_version_deletion_test.go b/services/core/internal/store/skill_version_deletion_test.go index 948b458d6..b609dfe8c 100644 --- a/services/core/internal/store/skill_version_deletion_test.go +++ b/services/core/internal/store/skill_version_deletion_test.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/google/uuid" ) @@ -29,7 +30,7 @@ func TestSoleSkillVersionDeletionKeepsFrozenSetup(t *testing.T) { t.Fatal(err) } reference := environmentconfig.Setup{Skills: []environmentconfig.Skill{{Metadata: environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: skill.ID}}}} - input := CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: reference} + input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"environment":{"type":"openai_hosted"}}`), Initialization: reference} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/stream_authority_http_test.go b/services/core/internal/store/stream_authority_http_test.go index 842166d79..a03a3fc5c 100644 --- a/services/core/internal/store/stream_authority_http_test.go +++ b/services/core/internal/store/stream_authority_http_test.go @@ -11,6 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -38,7 +39,7 @@ func TestLiveStreamClosesAfterKeyRevocationOrProjectArchive(t *testing.T) { if err != nil { t.Fatal(err) } - session, err := s.CreateSession(t.Context(), project.TenantID, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_fixture","model":"fixture","tools":[]},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) + session, err := s.CreateSession(t.Context(), project.TenantID, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_fixture","model":"fixture","tools":[]},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) if err != nil { t.Fatal(err) } @@ -100,7 +101,7 @@ func TestLiveStreamClosesAfterKeyRevocationOrProjectArchive(t *testing.T) { t.Fatal("revocation affected peer", valid.StatusCode) } // New events remain available to valid callers after the reader has closed. - if _, err := s.ReserveEnvironmentInput(t.Context(), project.TenantID, session.ID, "after-revocation", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"new event"}`)}}); err != nil { + if _, err := s.ReserveEnvironmentInput(t.Context(), project.TenantID, session.ID, "after-revocation", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"new event"}`)}}); err != nil { t.Fatal(err) } } diff --git a/services/core/internal/store/structured_output_dispatch_test.go b/services/core/internal/store/structured_output_dispatch_test.go index 6a8362fde..918f3ad4e 100644 --- a/services/core/internal/store/structured_output_dispatch_test.go +++ b/services/core/internal/store/structured_output_dispatch_test.go @@ -24,7 +24,7 @@ func TestStructuredOutputDispatchRechecksOperationQualification(t *testing.T) { t.Fatal(err) } var err error - h.session, err = h.s.CreateSession(t.Context(), h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "fixture_harness", IdempotencyKey: "structured", Configuration: configuration}) + h.session, err = h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "fixture_harness", IdempotencyKey: "structured", Configuration: configuration}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/subagent_coordination.go b/services/core/internal/store/subagent_coordination.go index 6edd9fd9e..e153f4cc1 100644 --- a/services/core/internal/store/subagent_coordination.go +++ b/services/core/internal/store/subagent_coordination.go @@ -19,10 +19,10 @@ import ( func coordinationItem(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, turn string, p proto.SubagentCoordinationPayload) (v1.Item, error) { value := v1.Item{ID: items.Identity(turn, "coordination:"+p.ID), TurnID: turn, Type: p.Kind, Status: p.Status, Model: p.Model, ReasoningEffort: p.ReasoningEffort} if !validNativeIdentity(p.ID) { - return value, ErrInvalidInput + return value, sessions.ErrInvalidInput } if p.Status != "in_progress" && p.Status != "completed" && p.Status != "failed" && p.Status != "incomplete" && p.Kind != "agent_message" { - return value, ErrInvalidInput + return value, sessions.ErrInvalidInput } resolve := func(native string, failedReference bool) (string, error) { if native == "" { @@ -36,7 +36,7 @@ func coordinationItem(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, return "", err } if !row.PublicVisible { - return "", ErrNotFound + return "", sessions.ErrNotFound } return uuid.UUID(row.ID.Bytes).String(), nil } @@ -45,7 +45,7 @@ func coordinationItem(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, return value, err } if actor == "" { - return value, ErrInvalidInput + return value, sessions.ErrInvalidInput } value.SenderAgentID = actor if p.Text != nil { @@ -65,21 +65,21 @@ func coordinationItem(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, } case "send_subagent_input_call", "resume_subagent_call", "interrupt_subagent_call", "close_subagent_call", "agent_message": if len(p.Recipients) != 1 { - return value, ErrInvalidInput + return value, sessions.ErrInvalidInput } value.RecipientAgentID, err = resolve(p.Recipients[0], p.Status == "failed") if err != nil { return value, err } default: - return value, ErrInvalidInput + return value, sessions.ErrInvalidInput } return value, nil } func projectRootCoordination(ctx context.Context, q *sqlc.Queries, session, turn pgtype.UUID, raw json.RawMessage, created pgtype.Timestamptz) error { var p proto.SubagentCoordinationPayload if json.Unmarshal(raw, &p) != nil || p.ActorID != "" { - return ErrInvalidInput + return sessions.ErrInvalidInput } value, err := coordinationItem(ctx, q, session, uuid.UUID(turn.Bytes).String(), p) if err != nil { diff --git a/services/core/internal/store/subagent_dispatch_test.go b/services/core/internal/store/subagent_dispatch_test.go index 87c8018b6..4cedb3f34 100644 --- a/services/core/internal/store/subagent_dispatch_test.go +++ b/services/core/internal/store/subagent_dispatch_test.go @@ -20,7 +20,7 @@ func TestSubagentIdentityUsesLeasedDispatchJournal(t *testing.T) { "environment": map[string]string{"type": "none"}, }) var err error - h.session, err = h.s.CreateSession(ctx, h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "identity-dispatch", Configuration: configuration}) + h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "identity-dispatch", Configuration: configuration}) if err != nil { t.Fatal(err) } @@ -38,7 +38,7 @@ func TestSubagentIdentityUsesLeasedDispatchJournal(t *testing.T) { h.write(input.TurnID, proto.TypeSubagentIdentity, identity) if !enabled { h.finished(running, sessions.TurnFailed) - if _, err = h.s.GetSubagentIdentity(ctx, h.tenant, h.session.ID, "child"); !errors.Is(err, store.ErrNotFound) { + if _, err = h.s.GetSubagentIdentity(ctx, h.tenant, h.session.ID, "child"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("unsolicited identity committed", err) } return diff --git a/services/core/internal/store/subagent_identities.go b/services/core/internal/store/subagent_identities.go index ed7d125ca..e234046f4 100644 --- a/services/core/internal/store/subagent_identities.go +++ b/services/core/internal/store/subagent_identities.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -26,11 +27,11 @@ type SubagentIdentity struct { func projectSubagentIdentity(ctx context.Context, q *sqlc.Queries, session, turn pgtype.UUID, ordinal int32, raw json.RawMessage) error { var identity proto.SubagentIdentityPayload if json.Unmarshal(raw, &identity) != nil || identity.NativeCreatedAt <= 0 || identity.NativeID == identity.ParentNativeID { - return ErrInvalidInput + return sessions.ErrInvalidInput } for _, value := range []string{identity.NativeID, identity.ParentNativeID, identity.ParentTurnID, identity.SourceItemID} { if value == "" || len(value) > 512 || strings.TrimSpace(value) != value || strings.ContainsAny(value, "\x00\r\n") { - return ErrInvalidInput + return sessions.ErrInvalidInput } } id, err := q.PutSubagentIdentity(ctx, sqlc.PutSubagentIdentityParams{ @@ -39,7 +40,7 @@ func projectSubagentIdentity(ctx context.Context, q *sqlc.Queries, session, turn NativeCreatedAt: identity.NativeCreatedAt, FirstTurnID: turn, FirstEventOrdinal: ordinal, }) if errors.Is(err, pgx.ErrNoRows) { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } if err != nil { return err @@ -55,7 +56,7 @@ func (s *Store) GetSubagentIdentity(ctx context.Context, tenantID, sessionID, na } row, err := s.queries.GetSubagentIdentity(ctx, sqlc.GetSubagentIdentityParams{TenantID: p.TenantID, SessionID: p.ID, NativeID: nativeID}) if errors.Is(err, pgx.ErrNoRows) { - return SubagentIdentity{}, ErrNotFound + return SubagentIdentity{}, sessions.ErrNotFound } if err != nil { return SubagentIdentity{}, err diff --git a/services/core/internal/store/subagent_identities_test.go b/services/core/internal/store/subagent_identities_test.go index 26277bbfb..988f36961 100644 --- a/services/core/internal/store/subagent_identities_test.go +++ b/services/core/internal/store/subagent_identities_test.go @@ -14,10 +14,10 @@ import ( "github.com/google/uuid" ) -func subagentIdentityEvent(child, parent string, created int64) ExecutionEvent { +func subagentIdentityEvent(child, parent string, created int64) sessions.ExecutionEvent { raw, _ := json.Marshal(proto.SubagentIdentityPayload{NativeID: child, ParentNativeID: parent, NativeCreatedAt: created, ParentTurnID: "native-turn", SourceItemID: "native-spawn-item"}) - return ExecutionEvent{Kind: proto.TypeSubagentIdentity, Payload: raw} + return sessions.ExecutionEvent{Kind: proto.TypeSubagentIdentity, Payload: raw} } func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { @@ -35,7 +35,7 @@ func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { input := submitMessage(t, s, tenant, session.ID, "first") transition(t, w, tenant, session.ID, input.TurnID, sessions.TurnQueued, sessions.TurnInProgress) a, b := subagentIdentityEvent("child-a", "root", 102), subagentIdentityEvent("child-b", "root", 101) - batch := []ExecutionEvent{a, b, a} + batch := []sessions.ExecutionEvent{a, b, a} if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 1, batch); !errors.Is(err, ErrExecutionAuthority) { t.Fatal("unleased discovery accepted", err) } @@ -56,10 +56,10 @@ func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { t.Fatal("discovery order replaced identity or creation", other, err) } for _, owner := range []struct{ tenant, session string }{{uuid.NewString(), session.ID}, {tenant, uuid.NewString()}} { - if _, err = s.GetSubagentIdentity(ctx, owner.tenant, owner.session, "child-a"); !errors.Is(err, ErrNotFound) { + if _, err = s.GetSubagentIdentity(ctx, owner.tenant, owner.session, "child-a"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign read", err) } - if err = w.AppendTurnEvents(ctx, owner.tenant, owner.session, input.TurnID, 4, []ExecutionEvent{a}); !errors.Is(err, ErrNotFound) { + if err = w.AppendTurnEvents(ctx, owner.tenant, owner.session, input.TurnID, 4, []sessions.ExecutionEvent{a}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign write", err) } } @@ -67,18 +67,18 @@ func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { if err != nil { t.Fatal(err) } - for _, conflict := range []ExecutionEvent{ + for _, conflict := range []sessions.ExecutionEvent{ subagentIdentityEvent("child-a", "other-root", 102), subagentIdentityEvent("child-a", "root", 103), subagentIdentityEvent("child-new", "other-root", 104), } { // A preceding new identity and public output must roll back with the conflict. - bad := []ExecutionEvent{subagentIdentityEvent("rollback-child", "root", 105), + bad := []sessions.ExecutionEvent{subagentIdentityEvent("rollback-child", "root", 105), {Kind: proto.TypeDelta, Payload: json.RawMessage(`{"delta":"must roll back","sequence":1}`)}, conflict} - if err = w.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, bad); !errors.Is(err, ErrIdempotencyConflict) { + if err = w.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, bad); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("conflicting facts accepted", err) } - if _, err = s.GetSubagentIdentity(ctx, tenant, session.ID, "rollback-child"); !errors.Is(err, ErrNotFound) { + if _, err = s.GetSubagentIdentity(ctx, tenant, session.ID, "rollback-child"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("partial identity survived", err) } events, err := s.ListTurnEvents(ctx, tenant, session.ID, input.TurnID, 0, 100) @@ -90,7 +90,7 @@ func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { t.Fatal("partial public projection survived", cursor, err) } } - foreign, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "foreign"}) + foreign, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "foreign"}) if err != nil { t.Fatal(err) } @@ -99,13 +99,13 @@ func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { } foreignInput := submitMessage(t, s, tenant, foreign.ID, "first") transition(t, w, tenant, foreign.ID, foreignInput.TurnID, sessions.TurnQueued, sessions.TurnInProgress) - if err = w.AppendTurnEvents(ctx, tenant, foreign.ID, foreignInput.TurnID, 1, []ExecutionEvent{a}); !errors.Is(err, ErrIdempotencyConflict) { + if err = w.AppendTurnEvents(ctx, tenant, foreign.ID, foreignInput.TurnID, 1, []sessions.ExecutionEvent{a}); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("same device/native child reassigned to another Session", err) } if _, err = pool.Exec(ctx, "UPDATE session_devices SET native_session_id='known-root' WHERE session_id=$1", foreign.ID); err != nil { t.Fatal(err) } - if err = w.AppendTurnEvents(ctx, tenant, foreign.ID, foreignInput.TurnID, 1, []ExecutionEvent{subagentIdentityEvent("other-child", "root", 101)}); !errors.Is(err, ErrIdempotencyConflict) { + if err = w.AppendTurnEvents(ctx, tenant, foreign.ID, foreignInput.TurnID, 1, []sessions.ExecutionEvent{subagentIdentityEvent("other-child", "root", 101)}); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal("known root binding ignored", err) } if _, err = w.CompleteExecution(ctx, tenant, session.ID, input.TurnID, sessions.TurnCompleted, json.RawMessage(`{}`), "root", input.Sequence); err != nil { @@ -124,25 +124,25 @@ func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { } second := submitMessage(t, reopened, tenant, session.ID, "second") transition(t, nextOwner, tenant, session.ID, second.TurnID, sessions.TurnQueued, sessions.TurnInProgress) - if err = w.AppendTurnEvents(ctx, tenant, session.ID, second.TurnID, 1, []ExecutionEvent{a}); err == nil { + if err = w.AppendTurnEvents(ctx, tenant, session.ID, second.TurnID, 1, []sessions.ExecutionEvent{a}); err == nil { t.Fatal("closed owner wrote identity") } continued := proto.SubagentIdentityPayload{NativeID: "child-a", ParentNativeID: "root", NativeCreatedAt: 102, ParentTurnID: "later-native-turn", SourceItemID: "resume-item"} raw, _ := json.Marshal(continued) - if err = nextOwner.AppendTurnEvents(ctx, tenant, session.ID, second.TurnID, 1, []ExecutionEvent{{Kind: proto.TypeSubagentIdentity, Payload: raw}}); err != nil { + if err = nextOwner.AppendTurnEvents(ctx, tenant, session.ID, second.TurnID, 1, []sessions.ExecutionEvent{{Kind: proto.TypeSubagentIdentity, Payload: raw}}); err != nil { t.Fatal(err) } again, err = reopened.GetSubagentIdentity(ctx, tenant, session.ID, "child-a") if err != nil || !reflect.DeepEqual(again, saved) { t.Fatal("continuation changed immutable first observation", again, err) } - if err = reopened.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, ErrSessionNotIdle) { + if err = reopened.DeleteSession(ctx, tenant, session.ID); !errors.Is(err, sessions.ErrNotIdle) { t.Fatal("running Session deleted", err) } if err = reopened.commitLegacyDeletion(ctx, tenant, session.ID); err != nil { t.Fatal(err) } - if _, err = reopened.GetSubagentIdentity(ctx, tenant, session.ID, "child-a"); !errors.Is(err, ErrNotFound) { + if _, err = reopened.GetSubagentIdentity(ctx, tenant, session.ID, "child-a"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("deleted Session exposed identity", err) } } @@ -158,7 +158,7 @@ func TestSubagentIdentityRejectsLostLease(t *testing.T) { t.Fatal(killed, err) } successor := executionWriter(t, s) - if err := old.AppendTurnEvents(t.Context(), tenant, session.ID, input.TurnID, 1, []ExecutionEvent{subagentIdentityEvent("child", "root", 100)}); err == nil { + if err := old.AppendTurnEvents(t.Context(), tenant, session.ID, input.TurnID, 1, []sessions.ExecutionEvent{subagentIdentityEvent("child", "root", 100)}); err == nil { t.Fatal("lost owner committed identity") } if err := successor.lease.CheckOwnership(context.Background()); err != nil { diff --git a/services/core/internal/store/subagent_item_projection.go b/services/core/internal/store/subagent_item_projection.go index 643830866..f486860ea 100644 --- a/services/core/internal/store/subagent_item_projection.go +++ b/services/core/internal/store/subagent_item_projection.go @@ -25,14 +25,14 @@ func isSubagentObservation(kind string) bool { func projectSubagentItem(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, raw json.RawMessage) error { var p proto.SubagentItemPayload if json.Unmarshal(raw, &p) != nil || !validNativeIdentity(p.NativeID) || !validNativeIdentity(p.TurnID) || !validNativeIdentity(p.ItemID) || p.Position < 0 || p.Position > 1073741823 { - return ErrInvalidInput + return sessions.ErrInvalidInput } child, err := q.GetNativeSubagent(ctx, sqlc.GetNativeSubagentParams{SessionID: session, NativeID: p.NativeID}) if err != nil { return err } if !child.PublicVisible { - return ErrInvalidInput + return sessions.ErrInvalidInput } turnID := items.Identity(uuid.UUID(child.ID.Bytes).String(), "turn:"+p.TurnID) turn, err := childTurn(ctx, q, session, uuid.UUID(child.ID.Bytes).String(), turnID) @@ -65,7 +65,7 @@ func putChildItem(ctx context.Context, q *sqlc.Queries, session, childID pgtype. var previous v1.Item if !fresh { if old.TurnID != turn.ID || old.Position != position { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } if err = json.Unmarshal(old.Payload, &previous); err != nil { return err @@ -74,11 +74,11 @@ func putChildItem(ctx context.Context, q *sqlc.Queries, session, childID pgtype. return nil } if previous.Status != "in_progress" { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } } if sessions.TerminalStatus(turn.Status) { - return ErrTurnConflict + return sessions.ErrTurnConflict } // Child Items publish no Session events: the Session stream carries root work, // and child history is read through the Subagent routes. The stored output @@ -93,19 +93,19 @@ func childItems(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, turn case proto.TypeSubagentCoordination: var value proto.SubagentCoordinationPayload if json.Unmarshal(p.Payload, &value) != nil || value.ID != p.ItemID || value.ActorID != p.NativeID { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } item, err := coordinationItem(ctx, q, session, turn, value) return []v1.Item{item}, err case proto.TypeOutputMessage: var message proto.OutputMessagePayload if json.Unmarshal(p.Payload, &message) != nil || message.ID != p.ItemID || message.Text == nil { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } case proto.TypeToolCall: var call proto.ToolCallPayload if json.Unmarshal(p.Payload, &call) != nil || call.ID != p.ItemID || call.Observation == nil { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } case "message": // Input projection validates and normalizes the existing public message shape. @@ -115,30 +115,30 @@ func childItems(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, turn Summary []v1.SummaryText `json:"summary"` } if json.Unmarshal(p.Payload, &value) != nil { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } if value.Status != "" && value.Status != "in_progress" && value.Status != "completed" && value.Status != "incomplete" { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } for _, part := range value.Summary { if part.Type != "summary_text" { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } } return []v1.Item{{ID: items.Identity(turn, "reasoning:"+p.ItemID), TurnID: turn, Type: "reasoning", Status: value.Status, Summary: value.Summary}}, nil default: - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } updates, err := items.Project(turn, p.Kind, int64(p.Position), p.Payload) if err != nil { return result, err } if len(updates) < 1 || len(updates) > 2 { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } for _, update := range updates { if update.AppendText { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } result = append(result, update.Item) } diff --git a/services/core/internal/store/subagent_item_reads.go b/services/core/internal/store/subagent_item_reads.go index 80542544f..ad447755d 100644 --- a/services/core/internal/store/subagent_item_reads.go +++ b/services/core/internal/store/subagent_item_reads.go @@ -8,6 +8,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -17,14 +18,14 @@ func (s *Store) ListSubagentItems(ctx context.Context, tenant, session, child, a } func (s *Store) ListSubagentTurnItems(ctx context.Context, tenant, session, child, turn, after string, limit int, asc bool) (v1.ItemList, error) { if turn == "" { - return v1.ItemList{}, ErrInvalidInput + return v1.ItemList{}, sessions.ErrInvalidInput } return s.listChildItems(ctx, tenant, session, child, turn, after, limit, asc) } func (s *Store) listChildItems(ctx context.Context, tenant, session, child, turn, after string, limit int, asc bool) (v1.ItemList, error) { result := v1.ItemList{Data: []v1.Item{}} if limit < 1 || limit > 100 { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } err := s.withPublicSession(ctx, tenant, session, func(ctx context.Context, q *sqlc.Queries, sid pgtype.UUID) error { if _, err := publicSubagent(ctx, q, sid, child); err != nil { @@ -44,7 +45,7 @@ func (s *Store) listChildItems(ctx context.Context, tenant, session, child, turn // malformed one, uses the Session Item cursor error. row, err := q.GetChildItem(ctx, sqlc.GetChildItemParams{SessionID: sid, SubagentID: childID, ID: pgunit.PathID(after)}) if errors.Is(err, pgx.ErrNoRows) || (err == nil && p.TurnID.Valid && p.TurnID != row.TurnID) { - return errItemCursor + return sessions.ErrItemCursor } if err != nil { return err diff --git a/services/core/internal/store/subagent_lifecycle.go b/services/core/internal/store/subagent_lifecycle.go index 87bd2fa72..5cc94074c 100644 --- a/services/core/internal/store/subagent_lifecycle.go +++ b/services/core/internal/store/subagent_lifecycle.go @@ -50,24 +50,24 @@ func publishSubagent(ctx context.Context, q *sqlc.Queries, session, id pgtype.UU func projectSubagentLifecycle(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, raw json.RawMessage) error { var p proto.SubagentLifecyclePayload if json.Unmarshal(raw, &p) != nil || !validNativeIdentity(p.NativeID) || !validNativeIdentity(p.EffectID) || p.OccurredAtMS <= 0 || (p.Status != "active" && p.Status != "closed") { - return ErrInvalidInput + return sessions.ErrInvalidInput } child, err := q.GetNativeSubagent(ctx, sqlc.GetNativeSubagentParams{SessionID: session, NativeID: p.NativeID}) if err != nil { return err } if !child.PublicVisible || p.OccurredAtMS < child.NativeCreatedAt*1000 { - return ErrInvalidInput + return sessions.ErrInvalidInput } inserted, err := q.PutSubagentEffect(ctx, sqlc.PutSubagentEffectParams{SessionID: session, EffectID: p.EffectID, Payload: raw}) if errors.Is(err, pgx.ErrNoRows) { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } if err != nil || !inserted { return err } if p.OccurredAtMS < child.LifecycleAtMs { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } if p.Status == child.Status { return nil diff --git a/services/core/internal/store/subagent_native_outputs_test.go b/services/core/internal/store/subagent_native_outputs_test.go index 07f28b0fd..4a880f941 100644 --- a/services/core/internal/store/subagent_native_outputs_test.go +++ b/services/core/internal/store/subagent_native_outputs_test.go @@ -28,7 +28,7 @@ func TestSubagentNativeFunctionResultDoesNotConsumeOutputIndex(t *testing.T) { call := json.RawMessage(`{"id":"native-file-change","stage":"after","observation":{"status":"completed","kind":"function","name":"apply_patch","arguments":{"count":9007199254740993,"scale":1e2},"content":[{"type":"input_text","text":"file written"}]}}`) text := "child answer" message, _ := json.Marshal(proto.OutputMessagePayload{ID: "answer", Status: "completed", Text: &text}) - facts := []ExecutionEvent{ + facts := []sessions.ExecutionEvent{ subagentIdentityEvent("child", "root", 100), subagentFact(proto.TypeSubagentTurn, proto.SubagentTurnPayload{NativeID: "child", TurnID: "turn", Status: sessions.TurnInProgress, CreatedAtMS: 100000}), subagentFact(proto.TypeSubagentItem, proto.SubagentItemPayload{NativeID: "child", TurnID: "turn", ItemID: "native-file-change", Position: 0, Kind: proto.TypeToolCall, Payload: call}), @@ -39,7 +39,7 @@ func TestSubagentNativeFunctionResultDoesNotConsumeOutputIndex(t *testing.T) { } finished := int64(101000) terminal := subagentFact(proto.TypeSubagentTurn, proto.SubagentTurnPayload{NativeID: "child", TurnID: "turn", Status: sessions.TurnCompleted, CreatedAtMS: 100000, CompletedAtMS: &finished}) - if err = owner.AppendTurnEvents(t.Context(), tenant, session.ID, input.TurnID, 5, []ExecutionEvent{terminal, facts[2], facts[3]}); err != nil { + if err = owner.AppendTurnEvents(t.Context(), tenant, session.ID, input.TurnID, 5, []sessions.ExecutionEvent{terminal, facts[2], facts[3]}); err != nil { t.Fatal("identical native tool history must survive replay after completion", err) } child, err := s.GetSubagentIdentity(t.Context(), tenant, session.ID, "child") @@ -105,7 +105,7 @@ func TestSubagentCancelledPartialMessageSurvivesHistoryReplay(t *testing.T) { }) finished := int64(101000) terminal := subagentFact(proto.TypeSubagentTurn, proto.SubagentTurnPayload{NativeID: "child", TurnID: "child-turn", Status: sessions.TurnCancelled, CreatedAtMS: 100000, CompletedAtMS: &finished}) - facts := []ExecutionEvent{ + facts := []sessions.ExecutionEvent{ subagentIdentityEvent("child", "root", 100), subagentFact(proto.TypeSubagentTurn, proto.SubagentTurnPayload{NativeID: "child", TurnID: "child-turn", Status: sessions.TurnInProgress, CreatedAtMS: 100000}), message, terminal, diff --git a/services/core/internal/store/subagent_reads.go b/services/core/internal/store/subagent_reads.go index 1e4af963c..a8cfcf03a 100644 --- a/services/core/internal/store/subagent_reads.go +++ b/services/core/internal/store/subagent_reads.go @@ -17,7 +17,7 @@ import ( func publicSubagent(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, id string) (v1.Subagent, error) { row, err := q.GetPublicSubagent(ctx, sqlc.GetPublicSubagentParams{SessionID: session, ID: pgunit.PathID(id)}) if errors.Is(err, pgx.ErrNoRows) { - return v1.Subagent{}, ErrNotFound + return v1.Subagent{}, sessions.ErrNotFound } if err != nil { return v1.Subagent{}, err @@ -49,7 +49,7 @@ func (s *Store) GetSubagent(ctx context.Context, tenant, session, id string) (v1 func (s *Store) ListSubagents(ctx context.Context, tenant, session, after string, limit int, asc bool) (v1.SubagentList, error) { result := v1.SubagentList{Data: []v1.Subagent{}} if limit < 1 || limit > 100 { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } err := s.withPublicSession(ctx, tenant, session, func(ctx context.Context, q *sqlc.Queries, sid pgtype.UUID) error { p := sqlc.ListPublicSubagentsParams{SessionID: sid, Ascending: asc, PageLimit: int32(limit + 1), AfterID: pgtype.UUID{Valid: true}} @@ -58,7 +58,7 @@ func (s *Store) ListSubagents(ctx context.Context, tenant, session, after string // malformed one, is an invalid cursor rather than a missing resource. cursor, err := publicSubagent(ctx, q, sid, after) if err != nil { - return unresolvedCursor(err, errResourceCursor) + return unresolvedCursor(err, sessions.ErrResourceCursor) } p.AfterOpened = pgtype.Int8{Int64: cursor.OpenedAt, Valid: true} p.AfterID, _ = parseID(after) @@ -86,7 +86,7 @@ func (s *Store) ListSubagents(ctx context.Context, tenant, session, after string func childTurn(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, child, id string) (sqlc.SubagentTurn, error) { row, err := q.GetChildTurn(ctx, sqlc.GetChildTurnParams{SessionID: session, ID: pgunit.PathID(id)}) if errors.Is(err, pgx.ErrNoRows) || (err == nil && uuid.UUID(row.SubagentID.Bytes).String() != child) { - return row, ErrNotFound + return row, sessions.ErrNotFound } return row, err } @@ -144,7 +144,7 @@ func (s *Store) GetSubagentTurn(ctx context.Context, tenant, session, child, id func (s *Store) ListSubagentTurns(ctx context.Context, tenant, session, child, after string, limit int, asc bool) (v1.TurnList, error) { result := v1.TurnList{Data: []v1.Turn{}} if limit < 1 || limit > 100 { - return result, ErrInvalidInput + return result, sessions.ErrInvalidInput } err := s.withPublicSession(ctx, tenant, session, func(ctx context.Context, q *sqlc.Queries, sid pgtype.UUID) error { if _, err := publicSubagent(ctx, q, sid, child); err != nil { @@ -156,7 +156,7 @@ func (s *Store) ListSubagentTurns(ctx context.Context, tenant, session, child, a // Root Turns and other children's Turns are outside this list. row, err := childTurn(ctx, q, sid, child, after) if err != nil { - return unresolvedCursor(err, errResourceCursor) + return unresolvedCursor(err, sessions.ErrResourceCursor) } p.AfterCreated = row.CreatedAt p.AfterID = row.ID diff --git a/services/core/internal/store/subagent_resources_test.go b/services/core/internal/store/subagent_resources_test.go index b78b47ecf..f3149f9d6 100644 --- a/services/core/internal/store/subagent_resources_test.go +++ b/services/core/internal/store/subagent_resources_test.go @@ -14,18 +14,18 @@ import ( "github.com/google/uuid" ) -func newSubagentSession(t *testing.T, s *Store) (string, Session) { +func newSubagentSession(t *testing.T, s *Store) (string, sessions.Session) { t.Helper() tenant := uuid.NewString() - session, err := s.CreateSession(t.Context(), tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "subagent", Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test","multi_agent":{"enabled":true,"max_concurrent_subagents":6}}}`)}) + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "subagent", Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test","multi_agent":{"enabled":true,"max_concurrent_subagents":6}}}`)}) if err != nil { t.Fatal(err) } return tenant, session } -func subagentFact(kind string, value any) ExecutionEvent { +func subagentFact(kind string, value any) sessions.ExecutionEvent { raw, _ := json.Marshal(value) - return ExecutionEvent{Kind: kind, Payload: raw} + return sessions.ExecutionEvent{Kind: kind, Payload: raw} } func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) { s, pool := testStore(t) @@ -42,7 +42,7 @@ func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) { root := submitMessage(t, s, tenant, session.ID, "first") transition(t, owner, tenant, session.ID, root.TurnID, sessions.TurnQueued, sessions.TurnInProgress) ordinal := int32(1) - appendFacts := func(facts ...ExecutionEvent) { + appendFacts := func(facts ...sessions.ExecutionEvent) { t.Helper() if err := owner.AppendTurnEvents(ctx, tenant, session.ID, root.TurnID, ordinal, facts); err != nil { t.Fatal(err) @@ -89,10 +89,10 @@ func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) { t.Fatal(same, err) } // Session Turn reads carry root work only: a child Turn ID is missing there. - if _, err = s.GetTurn(ctx, tenant, session.ID, tid); !errors.Is(err, ErrNotFound) { + if _, err = s.GetTurn(ctx, tenant, session.ID, tid); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("child Turn in Session Turn retrieval", err) } - if _, err = s.ListTurns(ctx, tenant, session.ID, tid, 100, true); !errors.Is(err, ErrNotFound) { + if _, err = s.ListTurns(ctx, tenant, session.ID, tid, 100, true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("child Turn as a Session Turn cursor", err) } allTurns, err := s.ListTurns(ctx, tenant, session.ID, "", 100, true) @@ -106,10 +106,10 @@ func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) { if err != nil || len(nestedTurns.Data) != 1 || nestedTurns.Data[0].AgentID != "agent_root" || *nestedTurns.Data[0].SubagentID != nested.ID { t.Fatal(nestedTurns, err) } - if _, err = s.GetSubagentTurn(ctx, uuid.NewString(), session.ID, child.ID, tid); !errors.Is(err, ErrNotFound) { + if _, err = s.GetSubagentTurn(ctx, uuid.NewString(), session.ID, child.ID, tid); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign tenant child Turn", err) } - if _, err = s.ListSubagentTurns(ctx, uuid.NewString(), session.ID, child.ID, "", 20, true); !errors.Is(err, ErrNotFound) { + if _, err = s.ListSubagentTurns(ctx, uuid.NewString(), session.ID, child.ID, "", 20, true); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign tenant child Turns", err) } if allTurns, err = s.ListTurns(ctx, tenant, session.ID, "", 100, true); err != nil || len(allTurns.Turns) != 1 { @@ -128,13 +128,13 @@ func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) { t.Fatal("child Item leaked into root Items") } } - if _, err = s.GetSubagentTurn(ctx, tenant, session.ID, nested.ID, tid); !errors.Is(err, ErrNotFound) { + if _, err = s.GetSubagentTurn(ctx, tenant, session.ID, nested.ID, tid); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("nested ownership", err) } - if _, err = s.ListSubagentItems(ctx, tenant, session.ID, nested.ID, own.Data[0].ID, 20, true); !errors.Is(err, errItemCursor) { + if _, err = s.ListSubagentItems(ctx, tenant, session.ID, nested.ID, own.Data[0].ID, 20, true); !errors.Is(err, sessions.ErrItemCursor) { t.Fatal("another child's Item cursor", err) } - if _, err = s.GetSubagent(ctx, uuid.NewString(), session.ID, child.ID); !errors.Is(err, ErrNotFound) { + if _, err = s.GetSubagent(ctx, uuid.NewString(), session.ID, child.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("foreign tenant", err) } closed := proto.SubagentLifecyclePayload{NativeID: "child", EffectID: "native-close-1", Status: "closed", OccurredAtMS: 103000} @@ -177,11 +177,11 @@ func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) { } // A conflicting replay rolls back the whole batch, including an earlier new child. closed.OccurredAtMS++ - facts := []ExecutionEvent{subagentIdentityEvent("rollback", "root", 104), subagentFact(proto.TypeSubagentLifecycle, closed)} - if err = owner.AppendTurnEvents(ctx, tenant, session.ID, root.TurnID, ordinal, facts); !errors.Is(err, ErrIdempotencyConflict) { + facts := []sessions.ExecutionEvent{subagentIdentityEvent("rollback", "root", 104), subagentFact(proto.TypeSubagentLifecycle, closed)} + if err = owner.AppendTurnEvents(ctx, tenant, session.ID, root.TurnID, ordinal, facts); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal(err) } - if _, err = s.GetSubagentIdentity(ctx, tenant, session.ID, "rollback"); !errors.Is(err, ErrNotFound) { + if _, err = s.GetSubagentIdentity(ctx, tenant, session.ID, "rollback"); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("non-atomic batch", err) } // Reads do not invoke native processes, including after the root finishes. diff --git a/services/core/internal/store/subagent_turn_projection.go b/services/core/internal/store/subagent_turn_projection.go index 19622119c..720f3177e 100644 --- a/services/core/internal/store/subagent_turn_projection.go +++ b/services/core/internal/store/subagent_turn_projection.go @@ -27,23 +27,23 @@ func nativeMillis(value *int64) pgtype.Timestamptz { func projectSubagentTurn(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, raw json.RawMessage) error { var p proto.SubagentTurnPayload if json.Unmarshal(raw, &p) != nil || !validNativeIdentity(p.NativeID) || !validNativeIdentity(p.TurnID) || p.CreatedAtMS <= 0 { - return ErrInvalidInput + return sessions.ErrInvalidInput } if p.Status != sessions.TurnQueued && p.Status != sessions.TurnInProgress && p.Status != sessions.TurnWaiting && !sessions.TerminalStatus(p.Status) { - return ErrInvalidInput + return sessions.ErrInvalidInput } if sessions.TerminalStatus(p.Status) != (p.CompletedAtMS != nil) { - return ErrInvalidInput + return sessions.ErrInvalidInput } if (p.StartedAtMS != nil && *p.StartedAtMS < p.CreatedAtMS) || (p.CompletedAtMS != nil && (*p.CompletedAtMS < p.CreatedAtMS || (p.StartedAtMS != nil && *p.CompletedAtMS < *p.StartedAtMS))) { - return ErrInvalidInput + return sessions.ErrInvalidInput } child, err := q.GetNativeSubagent(ctx, sqlc.GetNativeSubagentParams{SessionID: session, NativeID: p.NativeID}) if err != nil { return err } if !child.PublicVisible { - return ErrInvalidInput + return sessions.ErrInvalidInput } id, _ := parseID(items.Identity(uuid.UUID(child.ID.Bytes).String(), "turn:"+p.TurnID)) old, err := q.GetChildTurn(ctx, sqlc.GetChildTurnParams{SessionID: session, ID: id}) @@ -57,7 +57,7 @@ func projectSubagentTurn(ctx context.Context, q *sqlc.Queries, session pgtype.UU if measured := sessions.MeasuredUsage("usage", value); measured != nil { usage, _ = json.Marshal(measured) } else { - return ErrInvalidInput + return sessions.ErrInvalidInput } } // Re-reading native history cannot reopen or mutate a completed child Turn. @@ -69,16 +69,16 @@ func projectSubagentTurn(ctx context.Context, q *sqlc.Queries, session pgtype.UU _ = json.Unmarshal(old.TokenUsage, &previousUsage) _ = json.Unmarshal(usage, &nextUsage) if old.Status != p.Status || old.CompletedAt.Time.UnixMilli() != *p.CompletedAtMS || !reflect.DeepEqual(previousUsage, nextUsage) { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } return nil } if !fresh && old.Status != p.Status && !validTransition(old.Status, p.Status) { - return ErrTurnConflict + return sessions.ErrTurnConflict } row, err := q.PutChildTurn(ctx, sqlc.PutChildTurnParams{ID: id, SessionID: session, SubagentID: child.ID, NativeID: p.TurnID, Status: p.Status, CreatedAt: pgtype.Timestamptz{Time: time.UnixMilli(p.CreatedAtMS), Valid: true}, StartedAt: nativeMillis(p.StartedAtMS), CompletedAt: nativeMillis(p.CompletedAtMS), TokenUsage: usage}) if errors.Is(err, pgx.ErrNoRows) { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } if err != nil { return err diff --git a/services/core/internal/store/subagent_visibility_public_test.go b/services/core/internal/store/subagent_visibility_public_test.go index a253c3210..bd66a6401 100644 --- a/services/core/internal/store/subagent_visibility_public_test.go +++ b/services/core/internal/store/subagent_visibility_public_test.go @@ -11,7 +11,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -60,9 +59,9 @@ func collectEvents(t *testing.T, stream sseLines) eventCollector { return collector } -func subagentFixture(kind string, value any) store.ExecutionEvent { +func subagentFixture(kind string, value any) sessions.ExecutionEvent { raw, _ := json.Marshal(value) - return store.ExecutionEvent{Kind: kind, Payload: raw} + return sessions.ExecutionEvent{Kind: kind, Payload: raw} } // Child work appears only where the official service shows it: Session Turn @@ -127,19 +126,19 @@ func TestSubagentVisibilityPublic(t *testing.T) { if err = writer.BindSessionDevice(ctx, tenant, session, host.ID); err != nil { t.Fatal(err) } - if _, err = writer.TransitionTurn(ctx, tenant, session, root, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = writer.TransitionTurn(ctx, tenant, session, root, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { t.Fatal(err) } - identity := func(child, parent string, created int64) store.ExecutionEvent { + identity := func(child, parent string, created int64) sessions.ExecutionEvent { return subagentFixture(proto.TypeSubagentIdentity, proto.SubagentIdentityPayload{NativeID: child, ParentNativeID: parent, NativeCreatedAt: created, ParentTurnID: "native-root", SourceItemID: "spawn-" + child}) } opened, finished := int64(1700000001000), int64(1700000002000) - message := func(child, turn, id string, position int32) store.ExecutionEvent { + message := func(child, turn, id string, position int32) sessions.ExecutionEvent { text := "answer " + id payload, _ := json.Marshal(proto.OutputMessagePayload{ID: id, Status: "completed", Text: &text}) return subagentFixture(proto.TypeSubagentItem, proto.SubagentItemPayload{NativeID: child, TurnID: turn, ItemID: id, Position: position, Kind: proto.TypeOutputMessage, Payload: payload}) } - facts := []store.ExecutionEvent{ + facts := []sessions.ExecutionEvent{ identity("child", "root", 1700000001), identity("nested", "child", 1700000001), subagentFixture(proto.TypeSubagentCoordination, proto.SubagentCoordinationPayload{ID: "spawn", Kind: "create_subagent_call", Status: "completed"}), subagentFixture(proto.TypeSubagentTurn, proto.SubagentTurnPayload{NativeID: "child", TurnID: "child-turn", Status: sessions.TurnInProgress, CreatedAtMS: opened, StartedAtMS: &opened}), @@ -151,7 +150,7 @@ func TestSubagentVisibilityPublic(t *testing.T) { if err = writer.AppendTurnEvents(ctx, tenant, session, root, 1, facts); err != nil { t.Fatal(err) } - if _, err = writer.TransitionTurn(ctx, tenant, session, root, store.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted}); err != nil { + if _, err = writer.TransitionTurn(ctx, tenant, session, root, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted}); err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/template_composition_public_test.go b/services/core/internal/store/template_composition_public_test.go index fb08725f3..2741a894d 100644 --- a/services/core/internal/store/template_composition_public_test.go +++ b/services/core/internal/store/template_composition_public_test.go @@ -16,6 +16,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -112,7 +113,7 @@ func TestTemplateCompositionOfficialClientPostgres(t *testing.T) { t.Fatalf("%s command order differs", label) } } - if _, err := current.ReadEnvironmentSetup(t.Context(), foreignTenant, id); !errors.Is(err, store.ErrNotFound) { + if _, err := current.ReadEnvironmentSetup(t.Context(), foreignTenant, id); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("%s foreign setup read: %v", label, err) } for position, want := range contents { diff --git a/services/core/internal/store/template_null_selection_public_test.go b/services/core/internal/store/template_null_selection_public_test.go index 523785b26..5b9cb0c26 100644 --- a/services/core/internal/store/template_null_selection_public_test.go +++ b/services/core/internal/store/template_null_selection_public_test.go @@ -18,6 +18,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -117,7 +118,7 @@ func TestTemplateNullSelectionOfficialClientPostgres(t *testing.T) { t.Fatalf("%s frozen Plugin bytes changed", label) } } - if _, err := current.ReadEnvironmentSetup(t.Context(), foreignTenant, id); !errors.Is(err, store.ErrNotFound) { + if _, err := current.ReadEnvironmentSetup(t.Context(), foreignTenant, id); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("%s foreign setup read: %v", label, err) } file, body, err := current.ReadInitialEnvironmentFile(t.Context(), tenant, id, 0) diff --git a/services/core/internal/store/token_usage_integration_test.go b/services/core/internal/store/token_usage_integration_test.go index d2c941ec9..1dcadab59 100644 --- a/services/core/internal/store/token_usage_integration_test.go +++ b/services/core/internal/store/token_usage_integration_test.go @@ -17,7 +17,7 @@ func TestTokenUsageDurableSnapshotsAndSessionTotals(t *testing.T) { ctx := context.Background() s, pool := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "usage"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "usage"}) if err != nil { t.Fatal(err) } @@ -36,18 +36,18 @@ func TestTokenUsageDurableSnapshotsAndSessionTotals(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, admission.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, admission.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } - batch := []store.ExecutionEvent{{Kind: "usage", Payload: usage(10)}} + batch := []sessions.ExecutionEvent{{Kind: "usage", Payload: usage(10)}} for range 2 { if err = s.AppendTurnEvents(ctx, tenant, session.ID, admission.TurnID, 1, batch); err != nil { t.Fatal(err) } } // A later snapshot replaces the earlier measurement; it is not a delta. - if err = s.AppendTurnEvents(ctx, tenant, session.ID, admission.TurnID, 2, []store.ExecutionEvent{{Kind: "usage", Payload: usage(20)}}); err != nil { + if err = s.AppendTurnEvents(ctx, tenant, session.ID, admission.TurnID, 2, []sessions.ExecutionEvent{{Kind: "usage", Payload: usage(20)}}); err != nil { t.Fatal(err) } measured, err := s.GetTurn(ctx, tenant, session.ID, admission.TurnID) @@ -55,7 +55,7 @@ func TestTokenUsageDurableSnapshotsAndSessionTotals(t *testing.T) { t.Fatal(err) } check(measured.Usage, 20) - if _, err = s.CompleteExecution(ctx, tenant, session.ID, admission.TurnID, sessions.TurnCompleted, json.RawMessage(`{"done":{"usage":`+string(usage(99))+`}}`), "missing-binding", admission.Sequence); !errors.Is(err, store.ErrNotFound) { + if _, err = s.CompleteExecution(ctx, tenant, session.ID, admission.TurnID, sessions.TurnCompleted, json.RawMessage(`{"done":{"usage":`+string(usage(99))+`}}`), "missing-binding", admission.Sequence); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } rolledBack, err := s.GetTurn(ctx, tenant, session.ID, admission.TurnID) @@ -69,10 +69,10 @@ func TestTokenUsageDurableSnapshotsAndSessionTotals(t *testing.T) { t.Fatal(err) } check(completed.Usage, 20) - if _, err = s.CompleteExecution(ctx, tenant, session.ID, admission.TurnID, status, json.RawMessage(`{"done":{"usage":`+string(usage(99))+`}}`), "", admission.Sequence); !errors.Is(err, store.ErrTurnConflict) { + if _, err = s.CompleteExecution(ctx, tenant, session.ID, admission.TurnID, status, json.RawMessage(`{"done":{"usage":`+string(usage(99))+`}}`), "", admission.Sequence); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } - if _, err = s.GetTurn(ctx, uuid.NewString(), session.ID, admission.TurnID); !errors.Is(err, store.ErrNotFound) { + if _, err = s.GetTurn(ctx, uuid.NewString(), session.ID, admission.TurnID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } @@ -98,7 +98,7 @@ func TestTokenUsageDurableSnapshotsAndSessionTotals(t *testing.T) { if err != nil || len(page.Sessions) != 1 || string(page.Sessions[0].Usage) != string(got.Usage) { t.Fatalf("list totals: %+v %v", page, err) } - if _, err = fresh.GetSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, store.ErrNotFound) { + if _, err = fresh.GetSession(ctx, uuid.NewString(), session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } @@ -107,7 +107,7 @@ func TestCancellationReceiptUsageSurvivesRecovery(t *testing.T) { ctx := context.Background() s, _ := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "cancel-recovery"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "cancel-recovery"}) if err != nil { t.Fatal(err) } @@ -115,16 +115,16 @@ func TestCancellationReceiptUsageSurvivesRecovery(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, admission.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, admission.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } receipt := json.RawMessage(`{"applied":true,"outcome":{"usage":{"tokens":{"input_tokens":10,"cached_input_tokens":4,"output_tokens":3,"reasoning_output_tokens":2,"total_tokens":13}}}}`) - if err = s.AppendTurnEvents(ctx, tenant, session.ID, admission.TurnID, 1, []store.ExecutionEvent{{Kind: "cancel_receipt", Payload: receipt}}); err != nil { + if err = s.AppendTurnEvents(ctx, tenant, session.ID, admission.TurnID, 1, []sessions.ExecutionEvent{{Kind: "cancel_receipt", Payload: receipt}}); err != nil { t.Fatal(err) } // Startup recovery has no in-memory cancellation outcome. - recovered, err := s.TransitionTurn(ctx, tenant, session.ID, admission.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"execution_interrupted"}`)}) + recovered, err := s.TransitionTurn(ctx, tenant, session.ID, admission.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"execution_interrupted"}`)}) if err != nil { t.Fatal(err) } @@ -141,12 +141,12 @@ func TestSessionUsageRequiresEveryRootTurnEndedAndMeasured(t *testing.T) { ctx := context.Background() s, _ := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "unknown-usage"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "unknown-usage"}) if err != nil { t.Fatal(err) } - usage := func(input int) []store.ExecutionEvent { - return []store.ExecutionEvent{{Kind: "usage", Payload: json.RawMessage(fmt.Sprintf(`{"tokens":{"input_tokens":%d,"cached_input_tokens":4,"output_tokens":3,"reasoning_output_tokens":2,"total_tokens":%d}}`, input, input+3))}} + usage := func(input int) []sessions.ExecutionEvent { + return []sessions.ExecutionEvent{{Kind: "usage", Payload: json.RawMessage(fmt.Sprintf(`{"tokens":{"input_tokens":%d,"cached_input_tokens":4,"output_tokens":3,"reasoning_output_tokens":2,"total_tokens":%d}}`, input, input+3))}} } // Runtime telemetry keeps counting every recorded snapshot, active Turns // included, and is scoped to the tenant. @@ -182,7 +182,7 @@ func TestSessionUsageRequiresEveryRootTurnEndedAndMeasured(t *testing.T) { t.Fatalf("usage = %s, want total %d", got.Usage, want) } } - submit := func(key string) store.InputReceipt { + submit := func(key string) sessions.InputReceipt { t.Helper() admission, err := s.SubmitMessage(ctx, tenant, session.ID, key, json.RawMessage(`{"text":"measure"}`)) if err != nil { @@ -192,11 +192,11 @@ func TestSessionUsageRequiresEveryRootTurnEndedAndMeasured(t *testing.T) { } move := func(turn, from, to string) { t.Helper() - if _, err := s.TransitionTurn(ctx, tenant, session.ID, turn, store.TurnTransition{ExpectedStatus: from, Status: to}); err != nil { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, turn, sessions.TurnTransition{ExpectedStatus: from, Status: to}); err != nil { t.Fatal(err) } } - finish := func(admission store.InputReceipt, status string) { + finish := func(admission sessions.InputReceipt, status string) { t.Helper() if _, err := s.CompleteExecution(ctx, tenant, session.ID, admission.TurnID, status, json.RawMessage(`{"done":{}}`), "", admission.Sequence); err != nil { t.Fatal(err) diff --git a/services/core/internal/store/turn_completion.go b/services/core/internal/store/turn_completion.go index 7281d984e..731476f62 100644 --- a/services/core/internal/store/turn_completion.go +++ b/services/core/internal/store/turn_completion.go @@ -15,8 +15,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -var ErrUnappliedInputs = errors.New("turn has messages without an executor receipt") - // CompleteExecution commits the outcome and native continuity under the admission lock. func (s *Store) CompleteExecution(ctx context.Context, tenantID, sessionID, turnID, status string, outcome json.RawMessage, nativeID string, appliedThrough int64) (sessions.Turn, error) { p, err := turnLookup(tenantID, sessionID, turnID) @@ -24,22 +22,22 @@ func (s *Store) CompleteExecution(ctx context.Context, tenantID, sessionID, turn return sessions.Turn{}, err } if !sessions.TerminalStatus(status) || len(outcome) > 512*1024 || len(nativeID) > 512 || appliedThrough < 0 { - return sessions.Turn{}, ErrInvalidInput + return sessions.Turn{}, sessions.ErrInvalidInput } outcome, err = jsonobject.Normalize(outcome) if err != nil { - return sessions.Turn{}, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return sessions.Turn{}, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } var row sqlc.Turn err = s.withSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { current, err := q.GetTurn(ctx, p) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } else if err != nil { return err } if current.Status != sessions.TurnInProgress && (current.Status != sessions.TurnWaiting || status == sessions.TurnCompleted) { - return ErrTurnConflict + return sessions.ErrTurnConflict } if status == sessions.TurnCompleted { pending, err := q.HasUnappliedMessages(ctx, sqlc.HasUnappliedMessagesParams{SessionID: session, TurnID: p.ID, Sequence: appliedThrough}) @@ -47,7 +45,7 @@ func (s *Store) CompleteExecution(ctx context.Context, tenantID, sessionID, turn return err } if pending { - return ErrUnappliedInputs + return sessions.ErrUnappliedInputs } } sourceCompleted := pgtype.Timestamptz{} @@ -58,12 +56,12 @@ func (s *Store) CompleteExecution(ctx context.Context, tenantID, sessionID, turn } `json:"done"` } if json.Unmarshal(outcome, &snapshot) != nil { - return ErrInvalidInput + return sessions.ErrInvalidInput } if snapshot.Done != nil && snapshot.Done.SourceCompletedAtMS != nil { ms := *snapshot.Done.SourceCompletedAtMS if ms <= 0 { - return ErrInvalidInput + return sessions.ErrInvalidInput } // Native and Core timestamps come from independent host clocks. // Preserve source time; committed activity uses the database clock. @@ -72,7 +70,7 @@ func (s *Store) CompleteExecution(ctx context.Context, tenantID, sessionID, turn } row, err = q.TransitionTurn(ctx, sqlc.TransitionTurnParams{ID: p.ID, SessionID: session, ExpectedStatus: current.Status, NewStatus: status, Outcome: outcome, SourceCompletedAt: sourceCompleted}) if errors.Is(err, pgx.ErrNoRows) { - return ErrTurnConflict + return sessions.ErrTurnConflict } if err != nil { return err @@ -86,7 +84,7 @@ func (s *Store) CompleteExecution(ctx context.Context, tenantID, sessionID, turn return err } if n != 1 { - return ErrNotFound + return sessions.ErrNotFound } } row, err = q.GetTurn(ctx, p) diff --git a/services/core/internal/store/turn_events.go b/services/core/internal/store/turn_events.go index d3d355d37..bb9ef93bd 100644 --- a/services/core/internal/store/turn_events.go +++ b/services/core/internal/store/turn_events.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "slices" - "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" @@ -15,51 +14,37 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -var ErrEventLimit = errors.New("execution event storage limit exceeded") - -type TurnEvent struct { - Ordinal int32 - Kind string - Payload json.RawMessage - CreatedAt time.Time -} - -type ExecutionEvent struct { - Kind string `json:"kind"` - Payload json.RawMessage `json:"payload"` -} - // AppendTurnEvents records an ordered batch atomically, not public SSE replay events. -func (s *Store) AppendTurnEvents(ctx context.Context, tenantID, sessionID, turnID string, first int32, events []ExecutionEvent) error { +func (s *Store) AppendTurnEvents(ctx context.Context, tenantID, sessionID, turnID string, first int32, events []sessions.ExecutionEvent) error { p, err := turnLookup(tenantID, sessionID, turnID) if err != nil { return err } if first < 1 || len(events) == 0 || len(events) > 64 { - return ErrInvalidInput + return sessions.ErrInvalidInput } // Subagent observations are projected under the execution journal, so only // the execution writer records a batch that contains one, replays included. - if slices.ContainsFunc(events, func(event ExecutionEvent) bool { return isSubagentObservation(event.Kind) }) { + if slices.ContainsFunc(events, func(event sessions.ExecutionEvent) bool { return isSubagentObservation(event.Kind) }) { if err := s.checkExecutionAuthority(); err != nil { return err } } - normalized := make([]ExecutionEvent, len(events)) + normalized := make([]sessions.ExecutionEvent, len(events)) payloadBytes := 0 for i, event := range events { - if len(event.Payload) > 512*1024 || !enginePattern.MatchString(event.Kind) { - return ErrInvalidInput + if len(event.Payload) > 512*1024 || !sessions.ValidEngine(event.Kind) { + return sessions.ErrInvalidInput } payload, err := jsonobject.Normalize(event.Payload) if err != nil { - return fmt.Errorf("%w: %w", ErrInvalidInput, err) + return fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } - normalized[i] = ExecutionEvent{Kind: event.Kind, Payload: payload} + normalized[i] = sessions.ExecutionEvent{Kind: event.Kind, Payload: payload} payloadBytes += len(payload) } if payloadBytes > 1024*1024 { - return ErrEventLimit + return sessions.ErrEventLimit } batch, err := json.Marshal(normalized) if err != nil { @@ -68,7 +53,7 @@ func (s *Store) AppendTurnEvents(ctx context.Context, tenantID, sessionID, turnI return s.withSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, _ pgtype.UUID) error { turn, err := q.GetTurn(ctx, p) if errors.Is(err, pgx.ErrNoRows) { - return ErrNotFound + return sessions.ErrNotFound } if err != nil { return err @@ -79,15 +64,15 @@ func (s *Store) AppendTurnEvents(ctx context.Context, tenantID, sessionID, turnI return err } if !matches { - return ErrIdempotencyConflict + return sessions.ErrIdempotencyConflict } return nil } if (turn.Status != sessions.TurnInProgress && turn.Status != sessions.TurnWaiting) || first != turn.EventCount+1 { - return ErrTurnConflict + return sessions.ErrTurnConflict } if turn.EventCount+int32(len(events)) > 65536 || turn.EventBytes+int64(payloadBytes) > 32*1024*1024 { - return ErrEventLimit + return sessions.ErrEventLimit } if err = q.InsertTurnEventBatch(ctx, sqlc.InsertTurnEventBatchParams{SessionID: p.SessionID, TurnID: p.ID, FirstOrdinal: first, Batch: batch}); err != nil { return err @@ -110,13 +95,13 @@ func insertTurnEvent(ctx context.Context, q *sqlc.Queries, turn sqlc.Turn, kind return indexEvents(ctx, q, turn.SessionID, turn.ID, turn.EventCount+1) } -func (s *Store) ListTurnEvents(ctx context.Context, tenantID, sessionID, turnID string, after int32, limit int) ([]TurnEvent, error) { +func (s *Store) ListTurnEvents(ctx context.Context, tenantID, sessionID, turnID string, after int32, limit int) ([]sessions.TurnEvent, error) { p, err := turnLookup(tenantID, sessionID, turnID) if err != nil { return nil, err } if after < 0 || limit < 1 || limit > 100 { - return nil, ErrInvalidInput + return nil, sessions.ErrInvalidInput } if _, err = s.GetTurn(ctx, tenantID, sessionID, turnID); err != nil { return nil, err @@ -125,9 +110,9 @@ func (s *Store) ListTurnEvents(ctx context.Context, tenantID, sessionID, turnID if err != nil { return nil, err } - events := make([]TurnEvent, 0, len(rows)) + events := make([]sessions.TurnEvent, 0, len(rows)) for _, row := range rows { - events = append(events, TurnEvent{Ordinal: row.Ordinal, Kind: row.Kind, Payload: row.Payload, CreatedAt: row.CreatedAt.Time}) + events = append(events, sessions.TurnEvent{Ordinal: row.Ordinal, Kind: row.Kind, Payload: row.Payload, CreatedAt: row.CreatedAt.Time}) } return events, nil } diff --git a/services/core/internal/store/turn_events_test.go b/services/core/internal/store/turn_events_test.go index 505ed61f9..d95047362 100644 --- a/services/core/internal/store/turn_events_test.go +++ b/services/core/internal/store/turn_events_test.go @@ -16,7 +16,7 @@ func TestTurnEventBatchesAreOrderedIsolatedAndDurable(t *testing.T) { ctx := context.Background() s, _ := store.NewTestStore(t) tenant := uuid.NewString() - session, err := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "events"}) + session, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "events"}) if err != nil { t.Fatal(err) } @@ -24,11 +24,11 @@ func TestTurnEventBatchesAreOrderedIsolatedAndDurable(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err = s.TransitionTurn(ctx, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } - batch := []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"部分内容","sequence":1}`)}, {Kind: "usage", Payload: json.RawMessage(`{"input_tokens":10}`)}} + batch := []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"部分内容","sequence":1}`)}, {Kind: "usage", Payload: json.RawMessage(`{"input_tokens":10}`)}} var wg sync.WaitGroup errs := make(chan error, 8) for range 8 { @@ -42,27 +42,27 @@ func TestTurnEventBatchesAreOrderedIsolatedAndDurable(t *testing.T) { t.Fatal(err) } } - conflict := []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"changed"}`)}} - if err := s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 1, conflict); !errors.Is(err, store.ErrIdempotencyConflict) { + conflict := []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"changed"}`)}} + if err := s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 1, conflict); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatal(err) } - if err := s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, conflict); !errors.Is(err, store.ErrTurnConflict) { + if err := s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, conflict); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } for _, owner := range []string{uuid.NewString()} { - if err := s.AppendTurnEvents(ctx, owner, session.ID, input.TurnID, 1, batch); !errors.Is(err, store.ErrNotFound) { + if err := s.AppendTurnEvents(ctx, owner, session.ID, input.TurnID, 1, batch); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } - if _, err := s.ListTurnEvents(ctx, owner, session.ID, input.TurnID, 0, 100); !errors.Is(err, store.ErrNotFound) { + if _, err := s.ListTurnEvents(ctx, owner, session.ID, input.TurnID, 0, 100); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } } - other, _ := s.CreateSession(ctx, tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) - if _, err := s.ListTurnEvents(ctx, tenant, other.ID, input.TurnID, 0, 100); !errors.Is(err, store.ErrNotFound) { + other, _ := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "other"}) + if _, err := s.ListTurnEvents(ctx, tenant, other.ID, input.TurnID, 0, 100); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } // A failed native binding write must roll back both the terminal event and status. - if _, err = s.CompleteExecution(ctx, tenant, session.ID, input.TurnID, sessions.TurnCompleted, json.RawMessage(`{}`), "missing-binding", input.Sequence); !errors.Is(err, store.ErrNotFound) { + if _, err = s.CompleteExecution(ctx, tenant, session.ID, input.TurnID, sessions.TurnCompleted, json.RawMessage(`{}`), "missing-binding", input.Sequence); !errors.Is(err, sessions.ErrNotFound) { t.Fatal(err) } events, _ := s.ListTurnEvents(ctx, tenant, session.ID, input.TurnID, 0, 100) @@ -75,7 +75,7 @@ func TestTurnEventBatchesAreOrderedIsolatedAndDurable(t *testing.T) { if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 1, batch); err != nil { t.Fatal("retry after terminal", err) } - if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, conflict); !errors.Is(err, store.ErrTurnConflict) { + if err = s.AppendTurnEvents(ctx, tenant, session.ID, input.TurnID, 4, conflict); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } reopened, pool := store.NewTestStore(t) @@ -94,7 +94,7 @@ func TestEventLimitStillAllowsTerminalFailure(t *testing.T) { h := newDispatchHarness(t) ctx := context.Background() input := h.message("start", "Test output budget") - _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, input.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) + _, err := h.s.TransitionTurn(ctx, h.tenant, h.session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) if err != nil { t.Fatal(err) } @@ -103,8 +103,8 @@ func TestEventLimitStillAllowsTerminalFailure(t *testing.T) { if _, err := pool.Exec(ctx, "UPDATE turns SET event_bytes=33554432 WHERE id=$1", input.TurnID); err != nil { t.Fatal(err) } - events := []store.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"more"}`)}} - if err = h.s.AppendTurnEvents(ctx, h.tenant, h.session.ID, input.TurnID, 1, events); !errors.Is(err, store.ErrEventLimit) { + events := []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"delta":"more"}`)}} + if err = h.s.AppendTurnEvents(ctx, h.tenant, h.session.ID, input.TurnID, 1, events); !errors.Is(err, sessions.ErrEventLimit) { t.Fatal(err) } if _, err = h.s.CompleteExecution(ctx, h.tenant, h.session.ID, input.TurnID, sessions.TurnFailed, json.RawMessage(`{"error_code":"event_limit"}`), "", input.Sequence); err != nil { diff --git a/services/core/internal/store/turn_inputs.go b/services/core/internal/store/turn_inputs.go index 697c42cd1..8c731d98b 100644 --- a/services/core/internal/store/turn_inputs.go +++ b/services/core/internal/store/turn_inputs.go @@ -6,8 +6,6 @@ import ( "errors" "fmt" "slices" - "strings" - "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -20,39 +18,19 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -type InputReceipt struct { - Sequence int64 - TurnID string // Empty for a cancellation accepted while the Session was idle. - Replayed bool -} - -type TurnInput struct { - Sequence int64 - Kind string - Payload json.RawMessage - CreatedAt time.Time -} - -// Input is a validated execution command, not an upstream wire type. -// The API validates event fields before constructing this storage input. -type Input struct { - Kind string `json:"kind"` - Payload json.RawMessage `json:"payload"` -} - // SubmitMessage and RequestCancel use the same request-level admission as batches. -func (s *Store) SubmitMessage(ctx context.Context, tenantID, sessionID, key string, payload json.RawMessage) (InputReceipt, error) { - return s.submitOne(ctx, tenantID, sessionID, key, Input{Kind: "message", Payload: payload}) +func (s *Store) SubmitMessage(ctx context.Context, tenantID, sessionID, key string, payload json.RawMessage) (sessions.InputReceipt, error) { + return s.submitOne(ctx, tenantID, sessionID, key, sessions.Input{Kind: "message", Payload: payload}) } -func (s *Store) RequestCancel(ctx context.Context, tenantID, sessionID, key string) (InputReceipt, error) { - return s.submitOne(ctx, tenantID, sessionID, key, Input{Kind: "cancel", Payload: json.RawMessage(`{}`)}) +func (s *Store) RequestCancel(ctx context.Context, tenantID, sessionID, key string) (sessions.InputReceipt, error) { + return s.submitOne(ctx, tenantID, sessionID, key, sessions.Input{Kind: "cancel", Payload: json.RawMessage(`{}`)}) } -func (s *Store) submitOne(ctx context.Context, tenantID, sessionID, key string, input Input) (InputReceipt, error) { - receipts, err := s.SubmitInputs(ctx, tenantID, sessionID, key, []Input{input}) +func (s *Store) submitOne(ctx context.Context, tenantID, sessionID, key string, input sessions.Input) (sessions.InputReceipt, error) { + receipts, err := s.SubmitInputs(ctx, tenantID, sessionID, key, []sessions.Input{input}) if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } return receipts[0], nil } @@ -60,15 +38,15 @@ func (s *Store) submitOne(ctx context.Context, tenantID, sessionID, key string, // SubmitInputs commits a request in order under one Session lock. The entire // batch is the retry identity; replay never re-evaluates a cancellation target. // Internal receipts are not the response body of the public events endpoint. -func (s *Store) SubmitInputs(ctx context.Context, tenantID, sessionID, key string, inputs []Input) ([]InputReceipt, error) { - if err := ValidateInputKey(key); err != nil { +func (s *Store) SubmitInputs(ctx context.Context, tenantID, sessionID, key string, inputs []sessions.Input) ([]sessions.InputReceipt, error) { + if err := sessions.ValidateInputKey(key); err != nil { return nil, err } batch, encoded, err := validateInputs(inputs) if err != nil { return nil, err } - receipts := make([]InputReceipt, 0, len(batch)) + receipts := make([]sessions.InputReceipt, 0, len(batch)) err = s.withPublicSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { previous, err := inputBatchReceipts(ctx, q, session, key, encoded) if err != nil { @@ -78,7 +56,7 @@ func (s *Store) SubmitInputs(ctx context.Context, tenantID, sessionID, key strin receipts = previous return auditpg.RecordWriteAudit(ctx, q, tenantID, "send_events", "session", uuid.UUID(session.Bytes).String(), "") } - if slices.ContainsFunc(batch, func(input Input) bool { return input.Kind == "message" }) { + if slices.ContainsFunc(batch, func(input sessions.Input) bool { return input.Kind == "message" }) { if err := checkEnvironmentFileWriteGate(ctx, q, session); err != nil { return err } @@ -101,59 +79,51 @@ func (s *Store) SubmitInputs(ctx context.Context, tenantID, sessionID, key strin return receipts, nil } -// ValidateInputKey enforces the shared request identity limit, including no-op requests. -func ValidateInputKey(key string) error { - if strings.TrimSpace(key) == "" || len(key) > 128 { - return fmt.Errorf("%w: idempotency key is required and limited to 128 bytes", ErrInvalidInput) - } - return nil -} - -func inputBatchReceipts(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, key string, batch json.RawMessage) ([]InputReceipt, error) { +func inputBatchReceipts(ctx context.Context, q *sqlc.Queries, session pgtype.UUID, key string, batch json.RawMessage) ([]sessions.InputReceipt, error) { rows, err := q.FindInputBatch(ctx, sqlc.FindInputBatchParams{SessionID: session, IdempotencyKey: key, Batch: batch}) if err != nil { return nil, err } - receipts := make([]InputReceipt, 0, len(rows)) + receipts := make([]sessions.InputReceipt, 0, len(rows)) for _, row := range rows { if !row.Matches { - return nil, ErrIdempotencyConflict + return nil, sessions.ErrIdempotencyConflict } receipts = append(receipts, inputReceipt(row.Sequence, row.TurnID, true)) } return receipts, nil } -func validateInputs(inputs []Input) ([]Input, json.RawMessage, error) { +func validateInputs(inputs []sessions.Input) ([]sessions.Input, json.RawMessage, error) { if len(inputs) == 0 || len(inputs) > 64 { - return nil, nil, fmt.Errorf("%w: input batch must contain 1..64 events", ErrInvalidInput) + return nil, nil, fmt.Errorf("%w: input batch must contain 1..64 events", sessions.ErrInvalidInput) } - batch := make([]Input, len(inputs)) + batch := make([]sessions.Input, len(inputs)) size := 0 for i, input := range inputs { size += len(input.Payload) if size > 512*1024 || len(input.Payload) == 0 || (input.Kind != "message" && input.Kind != "cancel" && input.Kind != "tool_result") { - return nil, nil, fmt.Errorf("%w: input payloads must be nonempty and total at most 512 KiB", ErrInvalidInput) + return nil, nil, fmt.Errorf("%w: input payloads must be nonempty and total at most 512 KiB", sessions.ErrInvalidInput) } payload, err := jsonobject.Normalize(input.Payload) if err != nil { - return nil, nil, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return nil, nil, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } if input.Kind == "cancel" && string(payload) != "{}" { - return nil, nil, fmt.Errorf("%w: cancel payload must be empty", ErrInvalidInput) + return nil, nil, fmt.Errorf("%w: cancel payload must be empty", sessions.ErrInvalidInput) } if input.Kind == "tool_result" { if _, err := functionInput(payload); err != nil { return nil, nil, err } } - batch[i] = Input{Kind: input.Kind, Payload: payload} + batch[i] = sessions.Input{Kind: input.Kind, Payload: payload} } encoded, err := json.Marshal(batch) return batch, encoded, err } -func admitInput(ctx context.Context, q *sqlc.Queries, tenantID string, session pgtype.UUID, key string, position int32, input Input) (InputReceipt, error) { +func admitInput(ctx context.Context, q *sqlc.Queries, tenantID string, session pgtype.UUID, key string, position int32, input sessions.Input) (sessions.InputReceipt, error) { if input.Kind == "tool_result" { return admitFunctionResult(ctx, q, tenantID, session, key, position, input) } @@ -171,44 +141,44 @@ func admitInput(ctx context.Context, q *sqlc.Queries, tenantID string, session p } } if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } sequence, err := q.CreateTurnInput(ctx, sqlc.CreateTurnInputParams{ SessionID: session, TurnID: turn.ID, IdempotencyKey: key, Kind: input.Kind, Payload: input.Payload, BatchPosition: position, }) if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } if input.Kind == "cancel" && turn.ID.Valid { if err := requestTurnCancel(ctx, q, session, turn); err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } } if err := indexInput(ctx, q, session, sequence); err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } if created { // A new Turn publishes turn.created, then its user input Items, then the // Session activity, within this transaction. usage, err := sessionpg.LoadUsage(ctx, q, session) if err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } if err := sessionpg.AppendChanges(ctx, q, session, sessions.ActivityChange(turnFromRow(turn), usage, nil)); err != nil { - return InputReceipt{}, err + return sessions.InputReceipt{}, err } } return inputReceipt(sequence, turn.ID, false), nil } // ListTurnInputs is an internal ordered recovery query, not the public SSE stream. -func (s *Store) ListTurnInputs(ctx context.Context, tenantID, sessionID, turnID string, after int64, limit int) ([]TurnInput, error) { +func (s *Store) ListTurnInputs(ctx context.Context, tenantID, sessionID, turnID string, after int64, limit int) ([]sessions.TurnInput, error) { params, err := turnLookup(tenantID, sessionID, turnID) if err != nil { return nil, err } if after < 0 || limit < 1 || limit > 100 { - return nil, fmt.Errorf("%w: nonnegative cursor and page size 1..100 required", ErrInvalidInput) + return nil, fmt.Errorf("%w: nonnegative cursor and page size 1..100 required", sessions.ErrInvalidInput) } if _, err := s.GetTurn(ctx, tenantID, sessionID, turnID); err != nil { return nil, err @@ -219,15 +189,15 @@ func (s *Store) ListTurnInputs(ctx context.Context, tenantID, sessionID, turnID if err != nil { return nil, fmt.Errorf("list turn inputs: %w", err) } - inputs := make([]TurnInput, 0, len(rows)) + inputs := make([]sessions.TurnInput, 0, len(rows)) for _, row := range rows { - inputs = append(inputs, TurnInput{Sequence: row.Sequence, Kind: row.Kind, Payload: row.Payload, CreatedAt: row.CreatedAt.Time}) + inputs = append(inputs, sessions.TurnInput{Sequence: row.Sequence, Kind: row.Kind, Payload: row.Payload, CreatedAt: row.CreatedAt.Time}) } return inputs, nil } -func inputReceipt(sequence int64, turn pgtype.UUID, replayed bool) InputReceipt { - receipt := InputReceipt{Sequence: sequence, Replayed: replayed} +func inputReceipt(sequence int64, turn pgtype.UUID, replayed bool) sessions.InputReceipt { + receipt := sessions.InputReceipt{Sequence: sequence, Replayed: replayed} if turn.Valid { receipt.TurnID = uuid.UUID(turn.Bytes).String() } diff --git a/services/core/internal/store/turn_inputs_test.go b/services/core/internal/store/turn_inputs_test.go index 4b69b8484..aee9a919b 100644 --- a/services/core/internal/store/turn_inputs_test.go +++ b/services/core/internal/store/turn_inputs_test.go @@ -15,10 +15,10 @@ import ( var messagePayload = json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) -func newTurnSession(t *testing.T, s *Store) (string, Session) { +func newTurnSession(t *testing.T, s *Store) (string, sessions.Session) { t.Helper() tenant := uuid.NewString() - session, err := s.CreateSession(context.Background(), tenant, CreateSessionInput{Creator: FixtureCreator(), + session, err := s.CreateSession(context.Background(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "session", Configuration: json.RawMessage(`{"agent":{"model":"test","instructions":"original"}}`), }) if err != nil { @@ -27,7 +27,7 @@ func newTurnSession(t *testing.T, s *Store) (string, Session) { return tenant, session } -func submitMessage(t *testing.T, s *Store, tenant, session, key string) InputReceipt { +func submitMessage(t *testing.T, s *Store, tenant, session, key string) sessions.InputReceipt { t.Helper() receipt, err := s.SubmitMessage(context.Background(), tenant, session, key, messagePayload) if err != nil { @@ -38,7 +38,7 @@ func submitMessage(t *testing.T, s *Store, tenant, session, key string) InputRec func transition(t *testing.T, s *Store, tenant, session, turn, from, to string) sessions.Turn { t.Helper() - got, err := s.TransitionTurn(context.Background(), tenant, session, turn, TurnTransition{ExpectedStatus: from, Status: to}) + got, err := s.TransitionTurn(context.Background(), tenant, session, turn, sessions.TurnTransition{ExpectedStatus: from, Status: to}) if err != nil { t.Fatal(err) } @@ -54,7 +54,7 @@ func TestConcurrentInputsUseOneTurnAndOneRetryReceipt(t *testing.T) { for _, repeatedKey := range []bool{true, false} { t.Run(fmt.Sprintf("repeated-key-%v", repeatedKey), func(t *testing.T) { var wg sync.WaitGroup - receipts := make(chan InputReceipt, count) + receipts := make(chan sessions.InputReceipt, count) errs := make(chan error, count) for i := range count { wg.Add(1) @@ -120,10 +120,10 @@ func TestTurnInputRetriesAndRestart(t *testing.T) { if err != nil || !retry.Replayed || retry.Sequence != first.Sequence || retry.TurnID != first.TurnID { t.Fatalf("equivalent retry = %+v, %v", retry, err) } - if _, err := s.SubmitMessage(ctx, tenant, session.ID, "first", json.RawMessage(`{"text":"changed"}`)); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.SubmitMessage(ctx, tenant, session.ID, "first", json.RawMessage(`{"text":"changed"}`)); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatalf("changed payload accepted: %v", err) } - if _, err := s.RequestCancel(ctx, tenant, session.ID, "first"); !errors.Is(err, ErrIdempotencyConflict) { + if _, err := s.RequestCancel(ctx, tenant, session.ID, "first"); !errors.Is(err, sessions.ErrIdempotencyConflict) { t.Fatalf("changed input kind accepted: %v", err) } completed := transition(t, s, tenant, session.ID, first.TurnID, sessions.TurnInProgress, sessions.TurnCompleted) @@ -141,7 +141,7 @@ func TestTurnInputRetriesAndRestart(t *testing.T) { if err != nil || !reflect.DeepEqual(got, completed) { t.Fatalf("restart turn: %+v, %v", got, err) } - var all []TurnInput + var all []sessions.TurnInput var cursor int64 for { page, err := recovered.ListTurnInputs(ctx, tenant, session.ID, first.TurnID, cursor, 1) @@ -189,24 +189,24 @@ func TestTurnOperationsAreTenantAndSessionScoped(t *testing.T) { return err }, "transition": func() error { - _, err := s.TransitionTurn(ctx, scope.tenant, scope.session, first.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed}) + _, err := s.TransitionTurn(ctx, scope.tenant, scope.session, first.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed}) return err }, } { - if err := call(); !errors.Is(err, ErrNotFound) { + if err := call(); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("%s escaped scope: %v", name, err) } } } // Turn IDs cannot be used with another valid Session in the same tenant either. - second, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "second"}) + second, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "second"}) if err != nil { t.Fatal(err) } - if _, err := s.GetTurn(ctx, tenant, second.ID, first.TurnID); !errors.Is(err, ErrNotFound) { + if _, err := s.GetTurn(ctx, tenant, second.ID, first.TurnID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("cross-session turn read: %v", err) } - if _, err := s.TransitionTurn(ctx, tenant, second.ID, first.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed}); !errors.Is(err, ErrNotFound) { + if _, err := s.TransitionTurn(ctx, tenant, second.ID, first.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnFailed}); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("cross-session turn write: %v", err) } otherInput := submitMessage(t, s, otherTenant, other.ID, "input") diff --git a/services/core/internal/store/turn_reads.go b/services/core/internal/store/turn_reads.go index 3a4fd8fd7..0f9d06655 100644 --- a/services/core/internal/store/turn_reads.go +++ b/services/core/internal/store/turn_reads.go @@ -11,19 +11,14 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -type TurnPage struct { - Turns []sessions.Turn - NextCursor string -} - // ListTurns pages a Session's root Turns. Subagent Turns are not Session Turns; // ListSubagentTurns reads them. -func (s *Store) ListTurns(ctx context.Context, tenantID, sessionID, cursor string, limit int, ascending bool) (TurnPage, error) { +func (s *Store) ListTurns(ctx context.Context, tenantID, sessionID, cursor string, limit int, ascending bool) (sessions.TurnPage, error) { if limit < 1 || limit > 100 { - return TurnPage{}, fmt.Errorf("%w: page size must be 1..100", ErrInvalidInput) + return sessions.TurnPage{}, fmt.Errorf("%w: page size must be 1..100", sessions.ErrInvalidInput) } if _, err := s.GetSession(ctx, tenantID, sessionID); err != nil { - return TurnPage{}, err + return sessions.TurnPage{}, err } tenant, _ := parseID(tenantID) session, _ := parseID(sessionID) @@ -32,16 +27,16 @@ func (s *Store) ListTurns(ctx context.Context, tenantID, sessionID, cursor strin // A child Turn is not a Session Turn, so its ID is a missing cursor here. after, err := s.GetTurn(ctx, tenantID, sessionID, pgunit.LookupCursor(cursor)) if err != nil { - return TurnPage{}, err + return sessions.TurnPage{}, err } params.AfterCreated = pgtype.Timestamptz{Time: after.CreatedAt, Valid: true} params.AfterID, _ = parseID(after.ID) } rows, err := s.queries.ListRootTurns(ctx, params) if err != nil { - return TurnPage{}, fmt.Errorf("list turns: %w", err) + return sessions.TurnPage{}, fmt.Errorf("list turns: %w", err) } - page := TurnPage{Turns: make([]sessions.Turn, 0, min(limit, len(rows)))} + page := sessions.TurnPage{Turns: make([]sessions.Turn, 0, min(limit, len(rows)))} if len(rows) > limit { page.NextCursor = uuid.UUID(rows[limit-1].ID.Bytes).String() rows = rows[:limit] diff --git a/services/core/internal/store/turn_reads_test.go b/services/core/internal/store/turn_reads_test.go index e5877661b..7d0ced7aa 100644 --- a/services/core/internal/store/turn_reads_test.go +++ b/services/core/internal/store/turn_reads_test.go @@ -49,13 +49,13 @@ func TestTurnPaginationRetainsScopeAndOrder(t *testing.T) { } } otherTenant, otherSession := newTurnSession(t, s) - sameTenantSession, err := s.CreateSession(ctx, tenant, CreateSessionInput{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString()}) + sameTenantSession, err := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString()}) if err != nil { t.Fatal(err) } for _, scope := range [][2]string{{otherTenant, session.ID}, {tenant, otherSession.ID}, {tenant, uuid.NewString()}, {tenant, sameTenantSession.ID}} { _, err := s.ListTurns(ctx, scope[0], scope[1], ids[0], 1, true) - if !errors.Is(err, ErrNotFound) { + if !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign cursor/session accepted: %v", err) } } @@ -64,7 +64,7 @@ func TestTurnPaginationRetainsScopeAndOrder(t *testing.T) { t.Fatalf("empty session: %+v %v", empty, err) } for _, limit := range []int{0, 101} { - if _, err := s.ListTurns(ctx, tenant, session.ID, "", limit, false); !errors.Is(err, ErrInvalidInput) { + if _, err := s.ListTurns(ctx, tenant, session.ID, "", limit, false); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("limit accepted: %v", err) } } diff --git a/services/core/internal/store/turns.go b/services/core/internal/store/turns.go index 9389b3319..db40430a4 100644 --- a/services/core/internal/store/turns.go +++ b/services/core/internal/store/turns.go @@ -17,14 +17,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -var ErrTurnConflict = errors.New("turn state changed or cancellation was requested") - -type TurnTransition struct { - ExpectedStatus string - Status string - Outcome json.RawMessage -} - // GetTurn reads a root Turn. A Subagent Turn ID is not found here, exactly like // a missing one; GetSubagentTurn reads child Turns. func (s *Store) GetTurn(ctx context.Context, tenantID, sessionID, turnID string) (sessions.Turn, error) { @@ -34,7 +26,7 @@ func (s *Store) GetTurn(ctx context.Context, tenantID, sessionID, turnID string) } row, err := s.queries.GetTurn(ctx, params) if errors.Is(err, pgx.ErrNoRows) { - return sessions.Turn{}, ErrNotFound + return sessions.Turn{}, sessions.ErrNotFound } if err != nil { return sessions.Turn{}, fmt.Errorf("get turn: %w", err) @@ -45,20 +37,20 @@ func (s *Store) GetTurn(ctx context.Context, tenantID, sessionID, turnID string) // TransitionTurn is a compare-and-set for execution callbacks. Once terminal, // a Turn cannot be reopened or have its outcome overwritten, including by retries. // A dispatcher must claim queued -> in_progress before sending work to a daemon. -func (s *Store) TransitionTurn(ctx context.Context, tenantID, sessionID, turnID string, input TurnTransition) (sessions.Turn, error) { +func (s *Store) TransitionTurn(ctx context.Context, tenantID, sessionID, turnID string, input sessions.TurnTransition) (sessions.Turn, error) { params, err := turnLookup(tenantID, sessionID, turnID) if err != nil { return sessions.Turn{}, err } if !validTransition(input.ExpectedStatus, input.Status) || len(input.Outcome) > 512*1024 { - return sessions.Turn{}, fmt.Errorf("%w: invalid turn transition or outcome size", ErrInvalidInput) + return sessions.Turn{}, fmt.Errorf("%w: invalid turn transition or outcome size", sessions.ErrInvalidInput) } outcome, err := jsonobject.Normalize(input.Outcome) if err != nil { - return sessions.Turn{}, fmt.Errorf("%w: %w", ErrInvalidInput, err) + return sessions.Turn{}, fmt.Errorf("%w: %w", sessions.ErrInvalidInput, err) } if !sessions.TerminalStatus(input.Status) && string(outcome) != "{}" { - return sessions.Turn{}, fmt.Errorf("%w: outcome requires a terminal status", ErrInvalidInput) + return sessions.Turn{}, fmt.Errorf("%w: outcome requires a terminal status", sessions.ErrInvalidInput) } input.Outcome = outcome var row sqlc.Turn @@ -73,9 +65,9 @@ func (s *Store) TransitionTurn(ctx context.Context, tenantID, sessionID, turnID return turnFromRow(row), nil } -func transitionTurn(ctx context.Context, q *sqlc.Queries, params sqlc.GetTurnParams, input TurnTransition) (sqlc.Turn, error) { +func transitionTurn(ctx context.Context, q *sqlc.Queries, params sqlc.GetTurnParams, input sessions.TurnTransition) (sqlc.Turn, error) { if _, err := q.GetTurn(ctx, params); errors.Is(err, pgx.ErrNoRows) { - return sqlc.Turn{}, ErrNotFound + return sqlc.Turn{}, sessions.ErrNotFound } else if err != nil { return sqlc.Turn{}, err } @@ -89,7 +81,7 @@ func transitionTurn(ctx context.Context, q *sqlc.Queries, params sqlc.GetTurnPar NewStatus: input.Status, Outcome: input.Outcome, }) if errors.Is(err, pgx.ErrNoRows) { - return sqlc.Turn{}, ErrTurnConflict + return sqlc.Turn{}, sessions.ErrTurnConflict } if err != nil { return sqlc.Turn{}, err diff --git a/services/core/internal/store/turns_test.go b/services/core/internal/store/turns_test.go index 4a6646db4..9b435ed49 100644 --- a/services/core/internal/store/turns_test.go +++ b/services/core/internal/store/turns_test.go @@ -33,7 +33,7 @@ func TestCancellationStaysBoundToItsOriginalTurn(t *testing.T) { } transition(t, s, tenant, session.ID, first.TurnID, sessions.TurnInProgress, sessions.TurnCancelled) next := submitMessage(t, s, tenant, session.ID, "next") - for key, original := range map[string]InputReceipt{"cancel": cancel, "idle-cancel": idle} { + for key, original := range map[string]sessions.InputReceipt{"cancel": cancel, "idle-cancel": idle} { retry, err := s.RequestCancel(ctx, tenant, session.ID, key) if err != nil || !retry.Replayed || retry.TurnID != original.TurnID || retry.Sequence != original.Sequence { t.Fatalf("cancellation retargeted: %+v, %v", retry, err) @@ -50,7 +50,7 @@ func TestCancellationStaysBoundToItsOriginalTurn(t *testing.T) { if err != nil || stopped.Status != sessions.TurnCancelled || stopped.CompletedAt.IsZero() || !stopped.StartedAt.IsZero() { t.Fatalf("queued work did not stop: %+v, %v", stopped, err) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, next.TurnID, TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); !errors.Is(err, ErrTurnConflict) { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, next.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("cancelled queued work was started: %v", err) } } @@ -74,7 +74,7 @@ func TestWaitingTurnRetainsInputsAndStartTime(t *testing.T) { if _, err := s.RequestCancel(ctx, tenant, session.ID, "cancel"); err != nil { t.Fatal(err) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, first.TurnID, TurnTransition{ExpectedStatus: sessions.TurnWaiting, Status: sessions.TurnInProgress}); !errors.Is(err, ErrTurnConflict) { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, first.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnWaiting, Status: sessions.TurnInProgress}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("cancelling Turn resumed: %v", err) } transition(t, s, tenant, session.ID, first.TurnID, sessions.TurnWaiting, sessions.TurnCancelled) @@ -98,7 +98,7 @@ func TestTerminalOutcomeIsImmutableDuringConcurrentCallbacks(t *testing.T) { go func() { defer wg.Done() outcome, _ := json.Marshal(map[string]string{"reported": status}) - turn, err := other.TransitionTurn(ctx, tenant, session.ID, first.TurnID, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: status, Outcome: outcome}) + turn, err := other.TransitionTurn(ctx, tenant, session.ID, first.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: status, Outcome: outcome}) if err == nil { winners <- turn } else { @@ -113,7 +113,7 @@ func TestTerminalOutcomeIsImmutableDuringConcurrentCallbacks(t *testing.T) { t.Fatalf("winners=%d errors=%d", len(winners), len(errs)) } for err := range errs { - if !errors.Is(err, ErrTurnConflict) { + if !errors.Is(err, sessions.ErrTurnConflict) { t.Fatal(err) } } @@ -121,7 +121,7 @@ func TestTerminalOutcomeIsImmutableDuringConcurrentCallbacks(t *testing.T) { if winner.CompletedAt.IsZero() || winner.CancelRequestedAt.IsZero() || winner.CompletedAt.Before(winner.StartedAt) { t.Fatalf("terminal timestamps: %+v", winner) } - if _, err := s.TransitionTurn(ctx, tenant, session.ID, first.TurnID, TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"late":true}`)}); !errors.Is(err, ErrTurnConflict) { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, first.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"late":true}`)}); !errors.Is(err, sessions.ErrTurnConflict) { t.Fatalf("late terminal callback accepted: %v", err) } pool.Close() @@ -137,7 +137,7 @@ func TestTurnInputValidationHasNoSideEffects(t *testing.T) { tenant, session := newTurnSession(t, s) ctx := context.Background() for _, raw := range []json.RawMessage{nil, json.RawMessage(`[]`), json.RawMessage(`null`), json.RawMessage(`{} {}`), json.RawMessage(`{"text":"` + string(make([]byte, 512*1024)) + `"}`)} { - if _, err := s.SubmitMessage(ctx, tenant, session.ID, "first", raw); !errors.Is(err, ErrInvalidInput) { + if _, err := s.SubmitMessage(ctx, tenant, session.ID, "first", raw); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("invalid input accepted: %v", err) } } @@ -150,13 +150,13 @@ func TestTurnInputValidationHasNoSideEffects(t *testing.T) { if first.Replayed { t.Fatal("failed submission persisted a receipt") } - for _, input := range []TurnTransition{ + for _, input := range []sessions.TurnTransition{ {ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnCompleted}, {ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnQueued}, {ExpectedStatus: sessions.TurnCompleted, Status: sessions.TurnInProgress}, {ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress, Outcome: json.RawMessage(`{"premature":true}`)}, } { - if _, err := s.TransitionTurn(ctx, tenant, session.ID, first.TurnID, input); !errors.Is(err, ErrInvalidInput) { + if _, err := s.TransitionTurn(ctx, tenant, session.ID, first.TurnID, input); !errors.Is(err, sessions.ErrInvalidInput) { t.Fatalf("invalid transition accepted: %v", err) } } diff --git a/services/core/internal/store/worker_input_race_test.go b/services/core/internal/store/worker_input_race_test.go index 34d15890b..c24a1a287 100644 --- a/services/core/internal/store/worker_input_race_test.go +++ b/services/core/internal/store/worker_input_race_test.go @@ -50,7 +50,7 @@ func TestWorkerInputReadSkipsConcurrentlyCancelledCandidate(t *testing.T) { mutated <- h.s.CommitLegacyDeletion(t.Context(), h.tenant, candidateSession) return } - _, err := h.s.SubmitInputs(t.Context(), h.tenant, candidateSession, "cancel", []store.Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}}) + _, err := h.s.SubmitInputs(t.Context(), h.tenant, candidateSession, "cancel", []sessions.Input{{Kind: "cancel", Payload: json.RawMessage(`{}`)}}) mutated <- err }} instrumented, err := pgxpool.NewWithConfig(t.Context(), cfg) @@ -85,7 +85,7 @@ func TestWorkerInputReadSkipsConcurrentlyCancelledCandidate(t *testing.T) { t.Fatal("candidate was not cancelled", err) } // A later Session must still execute through this same Worker. - h.session, err = h.s.CreateSession(ctx, h.tenant, store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "healthy", Configuration: h.session.Configuration}) + h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "healthy", Configuration: h.session.Configuration}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/worker_lease_loss_test.go b/services/core/internal/store/worker_lease_loss_test.go index 38c6ee3b0..83ae5d02f 100644 --- a/services/core/internal/store/worker_lease_loss_test.go +++ b/services/core/internal/store/worker_lease_loss_test.go @@ -30,7 +30,7 @@ func TestWorkerLeaseLossLeavesUncertainWorkForSuccessor(t *testing.T) { t.Error("worker did not stop") } }) - inputs := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"execute"}]}]}`)}} + inputs := []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"execute"}]}]}`)}} receipts, err := worker.SubmitInputs(t.Context(), h.tenant, h.session.ID, "active", inputs) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/worker_preparation_failure_test.go b/services/core/internal/store/worker_preparation_failure_test.go index fb1b22394..7c9dfd50b 100644 --- a/services/core/internal/store/worker_preparation_failure_test.go +++ b/services/core/internal/store/worker_preparation_failure_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) func TestWorkerSettlesConfirmedPreparationFailureAndAcceptsNewInput(t *testing.T) { @@ -15,7 +15,7 @@ func TestWorkerSettlesConfirmedPreparationFailureAndAcceptsNewInput(t *testing.T h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), false) enableWorkerEnvironment(t, h) frames := workerFrames(t, h) - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "first", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "first", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) if err != nil { t.Fatal(err) } @@ -31,7 +31,7 @@ func TestWorkerSettlesConfirmedPreparationFailureAndAcceptsNewInput(t *testing.T } awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "failed reservation settlement", func() bool { current, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, h.session.ID, pending.ID) - return err == nil && current.State == store.EnvironmentInputFailed + return err == nil && current.State == sessions.EnvironmentInputFailed }) session, err := h.s.GetSession(t.Context(), h.tenant, h.session.ID) if err != nil || session.PendingInput || session.LastTurn != nil || session.EnvironmentInputActivity == nil || session.EnvironmentInputActivity.Failure != "runtime_preparation_failed" { @@ -58,7 +58,7 @@ func TestWorkerSettlesConfirmedPreparationFailureAndAcceptsNewInput(t *testing.T t.Fatal("failed input retried", frame.Type) case <-time.After(1200 * time.Millisecond): } - next, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "next", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"next"}`)}}) + next, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "next", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"next"}`)}}) if err != nil { t.Fatal("new input remained blocked", err) } @@ -71,7 +71,7 @@ func TestWorkerSettlesConfirmedPreparationFailureAndAcceptsNewInput(t *testing.T t.Fatal("new input was not admitted") } current, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, h.session.ID, next.ID) - if err != nil || current.State != store.EnvironmentInputAdmitted { + if err != nil || current.State != sessions.EnvironmentInputAdmitted { t.Fatal("new input state", err) } h.write(start.RunID, proto.TypeDone, proto.DonePayload{Content: "complete"}) @@ -98,7 +98,7 @@ func TestWorkerRetriesUncertainPreparationFailure(t *testing.T) { h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), false) enableWorkerEnvironment(t, h) frames := workerFrames(t, h) - pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "retry", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"retry"}`)}}) + pending, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "retry", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"retry"}`)}}) if err != nil { t.Fatal(err) } @@ -116,7 +116,7 @@ func TestWorkerRetriesUncertainPreparationFailure(t *testing.T) { } nextWorkerFrame(t, frames, proto.TypeExecutionPrepare) current, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, h.session.ID, pending.ID) - if err != nil || current.State != store.EnvironmentInputPending || !current.Deadline.Equal(pending.Deadline) { + if err != nil || current.State != sessions.EnvironmentInputPending || !current.Deadline.Equal(pending.Deadline) { t.Fatal("transient failure settled or extended input", err) } if _, err := h.s.CancelEnvironmentInput(t.Context(), h.tenant, h.session.ID, pending.ID); err != nil { @@ -130,7 +130,7 @@ func TestWorkerPreparationRejectionPreservesCancellationAndNewerInput(t *testing h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), false) enableWorkerEnvironment(t, h) frames := workerFrames(t, h) - first, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "first", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) + first, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "first", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"first"}`)}}) if err != nil { t.Fatal(err) } @@ -140,14 +140,14 @@ func TestWorkerPreparationRejectionPreservesCancellationAndNewerInput(t *testing if _, err := h.s.CancelEnvironmentInput(t.Context(), h.tenant, h.session.ID, first.ID); err != nil { t.Fatal(err) } - next, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "next", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"next"}`)}}) + next, err := h.s.ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "next", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"next"}`)}}) if err != nil { t.Fatal(err) } rejection := proto.PreparationStatusPayload{State: "rejected", Operation: proto.TypeExecutionPrepare, ErrorCode: "unsupported_configuration"} h.write(old.ID, proto.TypePreparationStatus, rejection) prepare := nextWorkerFrame(t, frames, proto.TypeExecutionPrepare) - for id, state := range map[string]string{first.ID: store.EnvironmentInputCancelled, next.ID: store.EnvironmentInputPending} { + for id, state := range map[string]string{first.ID: sessions.EnvironmentInputCancelled, next.ID: sessions.EnvironmentInputPending} { current, err := h.s.GetEnvironmentInputReservation(t.Context(), h.tenant, h.session.ID, id) if err != nil || current.State != state { t.Fatal("late rejection changed cancellation or newer input", err) diff --git a/services/core/internal/store/worker_wakeup_test.go b/services/core/internal/store/worker_wakeup_test.go index a8be6fcca..1b3813e19 100644 --- a/services/core/internal/store/worker_wakeup_test.go +++ b/services/core/internal/store/worker_wakeup_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" @@ -61,12 +62,12 @@ func TestWorkerSchedulerCommittedAdmissionWakesBeforeMaintenance(t *testing.T) { t.Error("worker did not stop") } }() - input := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"wake"}`)}} + input := []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"wake"}`)}} switch operation { case "submit": _, err = worker.SubmitInputs(ctx, h.tenant, h.session.ID, "wake", input) case "create", "stream": - creation := store.CreateSessionInput{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "wake", Configuration: h.session.Configuration, InitialInputs: input} + creation := sessions.CreateSession{Creator: store.FixtureCreator(), Engine: "codex", IdempotencyKey: "wake", Configuration: h.session.Configuration, InitialInputs: input} if operation == "create" { _, err = worker.CreateSession(ctx, h.tenant, creation) } else { @@ -118,7 +119,7 @@ func TestWorkerSchedulerHintBypassesEnvironmentScanThrottle(t *testing.T) { admitted := make(chan error, 1) started := time.Now() go func() { - _, err := worker.SubmitInputs(ctx, h.tenant, h.session.ID, "wake", []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"wake"}`)}}) + _, err := worker.SubmitInputs(ctx, h.tenant, h.session.ID, "wake", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"wake"}`)}}) admitted <- err }() prepare := nextWorkerFrame(t, frames, proto.TypeExecutionPrepare) diff --git a/services/core/tests/fixtures/items.go b/services/core/tests/fixtures/items.go index c37d3ca27..afd89e75a 100644 --- a/services/core/tests/fixtures/items.go +++ b/services/core/tests/fixtures/items.go @@ -8,7 +8,7 @@ import ( ) func observeItems(ctx context.Context, s *store.Store, tenant, session, turn, status string) error { - events := []store.ExecutionEvent{ + events := []sessions.ExecutionEvent{ {Kind: "delta", Payload: json.RawMessage(`{"item_id":"answer","delta":"partial answer"}`)}, {Kind: "tool_call", Payload: json.RawMessage(`{"id":"command","stage":"after","observation":{"status":"failed","kind":"command","command":"exit 7","cwd":"/workspace","output":"command failed","exit_code":7,"duration_ms":8}}`)}, {Kind: "tool_call", Payload: json.RawMessage(`{"id":"mcp","stage":"after","observation":{"status":"completed","kind":"mcp","server":"reference","name":"lookup","arguments":{"n":9007199254740993},"output":{"structuredContent":{"n":9007199254740993}},"error":null}}`)}, @@ -17,7 +17,7 @@ func observeItems(ctx context.Context, s *store.Store, tenant, session, turn, st {Kind: "tool_call", Payload: json.RawMessage(`{"id":"search","stage":"after","observation":{"status":"completed","kind":"web_search","action":{"type":"search","query":"reference"}}}`)}, } if status == sessions.TurnCompleted || status == sessions.TurnFailed { - events = append(events, store.ExecutionEvent{Kind: "output_message", Payload: json.RawMessage(`{"id":"answer","status":"completed","text":"final answer","phase":"final_answer"}`)}) + events = append(events, sessions.ExecutionEvent{Kind: "output_message", Payload: json.RawMessage(`{"id":"answer","status":"completed","text":"final answer","phase":"final_answer"}`)}) } return s.AppendTurnEvents(ctx, tenant, session, turn, 1, events) } diff --git a/services/core/tests/fixtures/main.go b/services/core/tests/fixtures/main.go index 7d6bc6074..9c59768db 100644 --- a/services/core/tests/fixtures/main.go +++ b/services/core/tests/fixtures/main.go @@ -58,7 +58,7 @@ func seed() error { if err != nil { return err } - if _, err = s.TransitionTurn(ctx, f.Tenant, f.Session, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { + if _, err = s.TransitionTurn(ctx, f.Tenant, f.Session, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { return err } if err = observeItems(ctx, s, f.Tenant, f.Session, receipt.TurnID, status); err != nil { @@ -66,7 +66,7 @@ func seed() error { } if status != sessions.TurnInProgress { outcome := json.RawMessage(`{"error":"SECRET engine log","done":{"metadata":{"agent_session_id":"PRIVATE"}}}`) - if _, err = s.TransitionTurn(ctx, f.Tenant, f.Session, receipt.TurnID, store.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: status, Outcome: outcome}); err != nil { + if _, err = s.TransitionTurn(ctx, f.Tenant, f.Session, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: status, Outcome: outcome}); err != nil { return err } }