mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
The social-orchestrator migration off upstream Temporal onto the embedded
Hanzo Tasks engine needs five workflow primitives its digest/sendEmail/post
workflows depend on. Implemented server-side (event-sourced, replay-safe) plus
the @hanzoai/tasks TS SDK (the worker social runs):
- Signal → running workflow: engine.signalWorkflow now re-dispatches a workflow
task after appending WORKFLOW_EXECUTION_SIGNALED, so the stateless worker
replays the extended history and fires the handler (digest/sendEmail are
entirely signal-driven). TS decider read the wrong history attr key
("signal" vs "signalName") — fixed.
- continueAsNew (kind=4): closes the run CONTINUED_AS_NEW + starts a successor
under the same workflowId carrying input/search-attrs/memo. TS continueAsNew
emits the command instead of throwing ContinueAsNewNotSupported.
- startChild (kind=5): detached (ABANDON) child, seq-keyed + idempotent; retires
the 0x006D 501 for the command path. TS startChild/executeChild added.
- Typed search attributes: carried on StartWorkflow/SignalWithStart wire, stored
on the execution, visibility-queryable (already read by fieldValue). TS adds
TypedSearchAttributes/defineSearchAttributeKey/SearchAttributeType.
- workflowIdConflictPolicy USE_EXISTING/TERMINATE_EXISTING/FAIL.
- ActivityFailure wraps ApplicationFailure (with .type) so workflow error
matching (err.cause.type === 'refresh_token') works.
Also fixes a pre-existing wire defect: the server registered
SignalWithStartWorkflow at 0x0069 while every client sends 0x0066 (schema +
Go/TS SDK) — signalWithStart 404'd against the real engine. Swapped with the
HTTP-only GetWorkflowHistory (no ZAP client).
DescribeWorkflow(workflowId, "") now resolves the OPEN run (Temporal's current
execution) so a continueAsNew successor is the one signalled/described.
Tests: engine unit (search attrs, conflict policy, continueAsNew, startChild)
+ TS e2e vs real tasksd (signals-across-continueAsNew, startChild, search attrs
run to COMPLETED). go test -race ./pkg/... green; TS vitest green.
386 lines
12 KiB
Plaintext
386 lines
12 KiB
Plaintext
# Hanzo Tasks — native ZAP schema for the canonical workflow RPC
|
|
# surface. This is the one and only wire contract. Nothing on this
|
|
# wire speaks protobuf, gRPC, or any temporal.io-branded framing.
|
|
#
|
|
# Opcodes are stable across releases. Adding a new RPC appends a
|
|
# new opcode; never reuse. Request/response structs are evolved
|
|
# only by adding optional fields (future — tag-based at serde).
|
|
#
|
|
# Layout mirrors the service boundaries in hanzoai/tasks:
|
|
# frontend: user/SDK-facing workflow + schedule ops
|
|
# history: execution engine (internal)
|
|
# matching: task queue + workflow-task dispatch (internal)
|
|
# worker: scheduler + system workflows (internal)
|
|
|
|
# ── Core value types ───────────────────────────────────────────
|
|
|
|
struct WorkflowExecution
|
|
workflowId Text
|
|
runId Text
|
|
|
|
struct WorkflowType
|
|
name Text
|
|
|
|
struct TaskQueue
|
|
name Text
|
|
kind Int8 # 0=normal, 1=sticky
|
|
|
|
struct Payload
|
|
metadata Bytes # MIME-typed: application/json, application/zap, etc.
|
|
data Bytes
|
|
|
|
struct Payloads
|
|
items List(Payload)
|
|
|
|
struct RetryPolicy
|
|
initialIntervalMs Int64
|
|
backoffCoefficient Float64
|
|
maximumIntervalMs Int64
|
|
maximumAttempts Int32
|
|
nonRetryableErrorTypes List(Text)
|
|
|
|
struct Timeouts
|
|
workflowExecutionMs Int64
|
|
workflowRunMs Int64
|
|
workflowTaskMs Int64
|
|
|
|
# ── Namespace ops ──────────────────────────────────────────────
|
|
|
|
struct NamespaceInfo
|
|
name Text
|
|
state Int8 # 0=unspecified, 1=registered, 2=deprecated, 3=deleted
|
|
description Text
|
|
ownerEmail Text
|
|
id Text
|
|
|
|
struct NamespaceConfig
|
|
retentionMs Int64
|
|
|
|
struct Namespace
|
|
info NamespaceInfo
|
|
config NamespaceConfig
|
|
|
|
# ── Workflow execution ops ─────────────────────────────────────
|
|
|
|
struct WorkflowExecutionInfo
|
|
execution WorkflowExecution
|
|
type WorkflowType
|
|
startTimeMs Int64
|
|
closeTimeMs Int64
|
|
status Int8 # 0=unspecified, 1=running, 2=completed,
|
|
# 3=failed, 4=canceled, 5=terminated,
|
|
# 6=continued_as_new, 7=timed_out
|
|
historyLength Int64
|
|
taskQueue Text
|
|
memo Payloads
|
|
|
|
struct StartWorkflowRequest
|
|
namespace Text
|
|
workflowId Text
|
|
workflowType WorkflowType
|
|
taskQueue TaskQueue
|
|
input Payloads
|
|
retryPolicy RetryPolicy
|
|
timeouts Timeouts
|
|
memo Payloads
|
|
searchAttributes Payloads # name→value, stored on the execution + visibility-queryable
|
|
workflowIdConflictPolicy Text # USE_EXISTING | TERMINATE_EXISTING | FAIL (empty = none)
|
|
|
|
struct StartWorkflowResponse
|
|
runId Text
|
|
|
|
struct SignalWorkflowRequest
|
|
namespace Text
|
|
execution WorkflowExecution
|
|
signalName Text
|
|
input Payloads
|
|
|
|
struct CancelWorkflowRequest
|
|
namespace Text
|
|
execution WorkflowExecution
|
|
reason Text
|
|
|
|
struct TerminateWorkflowRequest
|
|
namespace Text
|
|
execution WorkflowExecution
|
|
reason Text
|
|
|
|
struct DescribeWorkflowRequest
|
|
namespace Text
|
|
execution WorkflowExecution
|
|
|
|
struct DescribeWorkflowResponse
|
|
info WorkflowExecutionInfo
|
|
|
|
# SignalWithStartWorkflow is the union of Start + Signal: start the
|
|
# workflow if no live execution matches workflowId, otherwise deliver
|
|
# the signal to the running execution. Opcode 0x0066.
|
|
#
|
|
# Wire shape: all fields of StartWorkflowRequest plus signalName and
|
|
# signalArg. Response is the same as StartWorkflowResponse.
|
|
struct SignalWithStartWorkflowRequest
|
|
namespace Text
|
|
workflowId Text
|
|
workflowType WorkflowType
|
|
taskQueue TaskQueue
|
|
input Payloads
|
|
retryPolicy RetryPolicy
|
|
timeouts Timeouts
|
|
memo Payloads
|
|
signalName Text
|
|
signalInput Payloads
|
|
|
|
struct SignalWithStartWorkflowResponse
|
|
runId Text
|
|
|
|
# QueryWorkflow runs a registered query against a (possibly running)
|
|
# workflow execution and returns the query's encoded result. Opcode
|
|
# 0x0067. Queries are read-only — they do not alter history.
|
|
struct QueryWorkflowRequest
|
|
namespace Text
|
|
execution WorkflowExecution
|
|
queryType Text
|
|
args Payloads
|
|
|
|
struct QueryWorkflowResponse
|
|
result Payload
|
|
|
|
struct ListWorkflowsRequest
|
|
namespace Text
|
|
query Text # SQL-subset filter ("WorkflowType='X' AND Status='Running'")
|
|
pageSize Int32
|
|
nextPageToken Bytes
|
|
|
|
struct ListWorkflowsResponse
|
|
executions List(WorkflowExecutionInfo)
|
|
nextPageToken Bytes
|
|
|
|
# ── Schedule ops ───────────────────────────────────────────────
|
|
|
|
struct ScheduleSpec
|
|
cron List(Text) # zero or more cron expressions
|
|
intervalMs Int64 # or single interval
|
|
startTimeMs Int64
|
|
endTimeMs Int64
|
|
jitterMs Int64
|
|
timezone Text
|
|
|
|
struct ScheduleAction
|
|
workflowId Text
|
|
workflowType WorkflowType
|
|
taskQueue TaskQueue
|
|
input Payloads
|
|
|
|
struct Schedule
|
|
id Text
|
|
spec ScheduleSpec
|
|
action ScheduleAction
|
|
paused Bool
|
|
|
|
struct CreateScheduleRequest
|
|
namespace Text
|
|
scheduleId Text
|
|
schedule Schedule
|
|
|
|
struct ListSchedulesRequest
|
|
namespace Text
|
|
pageSize Int32
|
|
nextPageToken Bytes
|
|
|
|
struct ListSchedulesResponse
|
|
schedules List(Schedule)
|
|
nextPageToken Bytes
|
|
|
|
# ── Task queue ops (internal: matching service) ────────────────
|
|
|
|
struct PollWorkflowTaskRequest
|
|
namespace Text
|
|
taskQueue TaskQueue
|
|
identity Text
|
|
workerBuildId Text
|
|
|
|
struct WorkflowTask
|
|
taskToken Bytes
|
|
workflowExecution WorkflowExecution
|
|
workflowType WorkflowType
|
|
history Bytes # encoded WorkflowHistory frame
|
|
nextPageToken Bytes
|
|
|
|
struct PollActivityTaskRequest
|
|
namespace Text
|
|
taskQueue TaskQueue
|
|
identity Text
|
|
|
|
struct ActivityTask
|
|
taskToken Bytes
|
|
workflowExecution WorkflowExecution
|
|
activityId Text
|
|
activityType Text
|
|
input Payloads
|
|
scheduledTimeMs Int64
|
|
startToCloseTimeoutMs Int64
|
|
heartbeatTimeoutMs Int64
|
|
|
|
struct RespondWorkflowTaskCompletedRequest
|
|
taskToken Bytes
|
|
commands Bytes # encoded command list
|
|
|
|
struct RespondActivityTaskCompletedRequest
|
|
taskToken Bytes
|
|
result Payloads
|
|
|
|
struct RespondActivityTaskFailedRequest
|
|
taskToken Bytes
|
|
failure Bytes # encoded failure
|
|
|
|
struct RecordActivityTaskHeartbeatRequest
|
|
taskToken Bytes
|
|
details Payloads
|
|
|
|
struct RecordActivityTaskHeartbeatResponse
|
|
cancelRequested Bool
|
|
|
|
# ── Worker → server: in-workflow activity scheduling ───────────
|
|
#
|
|
# When a workflow coroutine calls ExecuteActivity the worker asks
|
|
# the server to (a) mint a new ActivityTask (taskQueue addressed
|
|
# by name), and (b) hand back a stable id the worker can poll on
|
|
# for the final result. Two opcodes, one request each:
|
|
#
|
|
# scheduleActivity (0x006B) — workflow ── commands ──> server
|
|
# request : ScheduleActivityRequest
|
|
# response : ScheduleActivityResponse
|
|
#
|
|
# waitActivityResult (0x006C) — workflow ── long-poll ──> server
|
|
# request : WaitActivityResultRequest
|
|
# response : WaitActivityResultResponse
|
|
#
|
|
# Design tradeoff: long-polled waitActivityResult — not server push —
|
|
# because the worker already maintains a cheap client→server poll
|
|
# loop (pollWorkflowTask) and adding a second polled opcode keeps
|
|
# the single-direction (client→server) transport contract. A server
|
|
# push channel would require a persistent subscription connection,
|
|
# extra auth state, and back-pressure handling — all of which have
|
|
# zero upside when we already poll for workflow tasks. The long-poll
|
|
# has an explicit deadline (caller ctx); idle returns settle
|
|
# {ready=false} so workers that want to cancel the wait can do so.
|
|
|
|
struct ScheduleActivityRequest
|
|
taskQueue Text
|
|
activityType Text
|
|
input Bytes # encoded arguments (JSON v1)
|
|
startToCloseMs Int64
|
|
heartbeatMs Int64
|
|
retryPolicy RetryPolicy
|
|
|
|
struct ScheduleActivityResponse
|
|
activityTaskId Text # stable id; bind a Future to this
|
|
|
|
struct WaitActivityResultRequest
|
|
activityTaskId Text
|
|
waitMs Int64 # long-poll deadline; 0 = poll once
|
|
|
|
struct WaitActivityResultResponse
|
|
ready Bool # false → still pending, try again
|
|
result Bytes # encoded value (JSON v1); empty on pending/error
|
|
failure Bytes # encoded *temporal.Error; empty on pending/success
|
|
|
|
# ── Workflow command envelope (worker → frontend) ──────────────
|
|
#
|
|
# The `commands` Bytes field on RespondWorkflowTaskCompletedRequest
|
|
# carries a CommandsEnvelope serialised as JSON (v1). This is the
|
|
# canonical wire shape emitted by pkg/sdk/worker/dispatch.go; the
|
|
# frontend's handleRespondWorkflowTaskCompleted decodes it to
|
|
# mutate workflow history (complete / fail / schedule activity).
|
|
#
|
|
# Native ZAP serde will replace JSON in a follow-up PR. Version is
|
|
# pinned at 1; backwards-compatible additions bump the minor at
|
|
# the Command level, incompatible changes bump the envelope
|
|
# version and keep the old decoder for a release cycle.
|
|
#
|
|
# Kind values (Int8):
|
|
# 0 = completeWorkflow — workflow fn returned (non-error)
|
|
# `result` carries JSON-encoded fn return
|
|
# 1 = failWorkflow — workflow fn returned an error
|
|
# `failure` carries temporal.Encode bytes
|
|
# 2 = scheduleActivity — in-workflow ExecuteActivity call, seq-keyed
|
|
# 3 = canceled — worker ack of a CANCELING handshake
|
|
# 4 = continueAsNew — close this run (CONTINUED_AS_NEW) + start a
|
|
# successor; `input` carries the successor args,
|
|
# `workflowType`/`taskQueue` optionally override
|
|
# 5 = startChildWorkflow — detached (ABANDON) child, seq-keyed; `workflowType`
|
|
# = child type, `childWorkflowId` + `input` +
|
|
# `searchAttrs` identify/parameterize the child
|
|
|
|
struct CommandsEnvelope
|
|
version Int8 # 1 = v1
|
|
commands List(Command)
|
|
|
|
struct Command
|
|
kind Int8 # 0..5; see doc block above
|
|
result Bytes # kind=0 completeWorkflow (JSON)
|
|
failure Bytes # kind=1 failWorkflow (temporal.Encode) / kind=3 canceled reason
|
|
seq Int32 # kind=2 scheduleActivity / kind=5 startChildWorkflow
|
|
activityType Text # kind=2 scheduleActivity
|
|
workflowType Text # kind=4 continueAsNew (successor type) / kind=5 startChild (child type)
|
|
childWorkflowId Text # kind=5 startChildWorkflow
|
|
input Bytes # kind=2 activity args / kind=4 successor args / kind=5 child args
|
|
taskQueue Text # kind=2/4/5
|
|
searchAttrs Payloads # kind=5 startChildWorkflow (child visibility attrs)
|
|
|
|
# ── Canonical RPC service ──────────────────────────────────────
|
|
|
|
interface Tasks
|
|
# Namespace
|
|
listNamespaces (pageSize Int32, nextPageToken Bytes)
|
|
-> (namespaces List(Namespace), nextPageToken Bytes)
|
|
describeNamespace (name Text)
|
|
-> (namespace Namespace)
|
|
registerNamespace (namespace Namespace)
|
|
-> (ok Bool)
|
|
|
|
# Workflow lifecycle
|
|
startWorkflow (req StartWorkflowRequest)
|
|
-> (resp StartWorkflowResponse)
|
|
signalWorkflow (req SignalWorkflowRequest)
|
|
-> (ok Bool)
|
|
signalWithStartWorkflow (req SignalWithStartWorkflowRequest)
|
|
-> (resp SignalWithStartWorkflowResponse)
|
|
cancelWorkflow (req CancelWorkflowRequest)
|
|
-> (ok Bool)
|
|
terminateWorkflow (req TerminateWorkflowRequest)
|
|
-> (ok Bool)
|
|
describeWorkflow (req DescribeWorkflowRequest)
|
|
-> (resp DescribeWorkflowResponse)
|
|
listWorkflows (req ListWorkflowsRequest)
|
|
-> (resp ListWorkflowsResponse)
|
|
queryWorkflow (req QueryWorkflowRequest)
|
|
-> (resp QueryWorkflowResponse)
|
|
|
|
# Schedules
|
|
createSchedule (req CreateScheduleRequest)
|
|
-> (ok Bool)
|
|
listSchedules (req ListSchedulesRequest)
|
|
-> (resp ListSchedulesResponse)
|
|
deleteSchedule (namespace Text, scheduleId Text)
|
|
-> (ok Bool)
|
|
pauseSchedule (namespace Text, scheduleId Text, paused Bool)
|
|
-> (ok Bool)
|
|
|
|
# Task queue (matching)
|
|
pollWorkflowTask (req PollWorkflowTaskRequest)
|
|
-> (task WorkflowTask)
|
|
pollActivityTask (req PollActivityTaskRequest)
|
|
-> (task ActivityTask)
|
|
respondWorkflowTaskCompleted (req RespondWorkflowTaskCompletedRequest)
|
|
-> (ok Bool)
|
|
respondActivityTaskCompleted (req RespondActivityTaskCompletedRequest)
|
|
-> (ok Bool)
|
|
respondActivityTaskFailed (req RespondActivityTaskFailedRequest)
|
|
-> (ok Bool)
|
|
recordActivityTaskHeartbeat (req RecordActivityTaskHeartbeatRequest)
|
|
-> (resp RecordActivityTaskHeartbeatResponse)
|
|
|
|
# Health
|
|
health () -> (service Text, status Text)
|