Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/workflow-executor/src/adapters/console-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export default class ConsoleLogger implements Logger {
console.error(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
}

warn(message: string, context: Record<string, unknown>): void {
console.warn(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
}

info(message: string, context: Record<string, unknown>): void {
console.info(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export default class ForestServerWorkflowPort implements WorkflowPort {
);
}

const step = toAvailableStepExecution(run);
const step = toAvailableStepExecution(run, this.logger);
if (!step) return null;

return { step, auth: { forestServerToken: token } };
Expand Down
5 changes: 5 additions & 0 deletions packages/workflow-executor/src/adapters/pretty-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export default class PrettyLogger implements Logger {
console.info(this.format(pc.cyan('info '), message, context));
}

warn(message: string, context: Record<string, unknown>): void {
// eslint-disable-next-line no-console
console.warn(this.format(pc.yellow('warn '), message, context));
}

error(message: string, context: Record<string, unknown>): void {
// eslint-disable-next-line no-console
console.error(this.format(pc.red('error'), message, context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
ServerStepHistory,
ServerUserProfile,
} from './server-types';
import type { Logger } from '../ports/logger-port';
import type {
ConditionStepOutcome,
GuidanceStepOutcome,
Expand Down Expand Up @@ -37,8 +38,8 @@ function toRecordStatus(ctxStatus: unknown): RecordStepOutcome['status'] {
// `context` may come from the executor (our StepOutcome, stored verbatim) or the legacy frontend
// (free-form). We whitelist known fields per type to avoid leaking legacy ones back to the
// orchestrator and to enforce the discriminated-union shape.
function toStepOutcome(s: ServerStepHistory): StepOutcome {
const stepDef = toStepDefinition(s.stepDefinition);
function toStepOutcome(s: ServerStepHistory, logger: Logger): StepOutcome {
const stepDef = toStepDefinition(s.stepDefinition, logger);
const outcomeType = stepTypeToOutcomeType(stepDef.type);
const ctx = (s.context ?? {}) as Record<string, unknown>;

Expand All @@ -49,7 +50,7 @@ function toStepOutcome(s: ServerStepHistory): StepOutcome {
};

if (outcomeType === 'condition') {
const status: ConditionStepOutcome['status'] = ctx.status === 'error' ? 'error' : 'success';
const status: ConditionStepOutcome['status'] = toRecordStatus(ctx.status);
const selectedOption = typeof ctx.selectedOption === 'string' ? ctx.selectedOption : undefined;

return {
Expand All @@ -75,9 +76,12 @@ function toStepOutcome(s: ServerStepHistory): StepOutcome {
return { type: 'record', ...baseFromCtx, status } satisfies RecordStepOutcome;
}

function tryMapStep(s: ServerStepHistory): Step | null {
function tryMapStep(s: ServerStepHistory, logger: Logger): Step | null {
try {
return { stepDefinition: toStepDefinition(s.stepDefinition), stepOutcome: toStepOutcome(s) };
return {
stepDefinition: toStepDefinition(s.stepDefinition, logger),
stepOutcome: toStepOutcome(s, logger),
};
} catch (err) {
// Sub-workflow navigation steps (start-sub-workflow, close-sub-workflow) are not
// meaningful for AI context — skip them rather than failing the whole run.
Expand All @@ -89,10 +93,11 @@ function tryMapStep(s: ServerStepHistory): Step | null {
function toPreviousSteps(
history: ServerStepHistory[],
pendingStepIndex: number,
logger: Logger,
): ReadonlyArray<Step> {
return history
.filter(s => s.done && s.stepIndex < pendingStepIndex)
.map(s => tryMapStep(s))
.map(s => tryMapStep(s, logger))
.filter((s): s is Step => s !== null);
}

Expand Down Expand Up @@ -123,6 +128,7 @@ function toStepUser(runId: number, profile: ServerUserProfile): StepUser {
// userProfile) or an unmappable step definition.
export default function toAvailableStepExecution(
run: ServerHydratedWorkflowRun,
logger: Logger,
): AvailableStepExecution | null {
if (!run.collectionName) {
throw new InvalidStepDefinitionError(
Expand All @@ -149,8 +155,8 @@ export default function toAvailableStepExecution(
recordId: [run.selectedRecordId],
stepIndex: 0,
},
stepDefinition: toStepDefinition(pending.stepDefinition),
previousSteps: toPreviousSteps(run.workflowHistory, pending.stepIndex),
stepDefinition: toStepDefinition(pending.stepDefinition, logger),
previousSteps: toPreviousSteps(run.workflowHistory, pending.stepIndex, logger),
user: toStepUser(run.id, run.userProfile),
};

Expand Down
148 changes: 108 additions & 40 deletions packages/workflow-executor/src/adapters/server-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,129 @@ export interface ServerWorkflowTransition {
answer?: string;
}

export type ServerTaskType =
| 'guideline'
| 'trigger-action'
| 'get-data'
| 'update-data'
| 'load-related-record'
| 'mcp-server';

export interface ServerWorkflowTask {
type: 'task';
taskType: ServerTaskType;
isSubTask?: boolean;
title: string;
prompt: string;
allowedTools?: string[];
mcpServerId?: string;
automaticExecution?: boolean;
automaticCompletion?: boolean;
outgoing: ServerWorkflowTransition;
export enum ServerStepTypeEnum {
Task = 'task',
Condition = 'condition',
End = 'end',
Escalation = 'escalation',
StartSubWorkflow = 'start-sub-workflow',
CloseSubWorkflow = 'close-sub-workflow',
}

export interface ServerWorkflowCondition {
type: 'condition';
title: string;
prompt: string;
outgoing: ServerWorkflowTransition[];
automaticExecution?: boolean;
export enum ServerTaskTypeEnum {
Guideline = 'guideline',
TriggerAction = 'trigger-action',
GetData = 'get-data',
UpdateData = 'update-data',
LoadRelatedRecord = 'load-related-record',
McpServer = 'mcp-server',
}

export enum ServerStepExecutionTypeEnum {
Manual = 'manual',
AutomatedWithConfirmation = 'automated-with-confirmation',
FullyAutomated = 'fully-automated',
}

export interface ServerWorkflowEnd {
type: 'end';
interface ServerWorkflowStepBase {
type: ServerStepTypeEnum;
title: string;
prompt?: string;
executionType: ServerStepExecutionTypeEnum;
automaticCompletion: boolean;
outgoing: ServerWorkflowTransition[];
}

export interface ServerWorkflowEscalation {
type: 'escalation';
title: string;
export interface ServerWorkflowTaskBase extends ServerWorkflowStepBase {
type: ServerStepTypeEnum.Task;
taskType: ServerTaskTypeEnum;
isSubTask?: boolean;
prompt: string;
outgoing: ServerWorkflowTransition;
inboxId: string | null;
outgoing: [ServerWorkflowTransition];
}

export interface ServerStartSubWorkflow {
type: 'start-sub-workflow';
title: string;
export interface ServerWorkflowTaskGuideline extends ServerWorkflowTaskBase {
taskType: ServerTaskTypeEnum.Guideline;
executionType: ServerStepExecutionTypeEnum.Manual;
completionType: 'simple' | 'user-input';
inputType?: 'free-text';
automaticCompletion: false;
}

interface ServerWorkflowTaskGetData extends ServerWorkflowTaskBase {
taskType: ServerTaskTypeEnum.GetData;
executionType: ServerStepExecutionTypeEnum.FullyAutomated;
}

interface ServerWorkflowTaskUpdateData extends ServerWorkflowTaskBase {
taskType: ServerTaskTypeEnum.UpdateData;
executionType:
| ServerStepExecutionTypeEnum.FullyAutomated
| ServerStepExecutionTypeEnum.AutomatedWithConfirmation;
}

interface ServerWorkflowTaskTriggerAction extends ServerWorkflowTaskBase {
taskType: ServerTaskTypeEnum.TriggerAction;
executionType:
| ServerStepExecutionTypeEnum.FullyAutomated
| ServerStepExecutionTypeEnum.AutomatedWithConfirmation;
}

interface ServerWorkflowTaskLoadRelatedRecord extends ServerWorkflowTaskBase {
taskType: ServerTaskTypeEnum.LoadRelatedRecord;
executionType:
| ServerStepExecutionTypeEnum.FullyAutomated
| ServerStepExecutionTypeEnum.AutomatedWithConfirmation;
}

export interface ServerWorkflowTaskMcpServer extends ServerWorkflowTaskBase {
taskType: ServerTaskTypeEnum.McpServer;
executionType:
| ServerStepExecutionTypeEnum.FullyAutomated
| ServerStepExecutionTypeEnum.AutomatedWithConfirmation;
mcpServerId: string;
}

export type ServerWorkflowTask =
| ServerWorkflowTaskGuideline
| ServerWorkflowTaskGetData
| ServerWorkflowTaskUpdateData
| ServerWorkflowTaskTriggerAction
| ServerWorkflowTaskLoadRelatedRecord
| ServerWorkflowTaskMcpServer;

export interface ServerWorkflowEnd extends ServerWorkflowStepBase {
type: ServerStepTypeEnum.End;
executionType: ServerStepExecutionTypeEnum.Manual;
automaticCompletion: false;
outgoing: [];
}

export interface ServerWorkflowCondition extends ServerWorkflowStepBase {
type: ServerStepTypeEnum.Condition;
executionType: ServerStepExecutionTypeEnum.Manual | ServerStepExecutionTypeEnum.FullyAutomated;
prompt: string;
automaticCompletion: false;
}

export interface ServerWorkflowEscalation extends ServerWorkflowStepBase {
type: ServerStepTypeEnum.Escalation;
prompt: string;
outgoing: ServerWorkflowTransition;
outgoing: [ServerWorkflowTransition];
inboxId: string | null;
}

export interface ServerStartSubWorkflow extends ServerWorkflowStepBase {
type: ServerStepTypeEnum.StartSubWorkflow;
executionType: ServerStepExecutionTypeEnum.Manual;
outgoing: [ServerWorkflowTransition];
workflowId: string;
}

export interface ServerCloseSubWorkflow {
type: 'close-sub-workflow';
title?: string;
outgoing: ServerWorkflowTransition;
export interface ServerCloseSubWorkflow extends ServerWorkflowStepBase {
type: ServerStepTypeEnum.CloseSubWorkflow;
executionType: ServerStepExecutionTypeEnum.Manual;
outgoing: [ServerWorkflowTransition];
parentWorkflowId: string | null;
}

Expand Down
Loading
Loading