From d56bee16762148a7da0d82526a1866ec3cd4de27 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 19:44:18 +0000 Subject: [PATCH] Move the Session vocabulary and errors out of store into sessions Sessions, Turns, inputs, Environments and their provisioning failures, function calls, Item and Artifact reads, executor credentials and the Session errors move from store to sessions. api maps them in writeSessionsError, cursor errors become sessions.CursorError, and the Sessions, Admission and SessionHistory interfaces split into SessionCreation, SessionAdmission, InputAdmission, Turns and Items. AdminValidationError had no producer and is deleted. --- contracts/agents-api/core.openapi.yaml | 66 ++--- services/core/IMPLEMENTATION.md | 2 +- services/core/cmd/environment-key/main.go | 11 +- .../core/cmd/environment-key/main_test.go | 10 +- services/core/cmd/server/http_routes_test.go | 21 +- services/core/cmd/server/main.go | 21 +- services/core/internal/api/admin_resources.go | 3 +- .../core/internal/api/admin_resources_test.go | 4 +- .../internal/api/admin_session_archive.go | 12 +- .../api/admin_session_archive_test.go | 14 +- services/core/internal/api/admin_summary.go | 15 +- .../core/internal/api/capability_archive.go | 6 +- .../internal/api/claude_admission_test.go | 6 +- .../internal/api/core_validation_errors.go | 5 - services/core/internal/api/dependencies.go | 30 ++- .../core/internal/api/dependencies_test.go | 46 +++- .../internal/api/environment_creation_test.go | 22 +- .../api/environment_executor_management.go | 14 +- .../environment_executor_management_test.go | 26 +- .../core/internal/api/environment_files.go | 8 +- .../internal/api/environment_files_create.go | 14 +- .../api/environment_files_create_test.go | 4 +- .../internal/api/environment_files_query.go | 6 +- .../internal/api/environment_files_test.go | 18 +- .../api/environment_files_write_test.go | 4 +- .../internal/api/environment_input_test.go | 8 +- .../internal/api/environment_installation.go | 12 +- .../api/environment_installation_test.go | 10 +- .../internal/api/environment_network_test.go | 4 +- .../core/internal/api/environment_plugins.go | 10 +- .../internal/api/environment_plugins_test.go | 4 +- .../api/environment_preparation_input.go | 10 +- .../core/internal/api/environment_request.go | 10 +- .../core/internal/api/environment_setup.go | 20 +- .../api/environment_skill_selectors_test.go | 4 +- .../core/internal/api/environment_skills.go | 16 +- services/core/internal/api/environments.go | 12 +- .../core/internal/api/environments_test.go | 14 +- services/core/internal/api/errors.go | 73 +----- services/core/internal/api/errors_sessions.go | 82 ++++++ services/core/internal/api/errors_test.go | 25 +- services/core/internal/api/fakes_test.go | 244 +++++++++--------- services/core/internal/api/function_inputs.go | 42 +-- .../core/internal/api/function_inputs_test.go | 18 +- .../core/internal/api/function_state_test.go | 3 +- services/core/internal/api/handler.go | 42 +-- services/core/internal/api/handler_test.go | 27 +- services/core/internal/api/harness_test.go | 6 +- .../core/internal/api/hosted_environment.go | 34 +-- .../internal/api/hosted_environment_test.go | 20 +- .../core/internal/api/hosted_failure_test.go | 11 +- services/core/internal/api/initial_files.go | 14 +- services/core/internal/api/inputs.go | 36 ++- services/core/internal/api/inputs_test.go | 8 +- services/core/internal/api/items.go | 13 +- services/core/internal/api/items_test.go | 8 +- .../native_classification_integration_test.go | 7 +- .../core/internal/api/resource_query_test.go | 14 +- .../core/internal/api/runtime_history_test.go | 4 +- .../internal/api/sandbox_deployment_setup.go | 3 +- .../internal/api/sandbox_selector_test.go | 8 +- .../internal/api/session_admission_test.go | 3 +- .../core/internal/api/session_artifacts.go | 12 +- .../internal/api/session_artifacts_test.go | 18 +- .../internal/api/session_creation_identity.go | 10 +- .../internal/api/session_creation_stream.go | 13 +- .../api/session_creation_stream_test.go | 80 +++--- .../internal/api/session_credentials_test.go | 12 +- .../core/internal/api/session_deletion.go | 4 +- .../core/internal/api/session_diagnostics.go | 7 +- .../api/session_diagnostics_deadline_test.go | 7 +- .../session_diagnostics_public_compat_test.go | 5 +- .../internal/api/session_diagnostics_test.go | 21 +- .../api/session_environment_http_test.go | 9 +- .../internal/api/session_environment_test.go | 21 +- .../session_execution_configuration_test.go | 4 +- .../internal/api/session_initial_input.go | 8 +- .../api/session_initial_input_test.go | 2 +- services/core/internal/api/session_request.go | 20 +- .../core/internal/api/session_response.go | 5 +- .../internal/api/session_semantics_test.go | 8 +- .../core/internal/api/session_template.go | 12 +- .../internal/api/session_write_audit_test.go | 12 +- .../internal/api/source_files_errors_test.go | 23 -- services/core/internal/api/stream.go | 11 +- .../internal/api/stream_authority_test.go | 3 +- services/core/internal/api/stream_test.go | 19 +- services/core/internal/api/subagents_test.go | 6 +- .../internal/api/tool_search_response_test.go | 4 +- services/core/internal/api/turns.go | 14 +- services/core/internal/api/turns_test.go | 15 +- .../internal/api/validation_errors_test.go | 12 +- services/core/internal/api/write_audit.go | 4 +- .../archive_cancellation_cleanup_test.go | 7 +- services/core/internal/execution/artifacts.go | 5 +- .../deployment_provider_observations_test.go | 10 +- .../internal/execution/device_authority.go | 3 +- .../execution/directory_preparation.go | 6 +- .../execution/directory_preparation_test.go | 8 +- .../internal/execution/disabled_tools_test.go | 6 +- .../core/internal/execution/dispatcher.go | 8 +- .../core/internal/execution/engine_profile.go | 14 +- .../internal/execution/engine_profile_test.go | 10 +- .../execution/environment_admission.go | 32 +-- .../environment_capabilities_test.go | 14 +- .../execution/environment_directory.go | 11 +- .../execution/environment_file_write.go | 22 +- .../execution/environment_placement.go | 22 +- .../execution/environment_placement_test.go | 28 +- .../execution/executor_preparation.go | 4 +- .../execution/function_images_test.go | 6 +- services/core/internal/execution/functions.go | 6 +- .../core/internal/execution/functions_test.go | 6 +- services/core/internal/execution/journal.go | 12 +- .../core/internal/execution/journal_test.go | 10 +- .../internal/execution/mcp_support_test.go | 10 +- .../core/internal/execution/message_input.go | 12 +- .../internal/execution/message_support.go | 6 +- .../execution/message_support_test.go | 22 +- .../internal/execution/model_execution.go | 4 +- .../execution/model_execution_test.go | 8 +- .../core/internal/execution/preparation.go | 6 +- .../internal/execution/prepared_dispatch.go | 15 +- .../core/internal/execution/recovery_test.go | 4 +- services/core/internal/execution/request.go | 4 +- .../internal/execution/runtime_compute.go | 3 +- .../execution/runtime_compute_wake.go | 7 +- .../internal/execution/runtime_connections.go | 11 +- .../execution/runtime_initialization.go | 22 +- .../internal/execution/runtime_lifecycle.go | 3 +- .../core/internal/execution/runtime_setup.go | 8 +- .../internal/execution/runtime_setup_test.go | 4 +- .../internal/execution/runtime_wake_hint.go | 4 +- .../core/internal/execution/sandbox_reset.go | 3 +- .../internal/execution/session_archive.go | 4 +- .../execution/structured_output_test.go | 4 +- services/core/internal/execution/support.go | 18 +- services/core/internal/execution/worker.go | 24 +- .../core/internal/execution/worker_device.go | 27 +- .../internal/execution/worker_schedule.go | 17 +- .../core/internal/execution/worker_wakeup.go | 4 +- .../execution/workspace_images_test.go | 12 +- .../modelconfigurationpg/fixture_test.go | 7 +- .../internal/runtimeenrollment/connection.go | 24 +- .../runtimeenrollment/connection_test.go | 28 +- .../internal/runtimeenrollment/enrollment.go | 5 +- .../runtimeenrollment/enrollment_test.go | 5 +- .../runtimehistory/storeresolver/resolver.go | 4 +- .../storeresolver/resolver_test.go | 14 +- .../runtimeobs/storeresolver/resolver.go | 5 +- .../runtimeobs/storeresolver/resolver_test.go | 31 +-- .../providers/configuration_flow_test.go | 19 +- services/core/internal/sessions/doc.go | 10 +- .../core/internal/sessions/environment.go | 83 +++++- services/core/internal/sessions/errors.go | 75 ++++++ .../internal/sessions/executor_credentials.go | 48 ++++ services/core/internal/sessions/inputs.go | 53 ++++ .../core/internal/sessions/provisioning.go | 107 ++++++++ .../internal/sessions/provisioning_test.go | 27 ++ services/core/internal/sessions/reads.go | 50 ++++ services/core/internal/sessions/session.go | 75 ++++++ services/core/internal/sessions/turn.go | 39 +++ .../internal/store/admin_delete_audit_test.go | 4 +- .../internal/store/admin_session_archive.go | 39 ++- .../store/admin_session_archive_race_test.go | 5 +- .../store/admin_session_archive_test.go | 30 +-- .../admin_session_archive_worker_http_test.go | 6 +- services/core/internal/store/admin_summary.go | 11 +- .../core/internal/store/admin_validation.go | 12 - .../store/archive_cancellation_test.go | 6 +- .../core/internal/store/artifact_capture.go | 18 +- .../core/internal/store/artifact_lifecycle.go | 10 +- .../internal/store/claude_execution_test.go | 18 +- .../core/internal/store/claude_mcp_test.go | 3 +- .../internal/store/command_output_test.go | 16 +- services/core/internal/store/core_metrics.go | 3 +- .../core/internal/store/core_metrics_test.go | 4 +- .../creation_stream_settlement_public_test.go | 6 +- .../store/credential_matrix_http_test.go | 5 +- .../deployment_model_providers_http_test.go | 9 +- services/core/internal/store/devices.go | 65 ++--- services/core/internal/store/devices_test.go | 13 +- services/core/internal/store/dispatch_test.go | 20 +- .../store/environment_admission_test.go | 26 +- .../store/environment_claim_worker_test.go | 5 +- .../store/environment_connection_recovery.go | 3 +- .../environment_connection_recovery_test.go | 6 +- .../environment_connection_worker_test.go | 3 +- .../internal/store/environment_connections.go | 12 +- .../store/environment_connections_test.go | 6 +- .../internal/store/environment_device_test.go | 7 +- .../environment_directory_active_test.go | 3 +- .../store/environment_directory_test.go | 16 +- .../environment_executor_command_test.go | 7 +- .../store/environment_executor_credentials.go | 55 ++-- .../environment_executor_credentials_test.go | 28 +- .../store/environment_executor_management.go | 59 ++--- .../environment_executor_management_test.go | 39 +-- .../store/environment_expiry_dispatch_test.go | 8 +- .../store/environment_expiry_worker_test.go | 15 +- .../internal/store/environment_file_writes.go | 90 +++---- .../store/environment_file_writes_test.go | 45 ++-- .../store/environment_initial_input_test.go | 36 +-- .../store/environment_initial_public_test.go | 7 +- .../store/environment_initialization.go | 39 ++- .../store/environment_initialization_test.go | 15 +- .../store/environment_input_activity.go | 8 +- .../store/environment_input_activity_test.go | 30 +-- .../store/environment_input_claim_test.go | 6 +- .../store/environment_input_expiry_test.go | 20 +- .../store/environment_input_migration_test.go | 3 +- .../environment_input_settlement_test.go | 50 ++-- .../core/internal/store/environment_inputs.go | 141 ++++------ .../internal/store/environment_inputs_test.go | 72 +++--- .../store/environment_installation.go | 37 +-- .../store/environment_installation_test.go | 13 +- .../store/environment_plugins_test.go | 5 +- .../store/environment_runtime_fixture_test.go | 5 +- .../core/internal/store/environment_setup.go | 11 +- .../internal/store/environment_setup_test.go | 9 +- .../store/environment_skill_references.go | 15 +- .../environment_skill_references_test.go | 9 +- .../internal/store/environment_skills_test.go | 5 +- .../store/environment_steering_order_test.go | 8 +- .../internal/store/environment_work_test.go | 3 +- .../store/environment_worker_helpers_test.go | 10 +- .../store/environment_worker_scan_test.go | 5 +- .../internal/store/environment_worker_test.go | 6 +- .../store/environment_write_audit_test.go | 2 +- services/core/internal/store/environments.go | 37 +-- .../core/internal/store/environments_test.go | 29 ++- .../core/internal/store/execution_test.go | 22 +- .../store/executor_credential_target.go | 9 +- .../executor_principals_migration_test.go | 10 +- .../store/executor_principals_test.go | 29 ++- services/core/internal/store/export_test.go | 3 +- .../core/internal/store/function_calls.go | 40 ++- .../internal/store/function_calls_test.go | 38 +-- .../store/function_execution_native_test.go | 2 +- .../internal/store/function_execution_test.go | 8 +- .../store/function_input_execution_test.go | 5 +- .../core/internal/store/function_inputs.go | 49 ++-- .../store/function_inputs_public_test.go | 10 +- .../internal/store/function_inputs_test.go | 54 ++-- .../store/function_item_events_test.go | 6 +- .../core/internal/store/function_results.go | 23 +- .../store/function_state_public_test.go | 6 +- .../internal/store/function_state_test.go | 6 +- .../internal/store/function_worker_test.go | 4 +- ...sted_initialization_failure_public_test.go | 35 +-- services/core/internal/store/initial_files.go | 13 +- .../core/internal/store/initial_files_test.go | 7 +- .../core/internal/store/input_batches_test.go | 26 +- .../store/input_conflicts_public_test.go | 12 +- .../store/item_order_migration_test.go | 4 +- .../core/internal/store/item_order_test.go | 18 +- services/core/internal/store/item_reads.go | 14 +- .../core/internal/store/item_reads_test.go | 32 +-- .../internal/store/item_serialization_test.go | 8 +- .../internal/store/list_cursor_public_test.go | 14 +- services/core/internal/store/list_cursors.go | 24 +- .../store/local_artifact_export_test.go | 10 +- .../store/local_environment_devices.go | 19 +- .../store/local_environment_devices_test.go | 13 +- .../local_environment_file_write_test.go | 10 +- .../store/local_environment_worker_test.go | 12 +- .../mcp_credential_selection_public_test.go | 2 +- .../store/message_image_admission_test.go | 11 +- .../internal/store/native_environment_test.go | 2 +- .../internal/store/native_recovery_test.go | 2 +- .../internal/store/no_environment_test.go | 2 +- .../store/path_id_semantics_public_test.go | 3 +- .../store/prepared_dispatch_failure_test.go | 10 +- .../internal/store/prepared_dispatch_test.go | 10 +- .../store/provisioning_failure_detail.go | 36 --- .../internal/store/public_execution_test.go | 10 +- .../store/public_handler_fixture_test.go | 34 ++- .../store/request_body_public_test.go | 5 +- .../store/runtime_allocation_state.go | 12 +- .../internal/store/runtime_allocations.go | 11 +- .../store/runtime_allocations_test.go | 11 +- .../runtime_capabilities_pending_test.go | 5 +- .../store/runtime_compute_lifecycle_test.go | 3 +- .../internal/store/runtime_connection_test.go | 4 +- .../core/internal/store/runtime_deployment.go | 7 +- .../internal/store/runtime_deployment_test.go | 15 +- .../core/internal/store/runtime_enrollment.go | 7 +- .../runtime_enrollment_connection_test.go | 3 +- .../internal/store/runtime_enrollment_test.go | 11 +- .../store/runtime_environment_terminal.go | 52 +--- .../runtime_environment_terminal_test.go | 43 +-- .../store/runtime_file_admission_test.go | 18 +- .../store/runtime_history_acceptance_test.go | 3 +- .../internal/store/runtime_idle_clock_test.go | 10 +- .../internal/store/runtime_initialization.go | 5 +- .../store/runtime_initialization_test.go | 9 +- .../store/runtime_input_admission_test.go | 9 +- .../internal/store/runtime_lifecycle_nodes.go | 3 +- .../store/runtime_lifecycle_nodes_test.go | 5 +- .../internal/store/runtime_lifecycle_test.go | 5 +- .../store/runtime_node_generations_test.go | 3 +- .../runtime_node_lifecycle_fixture_test.go | 7 +- services/core/internal/store/runtime_nodes.go | 4 +- .../core/internal/store/runtime_nodes_test.go | 15 +- .../internal/store/runtime_observation.go | 3 +- .../store/runtime_observation_test.go | 3 +- .../internal/store/runtime_pending_test.go | 3 +- .../core/internal/store/runtime_suspension.go | 21 +- .../runtime_suspension_concurrency_test.go | 18 +- .../internal/store/runtime_suspension_test.go | 27 +- .../runtime_wake_hint_integration_test.go | 21 +- .../store/runtime_worker_recovery_test.go | 8 +- .../store/sandbox_deployment_switch_test.go | 12 +- .../store/sandbox_deployment_worker_test.go | 3 +- services/core/internal/store/sandbox_reset.go | 9 +- .../core/internal/store/sandbox_reset_test.go | 2 +- .../store/sandbox_specification_store_test.go | 15 +- services/core/internal/store/scheduling.go | 41 ++- .../store/self_hosted_cancel_public_test.go | 15 +- .../store/self_hosted_initial_public_test.go | 30 ++- .../store/session_agent_filter_test.go | 8 +- .../core/internal/store/session_artifacts.go | 55 ++-- .../store/session_artifacts_public_test.go | 4 +- .../internal/store/session_artifacts_test.go | 48 ++-- .../store/session_configuration_test.go | 14 +- .../store/session_creation_identity.go | 39 +-- .../store/session_creation_identity_test.go | 22 +- .../internal/store/session_creation_stream.go | 20 +- .../store/session_creation_stream_test.go | 18 +- .../internal/store/session_creator_test.go | 21 +- .../core/internal/store/session_deletion.go | 8 +- .../store/session_deletion_execution_test.go | 29 +-- .../session_deletion_lifecycle_public_test.go | 18 +- .../internal/store/session_deletion_test.go | 50 ++-- .../internal/store/session_diagnostics.go | 34 +-- .../store/session_diagnostics_test.go | 32 +-- .../store/session_environment_snapshot.go | 3 +- .../session_environment_snapshot_test.go | 14 +- .../core/internal/store/session_events.go | 10 +- .../internal/store/session_events_test.go | 20 +- .../store/session_execution_configuration.go | 11 +- .../session_execution_configuration_test.go | 15 +- .../internal/store/session_initial_input.go | 9 +- .../store/session_initial_input_test.go | 14 +- .../core/internal/store/session_metadata.go | 11 +- .../internal/store/session_metadata_test.go | 22 +- .../store/session_model_execution_test.go | 5 +- .../store/session_model_options_test.go | 3 +- .../internal/store/session_transaction.go | 5 +- .../internal/store/session_write_audit.go | 3 +- .../store/session_write_audit_test.go | 13 +- services/core/internal/store/sessions.go | 135 +++------- services/core/internal/store/sessions_test.go | 15 +- .../store/skill_version_deletion_test.go | 3 +- .../store/stream_authority_http_test.go | 5 +- .../store/structured_output_dispatch_test.go | 2 +- .../internal/store/subagent_coordination.go | 14 +- .../internal/store/subagent_dispatch_test.go | 4 +- .../internal/store/subagent_identities.go | 9 +- .../store/subagent_identities_test.go | 34 +-- .../store/subagent_item_projection.go | 28 +- .../internal/store/subagent_item_reads.go | 7 +- .../core/internal/store/subagent_lifecycle.go | 8 +- .../store/subagent_native_outputs_test.go | 6 +- .../core/internal/store/subagent_reads.go | 12 +- .../internal/store/subagent_resources_test.go | 30 +-- .../store/subagent_turn_projection.go | 18 +- .../store/subagent_visibility_public_test.go | 15 +- .../store/template_composition_public_test.go | 3 +- .../template_null_selection_public_test.go | 3 +- .../store/token_usage_integration_test.go | 36 +-- .../core/internal/store/turn_completion.go | 20 +- services/core/internal/store/turn_events.go | 49 ++-- .../core/internal/store/turn_events_test.go | 30 +-- services/core/internal/store/turn_inputs.go | 98 +++---- .../core/internal/store/turn_inputs_test.go | 26 +- services/core/internal/store/turn_reads.go | 17 +- .../core/internal/store/turn_reads_test.go | 6 +- services/core/internal/store/turns.go | 24 +- services/core/internal/store/turns_test.go | 18 +- .../internal/store/worker_input_race_test.go | 4 +- .../internal/store/worker_lease_loss_test.go | 2 +- .../store/worker_preparation_failure_test.go | 20 +- .../core/internal/store/worker_wakeup_test.go | 7 +- services/core/tests/fixtures/items.go | 4 +- services/core/tests/fixtures/main.go | 4 +- 386 files changed, 3534 insertions(+), 3258 deletions(-) create mode 100644 services/core/internal/api/errors_sessions.go create mode 100644 services/core/internal/sessions/errors.go create mode 100644 services/core/internal/sessions/executor_credentials.go create mode 100644 services/core/internal/sessions/inputs.go create mode 100644 services/core/internal/sessions/provisioning.go create mode 100644 services/core/internal/sessions/provisioning_test.go create mode 100644 services/core/internal/sessions/reads.go create mode 100644 services/core/internal/sessions/session.go delete mode 100644 services/core/internal/store/admin_validation.go delete mode 100644 services/core/internal/store/provisioning_failure_detail.go 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 } }