diff --git a/.github/scripts/builder-confirmed-tool-units.py b/.github/scripts/builder-confirmed-tool-units.py index 779f4466495..1ca396d17c3 100644 --- a/.github/scripts/builder-confirmed-tool-units.py +++ b/.github/scripts/builder-confirmed-tool-units.py @@ -192,6 +192,7 @@ FILES += [ FILES += [ "api/tests/unit_tests/services/dify_builder/test_knowledge_inventory.py", "api/tests/unit_tests/services/dify_builder/test_knowledge_discovery.py", + "api/tests/unit_tests/services/dify_builder/test_knowledge_source_cue.py", ] SELECTED_GUARDS = [ "api/tests/unit_tests/core/dify_builder/test_handlers_edit.py::test_sufficient_goal_reaches_plan_gate_or_actionable_retry_with_monotonic_progress", @@ -2664,13 +2665,27 @@ a4_expected.update( "test_reconfirmation_rechecks_permission_even_after_preference_was_recorded": 1, "test_first_decline_asks_explicit_goal_change_instead_of_approving": 2, "test_knowledge_catalogue_failure_without_selected_id_cannot_approve": 1, + "test_model_only_confirmation_does_not_ask_to_waive_an_explicitly_excluded_knowledge_goal": 1, + "test_mixed_or_uncertain_source_cannot_bypass_handler_with_false_model_hint": 8, "test_refusing_goal_change_keeps_knowledge_wait_and_never_replans": 1, "test_explicit_goal_change_is_traceable_and_allows_no_knowledge_plan": 1, "test_forged_or_old_decision_field_does_not_modify_goal": 1, "test_public_description_is_bounded_escaped_data_in_same_discovery_call": 1, "test_replaying_resolved_scope_card_does_not_reauthorize_or_replan": 1, "test_decision_is_rejected_after_goal_or_actor_changes": 1, - "test_real_discovery_empty_or_failed_source_cannot_authorize_omission": 4 + "test_real_discovery_empty_or_failed_source_cannot_authorize_omission": 4, + "test_excluded_resource_list_reaches_formal_plan_without_a_knowledge_waiver": 3, + "test_excluded_resource_list_never_disables_server_or_model_source_guards": 3 + }, + "tests.unit_tests.services.dify_builder.test_knowledge_source_cue": { + "test_explicitly_excluded_source_does_not_create_a_knowledge_goal": 11, + "test_affirmative_mixed_or_uncertain_source_still_requires_clarification": 29, + "test_requirement_keys_and_nontext_values_do_not_invent_source_use": 6, + "test_affirmative_requirement_overrides_goal_exclusion": 5, + "test_captured_agent_only_goal_and_forbidden_resource_list_do_not_invent_knowledge_use": 3, + "test_direct_add_exclusion_only_discounts_known_resource_categories": 5, + "test_explicit_resource_exclusion_list_keeps_its_role_when_nested": 3, + "test_exclusion_lists_never_hide_positive_unknown_or_node_only_requirements": 14 }, "tests.unit_tests.services.dify_builder.test_resources": { "test_list_tenant_resources_normalizes_all_three_kinds": 1, diff --git a/api/tests/unit_tests/controllers/console/test_dify_builder_lifecycle_routes.py b/api/tests/unit_tests/controllers/console/test_dify_builder_lifecycle_routes.py index 6a3d6b89ae7..5a1ee0e9ec1 100644 --- a/api/tests/unit_tests/controllers/console/test_dify_builder_lifecycle_routes.py +++ b/api/tests/unit_tests/controllers/console/test_dify_builder_lifecycle_routes.py @@ -99,7 +99,7 @@ def test_fix_control_get_uses_owner_and_returns_only_correlated_receipt( ) -> None: service = _route_service() calls = [] - receipt = { + receipt: dict[str, object] = { "session_id": "s1", "operation": operation, "client_action_id": "decision-1", @@ -110,7 +110,7 @@ def test_fix_control_get_uses_owner_and_returns_only_correlated_receipt( "acknowledged": True, } - def read(session_id, actor, **query): + def read(session_id: str, actor: Actor, **query: object) -> dict[str, object]: calls.append((session_id, actor, query)) return receipt @@ -175,7 +175,9 @@ def test_real_http_action_and_fix_receipt_share_decision_namespace(request_sql_w ) assert prepared.status_code == 200, prepared.get_data(as_text=True) intent_id = prepared.get_json()["fix_intent_id"] - assert intents.find(actor, _SESSION_ID, raw_intake).id == intent_id + stored_intent = intents.find(actor, _SESSION_ID, raw_intake) + assert stored_intent is not None + assert stored_intent.id == intent_id intake_read = world.client.get( url + "/fix-intents", query_string={ diff --git a/api/tests/unit_tests/services/dify_builder/test_fix_intents.py b/api/tests/unit_tests/services/dify_builder/test_fix_intents.py index 656fed77f49..0add937a055 100644 --- a/api/tests/unit_tests/services/dify_builder/test_fix_intents.py +++ b/api/tests/unit_tests/services/dify_builder/test_fix_intents.py @@ -5,6 +5,7 @@ from datetime import datetime from uuid import uuid4 import pytest +from typing_extensions import TypedDict from core.dify_builder.contract import RunContextCard from core.dify_builder.errors import ConflictError, NotFoundError @@ -24,6 +25,14 @@ pytest_plugins = ["tests.unit_tests.services.dify_builder.test_service"] ACTOR = Actor(account_id=ACCOUNT_ID, tenant_id=TENANT_ID) +class _FixControlQuery(TypedDict, closed=True): + operation: str + client_action_id: str + base_version: int + fix_intent_id: str + expected_task_id: str + + def seed(repo: SqlDifyBuilderRepository, state: PcState = PcState.BUILD_PLAN_APPROVAL) -> Session: session = Session( app_id=APP_ID, @@ -619,7 +628,7 @@ def test_control_receipt_reads_exact_intake_and_decision_without_dispatch( kind="dismiss_fix", client_action_id="lost-dismiss", ) - decision = { + decision: _FixControlQuery = { "operation": "dismiss_fix", "client_action_id": "lost-dismiss", "base_version": 1, @@ -627,13 +636,15 @@ def test_control_receipt_reads_exact_intake_and_decision_without_dispatch( "expected_task_id": f"{session.id}:0", } assert service.get_fix_control_receipt(session.id, ACTOR, **decision)["acknowledged"] is True - for forged in ( - {"client_action_id": "other"}, - {"expected_task_id": "other-task"}, - {"operation": "stop_and_switch_fix"}, - ): + forged_action = decision.copy() + forged_action["client_action_id"] = "other" + forged_task = decision.copy() + forged_task["expected_task_id"] = "other-task" + forged_operation = decision.copy() + forged_operation["operation"] = "stop_and_switch_fix" + for forged in (forged_action, forged_task, forged_operation): with pytest.raises((ConflictError, NotFoundError)): - service.get_fix_control_receipt(session.id, ACTOR, **{**decision, **forged}) + service.get_fix_control_receipt(session.id, ACTOR, **forged) with pytest.raises(NotFoundError): service.get_fix_control_receipt(session.id, replace(ACTOR, account_id=str(uuid4())), **decision) with pytest.raises(NotFoundError): @@ -661,7 +672,7 @@ def test_switch_control_receipt_remains_unacknowledged_until_activation( kind="stop_and_switch_fix", client_action_id="lost-switch", ) - query = { + query: _FixControlQuery = { "operation": "stop_and_switch_fix", "client_action_id": "lost-switch", "base_version": 1, diff --git a/packages/contracts/generated/api/console/dify-builder/orpc.gen.ts b/packages/contracts/generated/api/console/dify-builder/orpc.gen.ts index 221d8d7f16e..8b96cd08f30 100644 --- a/packages/contracts/generated/api/console/dify-builder/orpc.gen.ts +++ b/packages/contracts/generated/api/console/dify-builder/orpc.gen.ts @@ -6,6 +6,9 @@ import { zGetDifyBuilderSessionsBySessionIdConversationPath, zGetDifyBuilderSessionsBySessionIdConversationQuery, zGetDifyBuilderSessionsBySessionIdConversationResponse, + zGetDifyBuilderSessionsBySessionIdFixIntentsPath, + zGetDifyBuilderSessionsBySessionIdFixIntentsQuery, + zGetDifyBuilderSessionsBySessionIdFixIntentsResponse, zGetDifyBuilderSessionsBySessionIdNativeTestInputPath, zGetDifyBuilderSessionsBySessionIdNativeTestInputResponse, zGetDifyBuilderSessionsBySessionIdNativeTestsByRequestIdPath, @@ -20,9 +23,18 @@ import { zGetDifyBuilderSessionsBySessionIdStreamResponse, zPostDifyBuilderAgentPingResponse, zPostDifyBuilderSessionsBody, + zPostDifyBuilderSessionsBySessionIdAbandonBody, + zPostDifyBuilderSessionsBySessionIdAbandonPath, + zPostDifyBuilderSessionsBySessionIdAbandonResponse, zPostDifyBuilderSessionsBySessionIdActionsBody, zPostDifyBuilderSessionsBySessionIdActionsPath, zPostDifyBuilderSessionsBySessionIdActionsResponse, + zPostDifyBuilderSessionsBySessionIdClearBody, + zPostDifyBuilderSessionsBySessionIdClearPath, + zPostDifyBuilderSessionsBySessionIdClearResponse, + zPostDifyBuilderSessionsBySessionIdFixIntentsBody, + zPostDifyBuilderSessionsBySessionIdFixIntentsPath, + zPostDifyBuilderSessionsBySessionIdFixIntentsResponse, zPostDifyBuilderSessionsBySessionIdMessagesBody, zPostDifyBuilderSessionsBySessionIdMessagesPath, zPostDifyBuilderSessionsBySessionIdMessagesResponse, @@ -75,6 +87,26 @@ export const agent = { } export const post2 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postDifyBuilderSessionsBySessionIdAbandon', + path: '/dify-builder/sessions/{session_id}/abandon', + tags: ['console'], + }) + .input( + z.object({ + body: zPostDifyBuilderSessionsBySessionIdAbandonBody, + params: zPostDifyBuilderSessionsBySessionIdAbandonPath, + }), + ) + .output(zPostDifyBuilderSessionsBySessionIdAbandonResponse) + +export const abandon = { + post: post2, +} + +export const post3 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -91,7 +123,27 @@ export const post2 = oc .output(eventIterator(zPostDifyBuilderSessionsBySessionIdActionsResponse)) export const actions = { - post: post2, + post: post3, +} + +export const post4 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postDifyBuilderSessionsBySessionIdClear', + path: '/dify-builder/sessions/{session_id}/clear', + tags: ['console'], + }) + .input( + z.object({ + body: zPostDifyBuilderSessionsBySessionIdClearBody, + params: zPostDifyBuilderSessionsBySessionIdClearPath, + }), + ) + .output(zPostDifyBuilderSessionsBySessionIdClearResponse) + +export const clear = { + post: post4, } export const get = oc @@ -114,7 +166,44 @@ export const conversation = { get, } -export const post3 = oc +export const get2 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getDifyBuilderSessionsBySessionIdFixIntents', + path: '/dify-builder/sessions/{session_id}/fix-intents', + tags: ['console'], + }) + .input( + z.object({ + params: zGetDifyBuilderSessionsBySessionIdFixIntentsPath, + query: zGetDifyBuilderSessionsBySessionIdFixIntentsQuery, + }), + ) + .output(zGetDifyBuilderSessionsBySessionIdFixIntentsResponse) + +export const post5 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postDifyBuilderSessionsBySessionIdFixIntents', + path: '/dify-builder/sessions/{session_id}/fix-intents', + tags: ['console'], + }) + .input( + z.object({ + body: zPostDifyBuilderSessionsBySessionIdFixIntentsBody, + params: zPostDifyBuilderSessionsBySessionIdFixIntentsPath, + }), + ) + .output(zPostDifyBuilderSessionsBySessionIdFixIntentsResponse) + +export const fixIntents = { + get: get2, + post: post5, +} + +export const post6 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -131,7 +220,7 @@ export const post3 = oc .output(eventIterator(zPostDifyBuilderSessionsBySessionIdMessagesResponse)) export const messages = { - post: post3, + post: post6, } /** @@ -139,7 +228,7 @@ export const messages = { * * @deprecated */ -export const get2 = oc +export const get3 = oc .route({ deprecated: true, description: @@ -154,7 +243,7 @@ export const get2 = oc .output(zGetDifyBuilderSessionsBySessionIdNativeTestInputResponse) export const nativeTestInput = { - get: get2, + get: get3, } /** @@ -162,7 +251,7 @@ export const nativeTestInput = { * * @deprecated */ -export const post4 = oc +export const post7 = oc .route({ deprecated: true, description: @@ -182,7 +271,7 @@ export const post4 = oc .output(zPostDifyBuilderSessionsBySessionIdNativeTestsByRequestIdCancelResponse) export const cancel = { - post: post4, + post: post7, } /** @@ -190,7 +279,7 @@ export const cancel = { * * @deprecated */ -export const post5 = oc +export const post8 = oc .route({ deprecated: true, description: @@ -210,7 +299,7 @@ export const post5 = oc .output(zPostDifyBuilderSessionsBySessionIdNativeTestsByRequestIdDecisionsResponse) export const decisions = { - post: post5, + post: post8, } /** @@ -218,7 +307,7 @@ export const decisions = { * * @deprecated */ -export const post6 = oc +export const post9 = oc .route({ deprecated: true, description: @@ -238,7 +327,7 @@ export const post6 = oc .output(zPostDifyBuilderSessionsBySessionIdNativeTestsByRequestIdResumeResponse) export const resume = { - post: post6, + post: post9, } /** @@ -246,7 +335,7 @@ export const resume = { * * @deprecated */ -export const get3 = oc +export const get4 = oc .route({ deprecated: true, description: @@ -261,7 +350,7 @@ export const get3 = oc .output(zGetDifyBuilderSessionsBySessionIdNativeTestsByRequestIdResponse) export const byRequestId = { - get: get3, + get: get4, cancel, decisions, resume, @@ -272,7 +361,7 @@ export const byRequestId = { * * @deprecated */ -export const post7 = oc +export const post10 = oc .route({ deprecated: true, description: @@ -292,11 +381,11 @@ export const post7 = oc .output(zPostDifyBuilderSessionsBySessionIdNativeTestsResponse) export const nativeTests = { - post: post7, + post: post10, byRequestId, } -export const post8 = oc +export const post11 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -313,10 +402,10 @@ export const post8 = oc .output(zPostDifyBuilderSessionsBySessionIdRealCallConsentsByConsentIdConfirmResponse) export const confirm = { - post: post8, + post: post11, } -export const post9 = oc +export const post12 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -334,10 +423,10 @@ export const post9 = oc .output(zPostDifyBuilderSessionsBySessionIdRealCallConsentsByConsentIdExecuteResponse) export const execute = { - post: post9, + post: post12, } -export const get4 = oc +export const get5 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -349,12 +438,12 @@ export const get4 = oc .output(zGetDifyBuilderSessionsBySessionIdRealCallConsentsByConsentIdResponse) export const byConsentId = { - get: get4, + get: get5, confirm, execute, } -export const post10 = oc +export const post13 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -372,11 +461,11 @@ export const post10 = oc .output(zPostDifyBuilderSessionsBySessionIdRealCallConsentsResponse) export const realCallConsents = { - post: post10, + post: post13, byConsentId, } -export const post11 = oc +export const post14 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -393,10 +482,10 @@ export const post11 = oc .output(zPostDifyBuilderSessionsBySessionIdResumeResponse) export const resume2 = { - post: post11, + post: post14, } -export const post12 = oc +export const post15 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -413,10 +502,10 @@ export const post12 = oc .output(zPostDifyBuilderSessionsBySessionIdStopResponse) export const stop = { - post: post12, + post: post15, } -export const get5 = oc +export const get6 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -434,10 +523,10 @@ export const get5 = oc .output(eventIterator(zGetDifyBuilderSessionsBySessionIdStreamResponse)) export const stream = { - get: get5, + get: get6, } -export const get6 = oc +export const get7 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -449,9 +538,12 @@ export const get6 = oc .output(zGetDifyBuilderSessionsBySessionIdResponse) export const bySessionId = { - get: get6, + get: get7, + abandon, actions, + clear, conversation, + fixIntents, messages, nativeTestInput, nativeTests, @@ -461,7 +553,7 @@ export const bySessionId = { stream, } -export const post13 = oc +export const post16 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -473,7 +565,7 @@ export const post13 = oc .output(eventIterator(zPostDifyBuilderSessionsResponse)) export const sessions = { - post: post13, + post: post16, bySessionId, } diff --git a/packages/contracts/generated/api/console/dify-builder/types.gen.ts b/packages/contracts/generated/api/console/dify-builder/types.gen.ts index e4653bb543d..af92441ddb8 100644 --- a/packages/contracts/generated/api/console/dify-builder/types.gen.ts +++ b/packages/contracts/generated/api/console/dify-builder/types.gen.ts @@ -68,6 +68,7 @@ export type DifyBuilderSessionViewResponse = { app_revision?: DifyBuilderAppRevisionResponse | null attachments?: Array canvas_read_only: boolean + conversation_cleared?: boolean conversation_last_seq: number decision?: Decision | null diagnostics?: Array<{ @@ -75,16 +76,38 @@ export type DifyBuilderSessionViewResponse = { }> interrupted: boolean last_command_id?: string + lifecycle_settlement?: 'pending' | 'settled' | 'unknown' | null model?: SessionModel | null + pending_fix_context?: RunContextCard | PreflightContextCard | null phase: Phase recovery?: RecoveryRef | null reply_language: string run_status: RunStatus session_id: string + task_id?: string + task_status?: 'abandoned' | 'active' | 'completed' | 'paused' + task_summaries?: Array test_execution?: NativeTestPublicReceipt | null version: number } +export type DifyBuilderConversationLifecyclePayload = { + base_version: number + client_action_id: string + expected_task_id?: string +} + +export type DifyBuilderConversationLifecycleResponse = { + conversation_cleared: boolean + conversation_last_seq: number + last_command_id: string + session_id: string + settlement: 'pending' | 'settled' | 'unknown' + task_id: string + task_status: 'abandoned' + version: number +} + export type DifyBuilderSubmitActionPayload = { action_id: string base_app_revision: string @@ -96,6 +119,13 @@ export type DifyBuilderSubmitActionPayload = { receipt_snapshot?: DifyBuilderReceiptSnapshot | null } +export type DifyBuilderClearConversationPayload = { + base_version: number + client_action_id: string + confirmed: boolean + expected_task_id?: string +} + export type DifyBuilderConversationPageResponse = { data: Array< | DifyBuilderUserConversationItemResponse @@ -115,6 +145,31 @@ export type DifyBuilderConversationPageResponse = { last_seq: number | null } +export type DifyBuilderFixControlResponse = { + acknowledged: boolean + base_version: number + client_action_id: string + fix_intent_id: string + operation: 'dismiss_fix' | 'fix' | 'stop_and_switch_fix' + session_id: string + status: string + target_task_id: string +} + +export type DifyBuilderFixIntentPayload = { + base_version: number + checklist_errors?: Array + client_action_id: string + failed_run_id?: string | null + model_config?: SessionModel | null +} + +export type DifyBuilderFixIntentResponse = { + fix_intent_id: string + session: DifyBuilderSessionViewResponse + status: string +} + export type DifyBuilderSubmitMessagePayload = { attachments?: Array base_version: number @@ -302,18 +357,22 @@ export type DifyBuilderCreateEditSessionPayload = { export type DifyBuilderCreateFixSessionPayload = { app_id: string attachments?: Array + client_action_id?: string | null failed_run_id: string model_config?: SessionModel | null scenario: 'fix' + session_id?: string | null } export type DifyBuilderCreateChecklistFixSessionPayload = { app_id: string attachments?: Array checklist_errors: Array + client_action_id?: string | null failed_run_id?: string | null model_config?: SessionModel | null scenario: 'fix' + session_id?: string | null } export type DifyBuilderCommandStartedEventResponse = { @@ -439,6 +498,20 @@ export type SessionModel = { provider: string } +export type RunContextCard = { + error_code: string + message: string + run_id: string + title: string + trace_ref?: string +} + +export type PreflightContextCard = { + issue_count: number + issues?: Array + node_count: number +} + export type Phase = | 'clarify' | 'complete' @@ -464,6 +537,24 @@ export type RunStatus = | 'waiting_confirmation' | 'waiting_input' +export type TaskSummary = { + change_summary?: string + changed_node_ids?: Array + completion_version?: number + constraints?: { + [key: string]: string + } + entry_mode?: EntryMode + goal?: string + outcome?: 'kept_draft' | 'published' + output_contract?: string + resource_ids?: Array + schema_version?: 1 + task_id?: string + test_evidence?: TaskTestEvidence + unresolved_risks?: Array +} + export type NativeTestPublicReceipt = { can_resume?: boolean cancellation?: CancellationReceipt | null @@ -573,6 +664,15 @@ export type DifyBuilderTestResultConversationItemResponse = { seq: number } +export type DifyBuilderChecklistErrorPayload = { + messages: Array + node_id: string + node_type: string + plugin_missing: boolean + title: string + unconnected: boolean +} + export type DifyBuilderAttachmentPayload = { upload_file_id: string } @@ -629,15 +729,6 @@ export type DifyBuilderConsoleForbiddenResponse = { status: 403 } -export type DifyBuilderChecklistErrorPayload = { - messages: Array - node_id: string - node_type: string - plugin_missing: boolean - title: string - unconnected: boolean -} - export type DifyBuilderCommandStartedEventData = { command_id: string phase: Phase @@ -834,19 +925,25 @@ export type DifyBuilderCommandFinishedEventData = { attachments?: Array canvas_read_only: boolean command_id: string + conversation_cleared?: boolean conversation_last_seq: number decision?: Decision | null diagnostics?: Array<{ [key: string]: unknown }> interrupted: boolean + lifecycle_settlement?: 'pending' | 'settled' | 'unknown' | null model?: SessionModel | null + pending_fix_context?: RunContextCard | PreflightContextCard | null phase: Phase post_canvas_action_id?: string | null recovery?: RecoveryRef | null reply_language: string run_status: RunStatus session_id: string + task_id?: string + task_status?: 'abandoned' | 'active' | 'completed' | 'paused' + task_summaries?: Array test_execution?: NativeTestPublicReceipt | null version: number } @@ -913,6 +1010,30 @@ export type Action = { } } +export type PreflightIssue = { + messages?: Array + node_id: string + node_type: string + plugin_missing?: boolean + title: string + unconnected?: boolean +} + +export type EntryMode = 'build' | 'edit' | 'fix' | 'fix_checklist' + +export type TaskTestEvidence = { + business_acceptance_case_id?: string + business_acceptance_state?: 'failed' | 'passed' | 'unverified' + execution_revision?: string + graph_revision?: string + mode?: string + native_run_id?: string + run_id?: string + safety_outcome?: string + sealed?: boolean + status?: 'failed' | 'not_run' | 'running' | 'stopped' | 'succeeded' | 'unknown' +} + export type CancellationReceipt = { generation: number operation_states: { @@ -973,20 +1094,6 @@ export type NoticeItem = { tone?: string } -export type RunContextCard = { - error_code: string - message: string - run_id: string - title: string - trace_ref?: string -} - -export type PreflightContextCard = { - issue_count: number - issues?: Array - node_count: number -} - export type AssistantTurnItem = { card_state?: string | null cards?: Array @@ -1543,15 +1650,6 @@ export type InteractionResponseField = { value: unknown } -export type PreflightIssue = { - messages?: Array - node_id: string - node_type: string - plugin_missing?: boolean - title: string - unconnected?: boolean -} - export type ExecutionProgress = { activities?: Array status: 'completed' | 'error' | 'running' | 'stopped' @@ -2108,6 +2206,29 @@ export type GetDifyBuilderSessionsBySessionIdResponses = { export type GetDifyBuilderSessionsBySessionIdResponse = GetDifyBuilderSessionsBySessionIdResponses[keyof GetDifyBuilderSessionsBySessionIdResponses] +export type PostDifyBuilderSessionsBySessionIdAbandonData = { + body: DifyBuilderConversationLifecyclePayload + path: { + session_id: string + } + query?: never + url: '/dify-builder/sessions/{session_id}/abandon' +} + +export type PostDifyBuilderSessionsBySessionIdAbandonErrors = { + default: ErrorResponse +} + +export type PostDifyBuilderSessionsBySessionIdAbandonError = + PostDifyBuilderSessionsBySessionIdAbandonErrors[keyof PostDifyBuilderSessionsBySessionIdAbandonErrors] + +export type PostDifyBuilderSessionsBySessionIdAbandonResponses = { + 200: DifyBuilderConversationLifecycleResponse +} + +export type PostDifyBuilderSessionsBySessionIdAbandonResponse = + PostDifyBuilderSessionsBySessionIdAbandonResponses[keyof PostDifyBuilderSessionsBySessionIdAbandonResponses] + export type PostDifyBuilderSessionsBySessionIdActionsData = { body: DifyBuilderSubmitActionPayload path: { @@ -2134,6 +2255,29 @@ export type PostDifyBuilderSessionsBySessionIdActionsResponses = { export type PostDifyBuilderSessionsBySessionIdActionsResponse = PostDifyBuilderSessionsBySessionIdActionsResponses[keyof PostDifyBuilderSessionsBySessionIdActionsResponses] +export type PostDifyBuilderSessionsBySessionIdClearData = { + body: DifyBuilderClearConversationPayload + path: { + session_id: string + } + query?: never + url: '/dify-builder/sessions/{session_id}/clear' +} + +export type PostDifyBuilderSessionsBySessionIdClearErrors = { + default: ErrorResponse +} + +export type PostDifyBuilderSessionsBySessionIdClearError = + PostDifyBuilderSessionsBySessionIdClearErrors[keyof PostDifyBuilderSessionsBySessionIdClearErrors] + +export type PostDifyBuilderSessionsBySessionIdClearResponses = { + 200: DifyBuilderConversationLifecycleResponse +} + +export type PostDifyBuilderSessionsBySessionIdClearResponse = + PostDifyBuilderSessionsBySessionIdClearResponses[keyof PostDifyBuilderSessionsBySessionIdClearResponses] + export type GetDifyBuilderSessionsBySessionIdConversationData = { body?: never path: { @@ -2163,6 +2307,61 @@ export type GetDifyBuilderSessionsBySessionIdConversationResponses = { export type GetDifyBuilderSessionsBySessionIdConversationResponse = GetDifyBuilderSessionsBySessionIdConversationResponses[keyof GetDifyBuilderSessionsBySessionIdConversationResponses] +export type GetDifyBuilderSessionsBySessionIdFixIntentsData = { + body?: never + path: { + session_id: string + } + query: { + base_version: number + client_action_id: string + expected_task_id?: string + fix_intent_id?: string + operation: 'dismiss_fix' | 'fix' | 'stop_and_switch_fix' + } + url: '/dify-builder/sessions/{session_id}/fix-intents' +} + +export type GetDifyBuilderSessionsBySessionIdFixIntentsErrors = { + 400: DifyBuilderErrorResponse | ErrorResponse + 404: DifyBuilderErrorResponse | ErrorResponse + 409: DifyBuilderErrorResponse | ErrorResponse + default: ErrorResponse +} + +export type GetDifyBuilderSessionsBySessionIdFixIntentsError = + GetDifyBuilderSessionsBySessionIdFixIntentsErrors[keyof GetDifyBuilderSessionsBySessionIdFixIntentsErrors] + +export type GetDifyBuilderSessionsBySessionIdFixIntentsResponses = { + 200: DifyBuilderFixControlResponse +} + +export type GetDifyBuilderSessionsBySessionIdFixIntentsResponse = + GetDifyBuilderSessionsBySessionIdFixIntentsResponses[keyof GetDifyBuilderSessionsBySessionIdFixIntentsResponses] + +export type PostDifyBuilderSessionsBySessionIdFixIntentsData = { + body: DifyBuilderFixIntentPayload + path: { + session_id: string + } + query?: never + url: '/dify-builder/sessions/{session_id}/fix-intents' +} + +export type PostDifyBuilderSessionsBySessionIdFixIntentsErrors = { + default: ErrorResponse +} + +export type PostDifyBuilderSessionsBySessionIdFixIntentsError = + PostDifyBuilderSessionsBySessionIdFixIntentsErrors[keyof PostDifyBuilderSessionsBySessionIdFixIntentsErrors] + +export type PostDifyBuilderSessionsBySessionIdFixIntentsResponses = { + 200: DifyBuilderFixIntentResponse +} + +export type PostDifyBuilderSessionsBySessionIdFixIntentsResponse = + PostDifyBuilderSessionsBySessionIdFixIntentsResponses[keyof PostDifyBuilderSessionsBySessionIdFixIntentsResponses] + export type PostDifyBuilderSessionsBySessionIdMessagesData = { body: DifyBuilderSubmitMessagePayload path: { diff --git a/packages/contracts/generated/api/console/dify-builder/zod.gen.ts b/packages/contracts/generated/api/console/dify-builder/zod.gen.ts index f3b917364e3..be6a382c225 100644 --- a/packages/contracts/generated/api/console/dify-builder/zod.gen.ts +++ b/packages/contracts/generated/api/console/dify-builder/zod.gen.ts @@ -18,6 +18,55 @@ export const zErrorResponse = z.object({ status: z.int(), }) +/** + * DifyBuilderConversationLifecyclePayload + * + * Lifecycle control uses Session CAS, never a graph revision. + */ +export const zDifyBuilderConversationLifecyclePayload = z.object({ + base_version: z.int().gte(1), + client_action_id: z.string().min(1).max(128), + expected_task_id: z.string().max(255).optional().default(''), +}) + +/** + * DifyBuilderConversationLifecycleResponse + */ +export const zDifyBuilderConversationLifecycleResponse = z.object({ + conversation_cleared: z.boolean(), + conversation_last_seq: z.int(), + last_command_id: z.string(), + session_id: z.string(), + settlement: z.enum(['pending', 'settled', 'unknown']), + task_id: z.string(), + task_status: z.literal('abandoned'), + version: z.int(), +}) + +/** + * DifyBuilderClearConversationPayload + */ +export const zDifyBuilderClearConversationPayload = z.object({ + base_version: z.int().gte(1), + client_action_id: z.string().min(1).max(128), + confirmed: z.boolean(), + expected_task_id: z.string().max(255).optional().default(''), +}) + +/** + * DifyBuilderFixControlResponse + */ +export const zDifyBuilderFixControlResponse = z.object({ + acknowledged: z.boolean(), + base_version: z.int(), + client_action_id: z.string(), + fix_intent_id: z.string(), + operation: z.enum(['dismiss_fix', 'fix', 'stop_and_switch_fix']), + session_id: z.string(), + status: z.string(), + target_task_id: z.string(), +}) + /** * NativeTestStartPayload */ @@ -220,6 +269,17 @@ export const zSessionModel = z.object({ provider: z.string(), }) +/** + * RunContextCard + */ +export const zRunContextCard = z.object({ + error_code: z.string(), + message: z.string(), + run_id: z.string(), + title: z.string(), + trace_ref: z.string().optional().default(''), +}) + /** * Phase * @@ -264,6 +324,39 @@ export const zRunStatus = z.enum([ 'waiting_input', ]) +/** + * DifyBuilderRunContextConversationItemResponse + */ +export const zDifyBuilderRunContextConversationItemResponse = z.object({ + at_version: z.int(), + kind: z.literal('run_context'), + payload: zRunContextCard, + seq: z.int(), +}) + +/** + * DifyBuilderChecklistErrorPayload + */ +export const zDifyBuilderChecklistErrorPayload = z.object({ + messages: z.array(z.string()), + node_id: z.string(), + node_type: z.string(), + plugin_missing: z.boolean(), + title: z.string(), + unconnected: z.boolean(), +}) + +/** + * DifyBuilderFixIntentPayload + */ +export const zDifyBuilderFixIntentPayload = z.object({ + base_version: z.int().gte(1), + checklist_errors: z.array(zDifyBuilderChecklistErrorPayload).max(100).optional(), + client_action_id: z.string().min(1).max(128), + failed_run_id: z.string().min(1).nullish(), + model_config: zSessionModel.nullish(), +}) + /** * DifyBuilderAttachmentPayload * @@ -312,11 +405,39 @@ export const zDifyBuilderCreateEditSessionPayload = z.object({ export const zDifyBuilderCreateFixSessionPayload = z.object({ app_id: z.string().min(1), attachments: z.array(zDifyBuilderAttachmentPayload).max(5).optional(), + client_action_id: z.string().min(1).max(128).nullish(), failed_run_id: z.string().min(1), model_config: zSessionModel.nullish(), scenario: z.literal('fix'), + session_id: z.string().min(1).nullish(), }) +/** + * DifyBuilderCreateChecklistFixSessionPayload + */ +export const zDifyBuilderCreateChecklistFixSessionPayload = z.object({ + app_id: z.string().min(1), + attachments: z.array(zDifyBuilderAttachmentPayload).max(5).optional(), + checklist_errors: z.array(zDifyBuilderChecklistErrorPayload).min(1), + client_action_id: z.string().min(1).max(128).nullish(), + failed_run_id: z.string().min(1).nullish(), + model_config: zSessionModel.nullish(), + scenario: z.literal('fix'), + session_id: z.string().min(1).nullish(), +}) + +/** + * DifyBuilderCreateSessionPayload + * + * Create a Build, Edit, failed-run Fix, or checklist Fix session. + */ +export const zDifyBuilderCreateSessionPayload = z.union([ + zDifyBuilderCreateBuildSessionPayload, + zDifyBuilderCreateEditSessionPayload, + zDifyBuilderCreateFixSessionPayload, + zDifyBuilderCreateChecklistFixSessionPayload, +]) + /** * NativeTestInputBinding * @@ -452,42 +573,6 @@ export const zDifyBuilderRealCallForbiddenResponse = z.discriminatedUnion('code' zDifyBuilderConsoleForbiddenResponse, ]) -/** - * DifyBuilderChecklistErrorPayload - */ -export const zDifyBuilderChecklistErrorPayload = z.object({ - messages: z.array(z.string()), - node_id: z.string(), - node_type: z.string(), - plugin_missing: z.boolean(), - title: z.string(), - unconnected: z.boolean(), -}) - -/** - * DifyBuilderCreateChecklistFixSessionPayload - */ -export const zDifyBuilderCreateChecklistFixSessionPayload = z.object({ - app_id: z.string().min(1), - attachments: z.array(zDifyBuilderAttachmentPayload).max(5).optional(), - checklist_errors: z.array(zDifyBuilderChecklistErrorPayload).min(1), - failed_run_id: z.string().min(1).nullish(), - model_config: zSessionModel.nullish(), - scenario: z.literal('fix'), -}) - -/** - * DifyBuilderCreateSessionPayload - * - * Create a Build, Edit, failed-run Fix, or checklist Fix session. - */ -export const zDifyBuilderCreateSessionPayload = z.union([ - zDifyBuilderCreateBuildSessionPayload, - zDifyBuilderCreateEditSessionPayload, - zDifyBuilderCreateFixSessionPayload, - zDifyBuilderCreateChecklistFixSessionPayload, -]) - /** * DifyBuilderCommandStartedEventData */ @@ -637,6 +722,83 @@ export const zDifyBuilderMaterialReceiptResponse = z.object({ test_input_candidate: z.boolean(), }) +/** + * PreflightIssue + */ +export const zPreflightIssue = z.object({ + messages: z.array(z.string()).optional(), + node_id: z.string(), + node_type: z.string(), + plugin_missing: z.boolean().optional().default(false), + title: z.string(), + unconnected: z.boolean().optional().default(false), +}) + +/** + * PreflightContextCard + */ +export const zPreflightContextCard = z.object({ + issue_count: z.int(), + issues: z.array(zPreflightIssue).optional(), + node_count: z.int(), +}) + +/** + * DifyBuilderPreflightContextConversationItemResponse + */ +export const zDifyBuilderPreflightContextConversationItemResponse = z.object({ + at_version: z.int(), + kind: z.literal('preflight_context'), + payload: zPreflightContextCard, + seq: z.int(), +}) + +/** + * EntryMode + */ +export const zEntryMode = z.enum(['build', 'edit', 'fix', 'fix_checklist']) + +/** + * TaskTestEvidence + */ +export const zTaskTestEvidence = z.object({ + business_acceptance_case_id: z.string().optional().default(''), + business_acceptance_state: z + .enum(['failed', 'passed', 'unverified']) + .optional() + .default('unverified'), + execution_revision: z.string().optional().default(''), + graph_revision: z.string().optional().default(''), + mode: z.string().optional().default(''), + native_run_id: z.string().optional().default(''), + run_id: z.string().optional().default(''), + safety_outcome: z.string().optional().default(''), + sealed: z.boolean().optional().default(false), + status: z + .enum(['failed', 'not_run', 'running', 'stopped', 'succeeded', 'unknown']) + .optional() + .default('not_run'), +}) + +/** + * TaskSummary + */ +export const zTaskSummary = z.object({ + change_summary: z.string().optional().default(''), + changed_node_ids: z.array(z.string()).optional(), + completion_version: z.int().optional().default(0), + constraints: z.record(z.string(), z.string()).optional(), + entry_mode: zEntryMode.optional().default('build'), + goal: z.string().optional().default(''), + outcome: z.enum(['kept_draft', 'published']).optional().default('kept_draft'), + output_contract: z.string().optional().default(''), + resource_ids: z.array(z.string()).optional(), + schema_version: z.literal(1).optional().default(1), + task_id: z.string().optional().default(''), + test_evidence: zTaskTestEvidence.optional(), + unresolved_risks: z.array(z.string()).optional(), +}) + /** * CancellationReceipt */ @@ -702,27 +864,6 @@ export const zDifyBuilderUserConversationItemResponse = z.object({ seq: z.int(), }) -/** - * RunContextCard - */ -export const zRunContextCard = z.object({ - error_code: z.string(), - message: z.string(), - run_id: z.string(), - title: z.string(), - trace_ref: z.string().optional().default(''), -}) - -/** - * DifyBuilderRunContextConversationItemResponse - */ -export const zDifyBuilderRunContextConversationItemResponse = z.object({ - at_version: z.int(), - kind: z.literal('run_context'), - payload: zRunContextCard, - seq: z.int(), -}) - /** * WorkflowStreamMessage */ @@ -1397,21 +1538,36 @@ export const zDifyBuilderSessionViewResponse = z.object({ app_revision: zDifyBuilderAppRevisionResponse.nullish(), attachments: z.array(zDifyBuilderMaterialReceiptResponse).max(100).optional(), canvas_read_only: z.boolean(), + conversation_cleared: z.boolean().optional().default(false), conversation_last_seq: z.int(), decision: zDecision.nullish(), diagnostics: z.array(z.record(z.string(), z.unknown())).max(33).optional(), interrupted: z.boolean(), last_command_id: z.string().optional().default(''), + lifecycle_settlement: z.enum(['pending', 'settled', 'unknown']).nullish(), model: zSessionModel.nullish(), + pending_fix_context: z.union([zRunContextCard, zPreflightContextCard]).nullish(), phase: zPhase, recovery: zRecoveryRef.nullish(), reply_language: z.string(), run_status: zRunStatus, session_id: z.string(), + task_id: z.string().optional().default(''), + task_status: z.enum(['abandoned', 'active', 'completed', 'paused']).optional().default('active'), + task_summaries: z.array(zTaskSummary).max(8).optional(), test_execution: zNativeTestPublicReceipt.nullish(), version: z.int(), }) +/** + * DifyBuilderFixIntentResponse + */ +export const zDifyBuilderFixIntentResponse = z.object({ + fix_intent_id: z.string(), + session: zDifyBuilderSessionViewResponse, + status: z.string(), +}) + /** * DifyBuilderNativeTestResponse * @@ -1431,17 +1587,23 @@ export const zDifyBuilderCommandFinishedEventData = z.object({ attachments: z.array(zDifyBuilderMaterialReceiptResponse).max(100).optional(), canvas_read_only: z.boolean(), command_id: z.string(), + conversation_cleared: z.boolean().optional().default(false), conversation_last_seq: z.int(), decision: zDecision.nullish(), diagnostics: z.array(z.record(z.string(), z.unknown())).max(33).optional(), interrupted: z.boolean(), + lifecycle_settlement: z.enum(['pending', 'settled', 'unknown']).nullish(), model: zSessionModel.nullish(), + pending_fix_context: z.union([zRunContextCard, zPreflightContextCard]).nullish(), phase: zPhase, post_canvas_action_id: z.string().nullish(), recovery: zRecoveryRef.nullish(), reply_language: z.string(), run_status: zRunStatus, session_id: z.string(), + task_id: z.string().optional().default(''), + task_status: z.enum(['abandoned', 'active', 'completed', 'paused']).optional().default('active'), + task_summaries: z.array(zTaskSummary).max(8).optional(), test_execution: zNativeTestPublicReceipt.nullish(), version: z.int(), }) @@ -1503,37 +1665,6 @@ export const zDifyBuilderInteractionResponseConversationItemResponse = z.object( seq: z.int(), }) -/** - * PreflightIssue - */ -export const zPreflightIssue = z.object({ - messages: z.array(z.string()).optional(), - node_id: z.string(), - node_type: z.string(), - plugin_missing: z.boolean().optional().default(false), - title: z.string(), - unconnected: z.boolean().optional().default(false), -}) - -/** - * PreflightContextCard - */ -export const zPreflightContextCard = z.object({ - issue_count: z.int(), - issues: z.array(zPreflightIssue).optional(), - node_count: z.int(), -}) - -/** - * DifyBuilderPreflightContextConversationItemResponse - */ -export const zDifyBuilderPreflightContextConversationItemResponse = z.object({ - at_version: z.int(), - kind: z.literal('preflight_context'), - payload: zPreflightContextCard, - seq: z.int(), -}) - /** * FormField */ @@ -2507,6 +2638,19 @@ export const zGetDifyBuilderSessionsBySessionIdPath = z.object({ */ export const zGetDifyBuilderSessionsBySessionIdResponse = zDifyBuilderSessionViewResponse +export const zPostDifyBuilderSessionsBySessionIdAbandonBody = + zDifyBuilderConversationLifecyclePayload + +export const zPostDifyBuilderSessionsBySessionIdAbandonPath = z.object({ + session_id: z.string(), +}) + +/** + * Task abandoned + */ +export const zPostDifyBuilderSessionsBySessionIdAbandonResponse = + zDifyBuilderConversationLifecycleResponse + export const zPostDifyBuilderSessionsBySessionIdActionsBody = zDifyBuilderSubmitActionPayload export const zPostDifyBuilderSessionsBySessionIdActionsPath = z.object({ @@ -2518,6 +2662,18 @@ export const zPostDifyBuilderSessionsBySessionIdActionsPath = z.object({ */ export const zPostDifyBuilderSessionsBySessionIdActionsResponse = zDifyBuilderStreamEventResponse +export const zPostDifyBuilderSessionsBySessionIdClearBody = zDifyBuilderClearConversationPayload + +export const zPostDifyBuilderSessionsBySessionIdClearPath = z.object({ + session_id: z.string(), +}) + +/** + * Conversation cleared + */ +export const zPostDifyBuilderSessionsBySessionIdClearResponse = + zDifyBuilderConversationLifecycleResponse + export const zGetDifyBuilderSessionsBySessionIdConversationPath = z.object({ session_id: z.string(), }) @@ -2534,6 +2690,34 @@ export const zGetDifyBuilderSessionsBySessionIdConversationQuery = z.object({ export const zGetDifyBuilderSessionsBySessionIdConversationResponse = zDifyBuilderConversationPageResponse +export const zGetDifyBuilderSessionsBySessionIdFixIntentsPath = z.object({ + session_id: z.string(), +}) + +export const zGetDifyBuilderSessionsBySessionIdFixIntentsQuery = z.object({ + base_version: z.int().gte(1), + client_action_id: z.string().min(1).max(128), + expected_task_id: z.string().min(1).optional(), + fix_intent_id: z.string().min(1).optional(), + operation: z.enum(['dismiss_fix', 'fix', 'stop_and_switch_fix']), +}) + +/** + * Correlated durable Fix control receipt + */ +export const zGetDifyBuilderSessionsBySessionIdFixIntentsResponse = zDifyBuilderFixControlResponse + +export const zPostDifyBuilderSessionsBySessionIdFixIntentsBody = zDifyBuilderFixIntentPayload + +export const zPostDifyBuilderSessionsBySessionIdFixIntentsPath = z.object({ + session_id: z.string(), +}) + +/** + * Validated Fix intent + */ +export const zPostDifyBuilderSessionsBySessionIdFixIntentsResponse = zDifyBuilderFixIntentResponse + export const zPostDifyBuilderSessionsBySessionIdMessagesBody = zDifyBuilderSubmitMessagePayload export const zPostDifyBuilderSessionsBySessionIdMessagesPath = z.object({ diff --git a/packages/contracts/scripts/generate-builder-contract.ts b/packages/contracts/scripts/generate-builder-contract.ts new file mode 100644 index 00000000000..4860ec079b1 --- /dev/null +++ b/packages/contracts/scripts/generate-builder-contract.ts @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import { readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { createClient } from '@hey-api/openapi-ts' +import { + cloneDocumentWithPaths, + createApiConfig, + normalizeApiSwagger, + orpcErrorMaps, +} from '../openapi-ts.api.config.ts' +import { getTypedEventStreamOperations } from './api-contract-streaming-utils.mjs' + +// Refresh only the Builder owner through the ordinary API contract plugins. +// The shared config still needs the ignored OpenAPI inputs prepared first, as +// documented by the lifecycle compatibility generator. +const packageRoot = resolve(import.meta.dirname, '..') +const consoleDocument = normalizeApiSwagger( + JSON.parse( + execFileSync('uv', ['run', '--project', 'api', 'api/dev/cached_console_openapi.py'], { + cwd: resolve(packageRoot, '../..'), + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + timeout: 100000, + }), + ).console, +) +const paths = Object.fromEntries( + Object.entries(consoleDocument.paths ?? {}).filter(([route]) => + route.startsWith('/dify-builder/'), + ), +) +assert(Object.keys(paths).length > 0, 'Missing Builder Console operations') +const document = cloneDocumentWithPaths(consoleDocument, paths) +// This owner currently documents generic error bodies. Fail if it gains named +// errors rather than silently skipping the ordinary error-map postprocessor. +assert.equal( + orpcErrorMaps(document).size, + 0, + 'Builder named errors need scoped error-map generation', +) +const outputPath = resolve(packageRoot, 'generated/api/console/dify-builder') +await createClient(createApiConfig({ document, outputPath })) + +// The normal API pipeline wraps typed SSE outputs after generation. Scope that +// same transformation to this fresh owner so retained contracts are untouched. +const operations = getTypedEventStreamOperations(document) +if (operations.length > 0) { + const orpcPath = resolve(outputPath, 'orpc.gen.ts') + let source = replaceOnce( + readFileSync(orpcPath, 'utf8'), + "import { oc } from '@orpc/contract'", + "import { eventIterator, oc } from '@orpc/contract'", + ) + for (const operation of operations) { + const responseSchema = `z${operation.operationId.charAt(0).toUpperCase()}${operation.operationId.slice(1)}Response` + source = replaceOnce( + source, + `.output(${responseSchema})`, + `.output(eventIterator(${responseSchema}))`, + ) + } + writeFileSync(orpcPath, source) +} +execFileSync('pnpm', ['exec', 'vp', 'fmt', outputPath], { cwd: packageRoot, stdio: 'inherit' }) + +function replaceOnce(source: string, target: string, replacement: string) { + const firstIndex = source.indexOf(target) + assert( + firstIndex !== -1 && source.indexOf(target, firstIndex + target.length) === -1, + `Expected exactly one generated occurrence of ${target}`, + ) + return source.slice(0, firstIndex) + replacement + source.slice(firstIndex + target.length) +} diff --git a/packages/openapi-content-mapper/integration/compare-orpc.mjs b/packages/openapi-content-mapper/integration/compare-orpc.mjs index b74d80217d5..f528faef150 100644 --- a/packages/openapi-content-mapper/integration/compare-orpc.mjs +++ b/packages/openapi-content-mapper/integration/compare-orpc.mjs @@ -20,7 +20,6 @@ import { createRouter } from '../dist/tooling/router.js' const root = resolve(import.meta.dirname, '..') const repo = resolve(root, '../..') -const builderLifecycleContracts = join(repo, 'packages/contracts/fixtures/builder-lifecycle') const specDir = process.env.DIFY_OPENAPI_DIR ?? resolve(root, '../contracts/openapi') const enforceAlignment = process.argv.includes('--check') const consoleOnly = process.argv.includes('--console') @@ -51,10 +50,7 @@ report.compilerVersion = spawnSync( [join(root, 'node_modules/typescript/bin/tsc'), '--version'], { encoding: 'utf8' }, ).stdout.trim() -report.generatedFiles = [ - ...globSync(join(repo, 'packages/contracts/generated/api/**/*.ts')), - ...(consoleOnly ? globSync(join(builderLifecycleContracts, '*.gen.ts')) : []), -] +report.generatedFiles = [...globSync(join(repo, 'packages/contracts/generated/api/**/*.ts'))] .sort() .map((file) => ({ file: relative(repo, file), @@ -67,7 +63,6 @@ const contracts = new Map() const contractFiles = consoleOnly ? [ ...globSync(join(repo, 'packages/contracts/generated/api/console/**/orpc.gen.ts')), - join(builderLifecycleContracts, 'orpc.gen.ts'), join(repo, 'packages/contracts/generated/enterprise/orpc.gen.ts'), join(repo, 'packages/contracts/generated/enterprise-app-deploy/orpc.gen.ts'), ] diff --git a/packages/openapi-content-mapper/integration/console-preparation.test.mjs b/packages/openapi-content-mapper/integration/console-preparation.test.mjs index 439d2b37d37..8a3afb713f7 100644 --- a/packages/openapi-content-mapper/integration/console-preparation.test.mjs +++ b/packages/openapi-content-mapper/integration/console-preparation.test.mjs @@ -3,7 +3,10 @@ import assert from 'node:assert/strict' import { existsSync } from 'node:fs' import { test } from 'node:test' import { consoleRouterContract } from '../../contracts/console.ts' -import { contract as builderLifecycleContract } from '../../contracts/fixtures/builder-lifecycle/orpc.gen.ts' +import { + zDifyBuilderCommandFinishedEventData, + zDifyBuilderSessionViewResponse, +} from '../../contracts/generated/api/console/dify-builder/zod.gen.ts' import { composeConsoleDocuments, loadConsoleDocument, @@ -18,6 +21,79 @@ const files = [ 'console-client-openapi.json', ].map((name) => new URL(`../../contracts/openapi/${name}`, import.meta.url)) +test('production Console exposes the Builder lifecycle operations', () => { + const session = consoleRouterContract.difyBuilder.sessions.bySessionId + for (const route of ['abandon', 'clear', 'fixIntents']) { + const operation = session[route]?.post?.['~orpc'] + assert(operation, `Missing production Builder operation: ${route}`) + assert.equal(operation.route.method, 'POST') + assert.equal( + operation.route.path, + `/dify-builder/sessions/{session_id}/${route === 'fixIntents' ? 'fix-intents' : route}`, + ) + } +}) + +test('production Builder Fix recovery is a correlated read-only GET', () => { + const operation = consoleRouterContract.difyBuilder.sessions.bySessionId.fixIntents.get['~orpc'] + assert.equal(operation.route.method, 'GET') + assert.equal(operation.route.path, '/dify-builder/sessions/{session_id}/fix-intents') + const input = operation.inputSchema.parse({ + params: { session_id: 'session-1' }, + query: { + operation: 'dismiss_fix', + client_action_id: 'decision-1', + base_version: 4, + expected_task_id: 'task-1', + fix_intent_id: 'intent-1', + }, + }) + assert.equal('body' in input, false) + const receipt = { + session_id: 'session-1', + operation: 'dismiss_fix', + client_action_id: 'decision-1', + base_version: 4, + target_task_id: 'task-1', + fix_intent_id: 'intent-1', + status: 'stopping', + acknowledged: false, + } + assert.deepEqual(operation.outputSchema.parse(receipt), receipt) +}) + +test('production Builder snapshots and finished events preserve task lifecycle state', () => { + const lifecycle = { + task_id: 'task-1', + task_status: 'abandoned', + task_summaries: [], + conversation_cleared: true, + pending_fix_context: { + run_id: 'run-1', + title: 'Failed run', + error_code: 'node_failed', + message: 'The node failed', + trace_ref: '', + }, + } + for (const schema of [zDifyBuilderSessionViewResponse, zDifyBuilderCommandFinishedEventData]) { + const parsed = schema.partial().parse(lifecycle) + for (const [field, value] of Object.entries(lifecycle)) + assert.deepEqual(parsed[field], value, `Production response discarded ${field}`) + } +}) + +test('production Builder GET snapshots and finished SSE events preserve lifecycle settlement', () => { + for (const schema of [zDifyBuilderSessionViewResponse, zDifyBuilderCommandFinishedEventData]) { + for (const settlement of ['settled', 'pending', 'unknown', null]) { + const parsed = schema.partial().parse({ lifecycle_settlement: settlement }) + assert.equal(parsed.lifecycle_settlement, settlement, 'Lifecycle settlement was discarded') + } + assert.equal(schema.partial().parse({}).lifecycle_settlement, undefined) + assert.equal(schema.partial().safeParse({ lifecycle_settlement: 'cleaned' }).success, false) + } +}) + test('every existing Console member retains its name, HTTP method and route', (t) => { const before = files.map((file) => existsSync(file)) const document = loadConsoleDocument() @@ -46,7 +122,6 @@ test('every existing Console member retains its name, HTTP method and route', (t visited.add(operation) } walk(consoleRouterContract) - walk(builderLifecycleContract) assert.equal( visited.size, operations.size,