| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import type { ThinkingLevel } from '@maka/core/model-thinking'; |
| import type { AttachmentRef } from '@maka/core/events'; |
| import { isWorkHubActionResult, type WorkHubActionResult } from '@maka/core/workhub-action-result'; |
| import { decodeMessageContent } from './turn.js'; |
| import { |
| WORKHUB_COORDINATION_SESSION_ID, |
| isWorkHubCreateDefaults, |
| type WorkHubCreateDefaults, |
| } from '@maka/core/session'; |
| import { |
| requireCount, |
| requireEntityId, |
| requireExactRecord, |
| requireRecord, |
| requireShapedRecord, |
| requireUtf8String, |
| } from './codec.js'; |
| import { invalidProtocolFrame } from './errors.js'; |
| import { defineOperation } from './operation-spec.js'; |
| import { |
| decodeWorkspaceProjection, |
| decodeWorkspaceTarget, |
| type WorkspaceProjection, |
| type WorkspaceTarget, |
| } from './workspace.js'; |
| import { |
| decodeSessionConfigurationUpdateInput, |
| decodeSessionUpdateResult, |
| decodeSessionCatalogItem, |
| SESSION_CATALOG_OPERATION_SPECS, |
| type SessionModelTarget, |
| type SessionUpdateResult, |
| type SessionCatalogItem, |
| } from './session-catalog.js'; |
| |
| export interface WorkHubCoordinationConfigureModelInput { |
| readonly expectedRevision: number; |
| readonly modelTarget: Extract<SessionModelTarget, { readonly kind: 'explicit' }>; |
| readonly thinkingLevel: ThinkingLevel | null; |
| } |
| |
| export function decodeWorkHubCoordinationConfigureModelInput( |
| value: unknown, |
| ): WorkHubCoordinationConfigureModelInput { |
| const input = requireExactRecord(value, 'WorkHub model configuration', [ |
| 'expectedRevision', |
| 'modelTarget', |
| 'thinkingLevel', |
| ]); |
| const decoded = decodeSessionConfigurationUpdateInput({ |
| sessionId: WORKHUB_COORDINATION_SESSION_ID, |
| expectedRevision: input.expectedRevision, |
| patch: { |
| modelTarget: input.modelTarget, |
| thinkingLevel: input.thinkingLevel, |
| }, |
| }); |
| return { |
| expectedRevision: decoded.expectedRevision, |
| modelTarget: decoded.patch.modelTarget!, |
| thinkingLevel: decoded.patch.thinkingLevel ?? null, |
| }; |
| } |
| |
| export const WORKHUB_COORDINATION_TEXT_MAX_BYTES = 48 * 1024; |
| const COORDINATION_TITLE_MAX_BYTES = 512; |
| const CANDIDATE_SET_ID_MAX_BYTES = 96; |
| export const WORKHUB_COORDINATION_CANDIDATE_MAX_ITEMS = 32; |
| |
| const RESOLVE_ERRORS = [ |
| 'host_not_ready', |
| 'host_draining', |
| 'operation_unavailable', |
| 'operation_conflict', |
| 'persistence_failed', |
| 'commit_outcome_unknown', |
| 'internal_failure', |
| ] as const; |
| |
| const TURN_ERRORS = [ |
| 'host_not_ready', |
| 'host_draining', |
| 'operation_unavailable', |
| 'not_found', |
| 'session_archived', |
| 'session_busy', |
| 'operation_conflict', |
| 'persistence_failed', |
| 'commit_outcome_unknown', |
| 'internal_failure', |
| ] as const; |
| |
| const CANDIDATE_ERRORS = [ |
| 'host_not_ready', |
| 'host_draining', |
| 'operation_unavailable', |
| 'persistence_failed', |
| 'internal_failure', |
| ] as const; |
| |
| export type WorkHubCoordinationResolveInput = Record<string, never>; |
| |
| export interface WorkHubCoordinationResolveResult { |
| readonly sessionId: string; |
| } |
| |
| export interface WorkHubCoordinationAnswerInput { |
| readonly turnId: string; |
| readonly text: string; |
| readonly attachments?: AttachmentRef[]; |
| } |
| |
| export interface WorkHubCoordinationTurnResult { |
| readonly turnId: string; |
| } |
| |
| export type WorkHubCoordinationCandidateState = |
| | 'active' |
| | 'running' |
| | 'waiting_for_user' |
| | 'blocked' |
| | 'aborted'; |
| |
| export interface WorkHubCoordinationCandidate { |
| /** Opaque strategy-facing identity. Proposals never carry a Session id. */ |
| readonly candidateRef: string; |
| /** Presentation/navigation identity; adapters must not expose it to a model strategy. */ |
| readonly sessionId: string; |
| readonly sessionName: string; |
| readonly workspace: WorkspaceProjection; |
| readonly state: WorkHubCoordinationCandidateState; |
| readonly updatedAt: number; |
| /** Latest durable linkage for compare-and-swap correction; never model-facing. */ |
| readonly latestDelegationActionId?: string; |
| } |
| |
| export type WorkHubCoordinationCandidatesInput = Record<string, never>; |
| |
| export interface WorkHubCoordinationCandidatesResult { |
| readonly candidateSetId: string; |
| readonly candidates: readonly WorkHubCoordinationCandidate[]; |
| } |
| |
| export type WorkHubRoutingProposal = |
| | { |
| readonly disposition: 'delegate_existing'; |
| readonly candidateRef: string; |
| } |
| | { readonly disposition: 'create_new'; readonly title: string }; |
| |
| export type WorkHubLinkedOperationProposal = |
| | { |
| readonly operation: 'correct'; |
| /** Action identity of the exact durable delegation link being corrected. */ |
| readonly replacesActionId: string; |
| readonly target: |
| | { readonly disposition: 'delegate_existing'; readonly candidateRef: string } |
| | { readonly disposition: 'create_new'; readonly title: string }; |
| } |
| | { |
| readonly operation: 'stop'; |
| /** |
| * The expected state the Coordination policy resolved against. It carries no |
| * authority of its own; the Action Gate revalidates it against current |
| * durable facts, so a resolution that has gone stale fails closed instead |
| * of stopping work the user never resolved. |
| * |
| * Which delegation the stop ends is not stated here. A client cannot |
| * prove which link is live, so the Gate resolves it from its own active |
| * links, and on replay from the durable claim this action already owns. |
| */ |
| readonly expects: WorkHubCoordinationLinkedTargetPreconditions; |
| } |
| | { |
| readonly operation: 'resume'; |
| /** Bound reference from candidate discovery; the Gate checks current ownership. */ |
| readonly resumesActionId: string; |
| readonly expects: WorkHubCoordinationLinkedTargetPreconditions; |
| }; |
| |
| /** |
| * A coordination proposal is either a routing decision or an operation over a |
| * durable delegation. Linked operations are deliberately not dispositions. |
| */ |
| export type WorkHubCoordinationProposal = WorkHubRoutingProposal | WorkHubLinkedOperationProposal; |
| |
| export interface WorkHubCoordinationLinkedTargetPreconditions { |
| /** |
| * Session the resolved delegation was proposed against. Sole-active-delegation |
| * is proved by the Host from durable state under the admission lease, so the |
| * proposal states only what it resolved, never its own proof. |
| */ |
| readonly targetSessionId: string; |
| } |
| |
| export interface WorkHubCoordinationCreateContext { |
| /** Trusted desktop context. Model/strategy output never contains a workspace or identity. */ |
| readonly workspace: WorkspaceTarget; |
| } |
| |
| /** A model action can name its active Turn, never supply user-originated authority. */ |
| export interface WorkHubCoordinationActFromTurnInput { |
| readonly turnId: string; |
| readonly actionId: string; |
| readonly proposal: WorkHubCoordinationProposal; |
| readonly candidateSetId?: string; |
| readonly create?: WorkHubCoordinationCreateContext; |
| readonly newWorkDefaults?: WorkHubCreateDefaults; |
| /** Work content prepared by the coordination model for a delegated task. */ |
| readonly delegationText?: string; |
| } |
| |
| export interface WorkHubCoordinationSelectAndDelegateInput { |
| readonly turnId: string; |
| readonly actionId: string; |
| readonly candidateSetId: string; |
| readonly candidateRefs: readonly string[]; |
| readonly delegationText: string; |
| } |
| |
| export type WorkHubCoordinationSelectAndDelegateResult = |
| | { readonly kind: 'cancelled' } |
| | { readonly kind: 'delegated'; readonly result: WorkHubCoordinationActResult }; |
| |
| export type WorkHubCoordinationActResult = Exclude< |
| WorkHubActionResult, |
| { disposition: 'answer_here' | 'clarify' } |
| >; |
| |
| export const WORKHUB_COORDINATION_OPERATION_SPECS = { |
| 'workhub.coordination.configureModel': defineOperation< |
| WorkHubCoordinationConfigureModelInput, |
| SessionUpdateResult, |
| (typeof SESSION_CATALOG_OPERATION_SPECS)['session.configuration.update']['errors'][number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: SESSION_CATALOG_OPERATION_SPECS['session.configuration.update'].errors, |
| decodeInput: decodeWorkHubCoordinationConfigureModelInput, |
| decodeOutput: decodeSessionUpdateResult, |
| assertOutputForInput: (input, output) => |
| SESSION_CATALOG_OPERATION_SPECS['session.configuration.update'].assertOutputForInput?.( |
| { |
| sessionId: WORKHUB_COORDINATION_SESSION_ID, |
| expectedRevision: input.expectedRevision, |
| patch: { |
| modelTarget: input.modelTarget, |
| thinkingLevel: input.thinkingLevel, |
| }, |
| }, |
| output, |
| ), |
| }), |
| 'workhub.coordination.query': defineOperation< |
| Record<string, never>, |
| SessionCatalogItem, |
| (typeof RESOLVE_ERRORS)[number] |
| >({ |
| mode: 'query', |
| availability: 'ready', |
| errors: RESOLVE_ERRORS, |
| decodeInput: decodeWorkHubCoordinationResolveInput, |
| decodeOutput: decodeSessionCatalogItem, |
| }), |
| 'workhub.coordination.resolve': defineOperation< |
| WorkHubCoordinationResolveInput, |
| WorkHubCoordinationResolveResult, |
| (typeof RESOLVE_ERRORS)[number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: RESOLVE_ERRORS, |
| decodeInput: decodeWorkHubCoordinationResolveInput, |
| decodeOutput: decodeWorkHubCoordinationResolveResult, |
| }), |
| 'workhub.coordination.answer': defineOperation< |
| WorkHubCoordinationAnswerInput, |
| WorkHubCoordinationTurnResult, |
| (typeof TURN_ERRORS)[number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: TURN_ERRORS, |
| decodeInput: decodeWorkHubCoordinationAnswerInput, |
| decodeOutput: decodeWorkHubCoordinationTurnResult, |
| }), |
| |
| 'workhub.coordination.candidates': defineOperation< |
| WorkHubCoordinationCandidatesInput, |
| WorkHubCoordinationCandidatesResult, |
| (typeof CANDIDATE_ERRORS)[number] |
| >({ |
| mode: 'query', |
| availability: 'ready', |
| errors: CANDIDATE_ERRORS, |
| decodeInput: decodeWorkHubCoordinationCandidatesInput, |
| decodeOutput: decodeWorkHubCoordinationCandidatesResult, |
| }), |
| |
| 'workhub.coordination.selectAndDelegate': defineOperation< |
| WorkHubCoordinationSelectAndDelegateInput, |
| WorkHubCoordinationSelectAndDelegateResult, |
| (typeof TURN_ERRORS)[number] | 'candidate_set_stale' |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: [...TURN_ERRORS, 'candidate_set_stale'], |
| decodeInput: decodeWorkHubCoordinationSelectAndDelegateInput, |
| decodeOutput: (value) => { |
| const result = requireShapedRecord(value, 'WorkHub selection result', ['kind'], ['result']); |
| if (result.kind === 'cancelled' && result.result === undefined) return { kind: 'cancelled' }; |
| if (result.kind === 'delegated') |
| return { kind: 'delegated', result: decodeWorkHubCoordinationActResult(result.result) }; |
| throw invalidProtocolFrame('Invalid WorkHub selection result'); |
| }, |
| }), |
| |
| 'workhub.coordination.actFromTurn': defineOperation< |
| WorkHubCoordinationActFromTurnInput, |
| WorkHubCoordinationActResult, |
| (typeof TURN_ERRORS)[number] | 'candidate_set_stale' |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: [...TURN_ERRORS, 'candidate_set_stale'], |
| decodeInput: decodeWorkHubCoordinationActFromTurnInput, |
| decodeOutput: decodeWorkHubCoordinationActResult, |
| }), |
| } as const; |
| |
| export function decodeWorkHubCoordinationResolveInput( |
| value: unknown, |
| ): WorkHubCoordinationResolveInput { |
| requireExactRecord(value, 'WorkHub Coordination resolve input', []); |
| return {}; |
| } |
| |
| export function decodeWorkHubCoordinationResolveResult( |
| value: unknown, |
| ): WorkHubCoordinationResolveResult { |
| const result = requireExactRecord(value, 'WorkHub Coordination resolve result', ['sessionId']); |
| return { |
| sessionId: requireEntityId(result.sessionId, 'WorkHub Coordination Session id'), |
| }; |
| } |
| |
| export function decodeWorkHubCoordinationAnswerInput( |
| value: unknown, |
| ): WorkHubCoordinationAnswerInput { |
| const input = requireShapedRecord( |
| value, |
| 'WorkHub Coordination answer input', |
| ['turnId', 'text'], |
| ['attachments'], |
| ); |
| return { |
| ...(input.attachments !== undefined |
| ? { |
| attachments: decodeMessageContent({ text: input.text, attachments: input.attachments }) |
| .attachments!, |
| } |
| : {}), |
| turnId: requireEntityId(input.turnId, 'WorkHub Coordination Turn id'), |
| text: requireUtf8String( |
| input.text, |
| 'WorkHub Coordination answer text', |
| WORKHUB_COORDINATION_TEXT_MAX_BYTES, |
| ), |
| }; |
| } |
| |
| export function decodeWorkHubCoordinationTurnResult(value: unknown): WorkHubCoordinationTurnResult { |
| const result = requireExactRecord(value, 'WorkHub Coordination Turn result', ['turnId']); |
| return { |
| turnId: requireEntityId(result.turnId, 'WorkHub Coordination Turn id'), |
| }; |
| } |
| |
| export function decodeWorkHubCoordinationCandidatesInput( |
| value: unknown, |
| ): WorkHubCoordinationCandidatesInput { |
| requireExactRecord(value, 'WorkHub Coordination candidates input', []); |
| return {}; |
| } |
| |
| export function decodeWorkHubCoordinationCandidatesResult( |
| value: unknown, |
| ): WorkHubCoordinationCandidatesResult { |
| const result = requireExactRecord(value, 'WorkHub Coordination candidates result', [ |
| 'candidateSetId', |
| 'candidates', |
| ]); |
| if (!Array.isArray(result.candidates)) { |
| throw invalidProtocolFrame('Invalid WorkHub Coordination candidates'); |
| } |
| if (result.candidates.length > WORKHUB_COORDINATION_CANDIDATE_MAX_ITEMS) { |
| throw invalidProtocolFrame('Too many WorkHub Coordination candidates'); |
| } |
| return { |
| candidateSetId: candidateSetId(result.candidateSetId), |
| candidates: result.candidates.map(decodeWorkHubCoordinationCandidate), |
| }; |
| } |
| |
| export function decodeWorkHubCoordinationSelectAndDelegateInput( |
| value: unknown, |
| ): WorkHubCoordinationSelectAndDelegateInput { |
| const input = requireExactRecord(value, 'WorkHub selection input', [ |
| 'turnId', |
| 'actionId', |
| 'candidateSetId', |
| 'candidateRefs', |
| 'delegationText', |
| ]); |
| if ( |
| !Array.isArray(input.candidateRefs) || |
| input.candidateRefs.length < 1 || |
| input.candidateRefs.length > WORKHUB_COORDINATION_CANDIDATE_MAX_ITEMS |
| ) { |
| throw invalidProtocolFrame('Invalid WorkHub selection candidates'); |
| } |
| const candidateRefs = input.candidateRefs.map((ref) => |
| requireEntityId(ref, 'WorkHub candidate reference'), |
| ); |
| if (new Set(candidateRefs).size !== candidateRefs.length) |
| throw invalidProtocolFrame('Duplicate WorkHub selection candidates'); |
| return { |
| turnId: requireEntityId(input.turnId, 'WorkHub Coordination Turn id'), |
| actionId: requireEntityId(input.actionId, 'WorkHub action id'), |
| candidateSetId: requireUtf8String(input.candidateSetId, 'WorkHub candidate set', 256), |
| candidateRefs, |
| delegationText: requireUtf8String( |
| input.delegationText, |
| 'WorkHub delegation text', |
| WORKHUB_COORDINATION_TEXT_MAX_BYTES, |
| ), |
| }; |
| } |
| |
| export function decodeWorkHubCoordinationActFromTurnInput( |
| value: unknown, |
| ): WorkHubCoordinationActFromTurnInput { |
| const input = requireShapedRecord( |
| value, |
| 'WorkHub active Turn action input', |
| ['turnId', 'actionId', 'proposal'], |
| ['candidateSetId', 'create', 'newWorkDefaults', 'delegationText'], |
| ); |
| const fields = decodeWorkHubCoordinationActionFields(input); |
| |
| return { |
| ...fields, |
| proposal: fields.proposal, |
| turnId: requireEntityId(input.turnId, 'WorkHub Coordination Turn id'), |
| }; |
| } |
| |
| function decodeWorkHubCoordinationActionFields( |
| input: Record<string, unknown>, |
| ): Omit<WorkHubCoordinationActFromTurnInput, 'turnId'> { |
| const proposal = decodeWorkHubCoordinationProposal(input.proposal); |
| if ( |
| input.newWorkDefaults !== undefined && |
| (!isWorkHubCreateDefaults(input.newWorkDefaults) || |
| !( |
| ('disposition' in proposal && proposal.disposition === 'create_new') || |
| ('operation' in proposal && |
| proposal.operation === 'correct' && |
| proposal.target.disposition === 'create_new') |
| )) |
| ) { |
| throw invalidProtocolFrame('Invalid WorkHub creation defaults'); |
| } |
| const delegationText = |
| input.delegationText === undefined |
| ? undefined |
| : requireUtf8String( |
| input.delegationText, |
| 'WorkHub delegation text', |
| WORKHUB_COORDINATION_TEXT_MAX_BYTES, |
| ); |
| if ( |
| delegationText !== undefined && |
| (!delegationText.trim() || |
| !('disposition' in proposal || ('operation' in proposal && proposal.operation === 'correct'))) |
| ) { |
| throw invalidProtocolFrame('Invalid WorkHub delegation text'); |
| } |
| const base = { |
| actionId: requireEntityId(input.actionId, 'WorkHub Coordination action id'), |
| proposal, |
| ...(input.newWorkDefaults !== undefined |
| ? { newWorkDefaults: input.newWorkDefaults as WorkHubCreateDefaults } |
| : {}), |
| ...(delegationText === undefined ? {} : { delegationText }), |
| }; |
| if ('disposition' in proposal && proposal.disposition === 'delegate_existing') { |
| if (input.create !== undefined || input.candidateSetId === undefined) { |
| throw invalidProtocolFrame('Invalid WorkHub delegation context'); |
| } |
| return { ...base, candidateSetId: candidateSetId(input.candidateSetId) }; |
| } |
| if ('disposition' in proposal && proposal.disposition === 'create_new') { |
| if (input.candidateSetId !== undefined || input.create === undefined) { |
| throw invalidProtocolFrame('Invalid WorkHub creation context'); |
| } |
| return { ...base, create: decodeWorkHubCoordinationCreateContext(input.create) }; |
| } |
| if ('operation' in proposal && proposal.operation === 'correct') { |
| if (proposal.target.disposition === 'delegate_existing') { |
| if (input.candidateSetId === undefined || input.create !== undefined) { |
| throw invalidProtocolFrame('Invalid WorkHub replacement context'); |
| } |
| return { |
| ...base, |
| candidateSetId: candidateSetId(input.candidateSetId), |
| }; |
| } |
| if (input.candidateSetId !== undefined || input.create === undefined) { |
| throw invalidProtocolFrame('Invalid WorkHub replacement creation context'); |
| } |
| return { |
| ...base, |
| create: decodeWorkHubCoordinationCreateContext(input.create), |
| }; |
| } |
| if (input.candidateSetId !== undefined || input.create !== undefined) { |
| throw invalidProtocolFrame('Unexpected WorkHub action context'); |
| } |
| return base; |
| } |
| |
| export function decodeWorkHubCoordinationActResult(value: unknown): WorkHubCoordinationActResult { |
| if ( |
| !isWorkHubActionResult(value) || |
| value.disposition === 'answer_here' || |
| value.disposition === 'clarify' |
| ) { |
| throw invalidProtocolFrame('Invalid WorkHub Coordination action result'); |
| } |
| return Object.fromEntries( |
| Object.entries(value).filter(([, field]) => field !== undefined), |
| ) as WorkHubCoordinationActResult; |
| } |
| |
| function decodeWorkHubCoordinationCandidate(value: unknown): WorkHubCoordinationCandidate { |
| const candidate = requireShapedRecord( |
| value, |
| 'WorkHub Coordination candidate', |
| ['candidateRef', 'sessionId', 'sessionName', 'workspace', 'state', 'updatedAt'], |
| ['latestDelegationActionId'], |
| ); |
| return { |
| candidateRef: requireEntityId(candidate.candidateRef, 'WorkHub candidate ref'), |
| sessionId: requireEntityId(candidate.sessionId, 'WorkHub candidate Session id'), |
| sessionName: requireUtf8String(candidate.sessionName, 'WorkHub candidate name', 512), |
| workspace: decodeWorkspaceProjection(candidate.workspace), |
| state: candidateState(candidate.state), |
| updatedAt: requireCount(candidate.updatedAt, 'WorkHub candidate update time'), |
| ...(candidate.latestDelegationActionId === undefined |
| ? {} |
| : { |
| latestDelegationActionId: requireEntityId( |
| candidate.latestDelegationActionId, |
| 'WorkHub latest delegation action id', |
| ), |
| }), |
| }; |
| } |
| |
| function decodeWorkHubCoordinationProposal(value: unknown): WorkHubCoordinationProposal { |
| const proposal = requireRecord(value, 'WorkHub Coordination proposal'); |
| |
| if (proposal.disposition === 'delegate_existing') { |
| const exact = requireExactRecord(proposal, 'WorkHub delegation proposal', [ |
| 'disposition', |
| 'candidateRef', |
| ]); |
| return { |
| disposition: 'delegate_existing', |
| candidateRef: requireEntityId(exact.candidateRef, 'WorkHub candidate ref'), |
| }; |
| } |
| if (proposal.disposition === 'create_new') { |
| const exact = requireExactRecord(proposal, 'WorkHub creation proposal', [ |
| 'disposition', |
| 'title', |
| ]); |
| return { |
| disposition: 'create_new', |
| title: requireUtf8String(exact.title, 'WorkHub Session title', COORDINATION_TITLE_MAX_BYTES), |
| }; |
| } |
| if (proposal.operation === 'correct') { |
| const exact = requireExactRecord(proposal, 'WorkHub replacement proposal', [ |
| 'operation', |
| 'replacesActionId', |
| 'target', |
| ]); |
| const target = requireRecord(exact.target, 'WorkHub replacement target'); |
| if (target.disposition === 'delegate_existing') { |
| const targetExact = requireExactRecord(target, 'WorkHub replacement delegation target', [ |
| 'disposition', |
| 'candidateRef', |
| ]); |
| return { |
| operation: 'correct', |
| replacesActionId: requireEntityId(exact.replacesActionId, 'WorkHub replaced action id'), |
| target: { |
| disposition: 'delegate_existing', |
| candidateRef: requireEntityId(targetExact.candidateRef, 'WorkHub candidate ref'), |
| }, |
| }; |
| } |
| if (target.disposition === 'create_new') { |
| const targetExact = requireExactRecord(target, 'WorkHub replacement creation target', [ |
| 'disposition', |
| 'title', |
| ]); |
| return { |
| operation: 'correct', |
| replacesActionId: requireEntityId(exact.replacesActionId, 'WorkHub replaced action id'), |
| target: { |
| disposition: 'create_new', |
| title: requireUtf8String( |
| targetExact.title, |
| 'WorkHub Session title', |
| COORDINATION_TITLE_MAX_BYTES, |
| ), |
| }, |
| }; |
| } |
| throw invalidProtocolFrame('Invalid WorkHub replacement target'); |
| } |
| if (proposal.operation === 'stop') { |
| const exact = requireExactRecord(proposal, 'WorkHub stop proposal', ['operation', 'expects']); |
| return { |
| operation: 'stop', |
| expects: decodeWorkHubCoordinationLinkedTargetPreconditions(exact.expects), |
| }; |
| } |
| if (proposal.operation === 'resume') { |
| const exact = requireExactRecord(proposal, 'WorkHub resume proposal', [ |
| 'operation', |
| 'expects', |
| 'resumesActionId', |
| ]); |
| return { |
| operation: 'resume', |
| resumesActionId: requireEntityId(exact.resumesActionId, 'WorkHub resume assignment'), |
| expects: decodeWorkHubCoordinationLinkedTargetPreconditions(exact.expects), |
| }; |
| } |
| throw invalidProtocolFrame('Invalid WorkHub Coordination proposal'); |
| } |
| |
| function decodeWorkHubCoordinationLinkedTargetPreconditions( |
| value: unknown, |
| ): WorkHubCoordinationLinkedTargetPreconditions { |
| const expects = requireExactRecord(value, 'WorkHub linked-target preconditions', [ |
| 'targetSessionId', |
| ]); |
| return { |
| targetSessionId: requireEntityId(expects.targetSessionId, 'WorkHub target Session id'), |
| }; |
| } |
| |
| function decodeWorkHubCoordinationCreateContext(value: unknown): WorkHubCoordinationCreateContext { |
| const context = requireExactRecord(value, 'WorkHub creation context', ['workspace']); |
| return { |
| workspace: decodeWorkspaceTarget(context.workspace), |
| }; |
| } |
| |
| function candidateSetId(value: unknown): string { |
| const id = requireUtf8String(value, 'WorkHub candidate set id', CANDIDATE_SET_ID_MAX_BYTES); |
| if (!/^sha256:[a-f0-9]{64}$/u.test(id)) { |
| throw invalidProtocolFrame('Invalid WorkHub candidate set id'); |
| } |
| return id; |
| } |
| |
| function candidateState(value: unknown): WorkHubCoordinationCandidateState { |
| if ( |
| value === 'active' || |
| value === 'running' || |
| value === 'waiting_for_user' || |
| value === 'blocked' || |
| value === 'aborted' |
| ) { |
| return value; |
| } |
| throw invalidProtocolFrame('Invalid WorkHub candidate state'); |
| } |