Skip to main content

protocol/task

packages/protocol/src/task

Purpose

Public barrel for task, conversation, message, and task-manager protocol descriptors.

Public surface

AppId

TypeAlias
export const AppId = brandedId("AppId");

AppId

Variable
export const AppId = brandedId("AppId")

Conversation

TypeAlias
export type Conversation = Static<typeof ConversationSchema>;

ConversationArchivedError

Class
export class ConversationArchivedError extends Data.TaggedError(
  "ConversationArchived",
)<RpcErrorPayload> {
  static readonly code = -32022;
  static readonly message = "Conversation is archived";
}

ConversationFullError

Class
export class ConversationFullError extends Data.TaggedError(
  "ConversationFull",
)<RpcErrorPayload> {
  static readonly code = -32007;
  static readonly message = "Conversation is full";
}

ConversationId

TypeAlias
export const ConversationId = brandedId("ConversationId");

ConversationId

Variable
export const ConversationId = brandedId("ConversationId")

ConversationParticipant

TypeAlias
export type ConversationParticipant = Static<
  typeof ConversationParticipantSchema
>;

conversationSchema

Function
export function conversationSchema(): typeof ConversationSchema

ConversationSummary

TypeAlias
export type ConversationSummary = Static<typeof ConversationSummarySchema>;

DEFAULT_APP_ID

Variable
export const DEFAULT_APP_ID = "e12fe562-ed1f-4d2d-bed5-68b8edfa41cb" as AppId

HookBlockedError

Class
export class HookBlockedError extends Data.TaggedError(
  "HookBlocked",
)<RpcErrorPayload> {
  static readonly code = -32019;
  static readonly message = "Hook blocked the dispatch";
}

InitialConversationInput

TypeAlias
export type InitialConversationInput = Static<typeof InitialConversationSchema>;

LeaseId

TypeAlias
export const LeaseId = brandedId("LeaseId");

LeaseId

Variable
export const LeaseId = brandedId("LeaseId")

LogicalClock

TypeAlias
export type LogicalClock = Static<typeof LogicalClockSchema>;

logicalClockSchema

Function
export function logicalClockSchema(): typeof LogicalClockSchema

Message

TypeAlias
export type Message = Static<typeof MessageSchema>;

MessageId

TypeAlias
export const MessageId = brandedId("MessageId");

MessageId

Variable
export const MessageId = brandedId("MessageId")

messagePartsSchema

Function
export function messagePartsSchema(): typeof MessagePartsSchema

MessageReceivedNotification

TypeAlias
export type MessageReceivedNotification = Static<
  typeof MessageReceivedNotificationSchema
>;

MessageReceivedNotificationDefinition

Variable
export const MessageReceivedNotificationDefinition = defineNotification({
  name: "messages/received",
  params: MessageReceivedNotificationSchema,
})
Pushed when a new message is delivered to your WebSocket connection.

MessagesList

Variable
export const MessagesList = defineRpc({
  name: "messages/list",
  params: Type.Object(
    {
      taskId: TaskId,
      conversationId: ConversationId,
      sinceSeq: Type.Optional(
        Type.String({
          description: "Snowflake seq cursor (string-encoded BIGINT)",
        }),
      ),
      limit: ListLimitSchema,
    },
    { additionalProperties: false },
  ),
  result: Type.Object(
    {
      messages: Type.Array(MessageSchema),
      hasMore: Type.Boolean(),
    },
    { additionalProperties: false },
  ),
  capabilities: [
    {
      tag: TaskReadAccess,
      argsOf: (params: unknown, ctx: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (Spec F §3 dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as { readonly taskId: TaskId };
        const c = ctx as { readonly auth: { readonly agentId: AgentId } };
        return { taskId: p.taskId, callerAgentId: c.auth.agentId };
      },
    },
    {
      tag: ConversationInTask,
      argsOf: (params: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (Spec F §3 dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as {
          readonly taskId: TaskId;
          readonly conversationId: ConversationId;
        };
        return { taskId: p.taskId, conversationId: p.conversationId };
      },
    },
  ] as const,
})
List messages in a conversation with cursor-based pagination using sequence numbers.

MessagesSend

Variable
export const MessagesSend = defineRpc({
  name: "messages/send",
  params: Type.Object(
    {
      taskId: TaskId,
      conversationId: ConversationId,
      parts: MessagePartsSchema,
      replyToId: Type.Optional(MessageId),
      dispatchLeaseId: Type.Optional(LeaseId),
    },
    { additionalProperties: false },
  ),
  result: Type.Object(
    { message: MessageSchema },
    { additionalProperties: false },
  ),
  capabilities: [
    {
      tag: ConversationInTask,
      argsOf: (params: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as {
          readonly taskId: TaskId;
          readonly conversationId: ConversationId;
        };
        return { taskId: p.taskId, conversationId: p.conversationId };
      },
    },
    {
      tag: MessageSendPermission,
      argsOf: (
        params: unknown,
        ctx: unknown,
      ): ObtainMessageSendPermissionInput => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as {
          readonly taskId: TaskId;
          readonly conversationId: ConversationId;
          readonly replyToId?: Static<typeof MessageId>;
        };
        const c = ctx as {
          readonly auth: { readonly agentId: AgentId };
        };
        return {
          taskId: p.taskId,
          conversationId: p.conversationId,
          senderAgentId: c.auth.agentId,
          replyToId: p.replyToId,
        };
      },
    },
  ] as const,
})
Send a message to a conversation under a task. Both taskId and conversationId are required; the conversation must already exist (created via task/conversation/create) and the sender must be a participant. Returns: The created message with ID, sequence number, and timestamp.

MessageWithTmDecision

TypeAlias
export type MessageWithTmDecision = Static<typeof MessageWithTmDecisionSchema>;

messageWithTmDecisionSchema

Function
export function messageWithTmDecisionSchema(): typeof MessageWithTmDecisionSchema

nonTmAuthorityTaskRpcMethods

Variable
export const nonTmAuthorityTaskRpcMethods = [
  TaskRequest,
  TaskList,
  TaskLeave,
  MessagesSend,
  MessagesList,
] as const

Part

TypeAlias
export type Part = Static<typeof PartSchema>;

ParticipantNotAdmittedError

Class
export class ParticipantNotAdmittedError extends Data.TaggedError(
  "ParticipantNotAdmitted",
)<RpcErrorPayload> {
  static readonly code = -32023;
  static readonly message = "Agent is not admitted to the task";
}
task/conversation/create and task/conversation/participants/add reject agents who are not already in task_participants. The error tag lets clients distinguish “wrong agentId shape” (InvalidParams) from “agent exists but is not admitted to this task” (this tag) without parsing message strings.

Task

TypeAlias
export type Task = Static<typeof TaskSchema>;

TaskAddParticipant

Variable
export const TaskAddParticipant = defineRpc({
  name: "task/addParticipant",
  params: Type.Object(
    {
      taskId: TaskId,
      agentId: AgentId,
    },
    { additionalProperties: false },
  ),
  result: Type.Object(
    { participant: TaskParticipantSchema },
    { additionalProperties: false },
  ),
  capabilities: [
    {
      tag: TmAuthority,
      argsOf: (params: unknown, ctx: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as { readonly taskId: TaskId };
        const c = ctx as CallerConnIdCtx;
        return {
          taskId: p.taskId,
          callerConnId: c.connection.id,
        };
      },
    },
  ] as const,
})

TaskClose

Variable
export const TaskClose = defineRpc({
  name: "task/close",
  params: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
  result: Type.Object({ task: TaskSchema }, { additionalProperties: false }),
  capabilities: [
    {
      tag: TmAuthority,
      argsOf: (params: unknown, ctx: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as { readonly taskId: TaskId };
        const c = ctx as CallerConnIdCtx;
        return {
          taskId: p.taskId,
          callerConnId: c.connection.id,
        };
      },
    },
  ] as const,
})

TaskClosedError

Class
export class TaskClosedError extends Data.TaggedError(
  "TaskClosed",
)<RpcErrorPayload> {
  static readonly code = -32020;
  static readonly message = "Task is closed";
}

TaskClosedNotificationDefinition

Variable
export const TaskClosedNotificationDefinition = defineNotification({
  name: "task/closed",
  params: TaskClosedNotificationSchema,
})
Pushed when a task closes.

TaskConversationAddParticipant

Variable
export const TaskConversationAddParticipant = defineRpc({
  name: "task/conversation/participants/add",
  params: Type.Object(
    {
      taskId: TaskId,
      conversationId: ConversationId,
      agentId: AgentId,
    },
    { additionalProperties: false },
  ),
  result: Type.Object({}, { additionalProperties: false }),
  // Auth-first per per-flow doc §"Participant invariant" — the handler
  // also `yield* TmAuthority`s explicitly BEFORE
  // `requireAgentsAreInTaskParticipants` to force the obtain helper to
  // run early (lazy provideServiceEffect would otherwise defer it past
  // the participant-admitted probe).
  capabilities: [
    { tag: TmAuthority, argsOf: tmAuthorityArgsOfTask },
    { tag: ConversationInTask, argsOf: conversationInTaskArgsOfPair },
  ] as const,
})
TM-only: add an agent to one conversation. The agent MUST already appear in task_participants for taskId; otherwise ParticipantNotAdmittedError. Spec body Goal 1.

TaskConversationArchive

Variable
export const TaskConversationArchive = defineRpc({
  name: "task/conversation/archive",
  params: Type.Object(
    { taskId: TaskId, conversationId: ConversationId },
    { additionalProperties: false },
  ),
  result: Type.Object({}, { additionalProperties: false }),
  capabilities: [
    { tag: TmAuthority, argsOf: tmAuthorityArgsOfTask },
    { tag: ConversationInTask, argsOf: conversationInTaskArgsOfPair },
  ] as const,
})
TM-only: archive one conversation. Task stays open.

TaskConversationArchivedNotification

TypeAlias
export type TaskConversationArchivedNotification = Static<
  typeof TaskConversationArchivedNotificationSchema
>;

TaskConversationArchivedNotificationDefinition

Variable
export const TaskConversationArchivedNotificationDefinition =
  defineNotification({
    name: "task/conversation/archived",
    params: TaskConversationArchivedNotificationSchema,
  })

TaskConversationCreate

Variable
export const TaskConversationCreate = defineRpc({
  name: "task/conversation/create",
  params: Type.Object(
    {
      taskId: TaskId,
      name: Type.Optional(Type.String({ minLength: 1, maxLength: 100 })),
      participants: Type.Array(AgentId, { minItems: 1 }),
    },
    { additionalProperties: false },
  ),
  result: Type.Object(
    { conversation: ConversationSchema },
    { additionalProperties: false },
  ),
  // Tags are declared in auth-first order. The handler must explicitly
  // `yield* TmAuthority` before `requireAgentsAreInTaskParticipants` —
  // the dispatcher provisions tags lazily, so a non-TM caller would
  // otherwise see `ParticipantNotAdmittedError` (a state probe) instead
  // of `ForbiddenError`.
  capabilities: [
    {
      tag: TmAuthority,
      argsOf: (params: unknown, ctx: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as { readonly taskId: TaskId };
        const c = ctx as CallerConnIdCtx;
        return {
          taskId: p.taskId,
          callerConnId: c.connection.id,
        };
      },
    },
    {
      tag: ConversationCreateAuthorization,
      argsOf: (
        params: unknown,
        ctx: unknown,
      ): ObtainConversationCreateAuthorizationInput => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as {
          readonly participants: ReadonlyArray<AgentId>;
        };
        const c = ctx as { readonly auth: { readonly agentId: AgentId } };
        return {
          agentIds: [...p.participants],
          creatorAgentId: c.auth.agentId,
        };
      },
    },
  ] as const,
})
TM-only: mint a new conversation under an existing task. Every entry in participants MUST already appear in task_participants for taskId; violations return ParticipantNotAdmittedError.

TaskConversationCreatedNotification

TypeAlias
export type TaskConversationCreatedNotification = Static<
  typeof TaskConversationCreatedNotificationSchema
>;

TaskConversationCreatedNotificationDefinition

Variable
export const TaskConversationCreatedNotificationDefinition = defineNotification(
  {
    name: "task/conversation/created",
    params: TaskConversationCreatedNotificationSchema,
  },
)

TaskConversationList

Variable
export const TaskConversationList = defineRpc({
  name: "task/conversation/list",
  params: Type.Object(
    {
      limit: ListLimitSchema,
      cursor: Type.Optional(Type.String()),
    },
    { additionalProperties: false },
  ),
  result: Type.Object(
    {
      items: Type.Array(TaskConversationListItemSchema),
      nextCursor: Type.Optional(Type.String()),
    },
    { additionalProperties: false },
  ),
})
Self-only listing of every conversation the caller participates in (across all tasks). No filter params; archived rows are included; callers filter archivedAt locally. See spec body Goal 1 for the full pagination + visibility contract.

TaskConversationListItem

TypeAlias
export type TaskConversationListItem = Static<
  typeof TaskConversationListItemSchema
>;

TaskConversationParticipantsAddedNotification

TypeAlias
export type TaskConversationParticipantsAddedNotification = Static<
  typeof TaskConversationParticipantsAddedNotificationSchema
>;

TaskConversationParticipantsAddedNotificationDefinition

Variable
export const TaskConversationParticipantsAddedNotificationDefinition =
  defineNotification({
    name: "task/conversation/participants/added",
    params: TaskConversationParticipantsAddedNotificationSchema,
  })

TaskConversationParticipantsRemovedNotification

TypeAlias
export type TaskConversationParticipantsRemovedNotification = Static<
  typeof TaskConversationParticipantsRemovedNotificationSchema
>;

TaskConversationParticipantsRemovedNotificationDefinition

Variable
export const TaskConversationParticipantsRemovedNotificationDefinition =
  defineNotification({
    name: "task/conversation/participants/removed",
    params: TaskConversationParticipantsRemovedNotificationSchema,
  })

TaskConversationRemoveParticipant

Variable
export const TaskConversationRemoveParticipant = defineRpc({
  name: "task/conversation/participants/remove",
  params: Type.Object(
    {
      taskId: TaskId,
      conversationId: ConversationId,
      agentId: AgentId,
    },
    { additionalProperties: false },
  ),
  result: Type.Object({}, { additionalProperties: false }),
  capabilities: [
    { tag: TmAuthority, argsOf: tmAuthorityArgsOfTask },
    { tag: ConversationInTask, argsOf: conversationInTaskArgsOfPair },
  ] as const,
})
TM-only: remove an agent from one conversation. The agent stays in task_participants (so they may still receive messages on other conversations within the task).

TaskConversationUnarchive

Variable
export const TaskConversationUnarchive = defineRpc({
  name: "task/conversation/unarchive",
  params: Type.Object(
    { taskId: TaskId, conversationId: ConversationId },
    { additionalProperties: false },
  ),
  result: Type.Object({}, { additionalProperties: false }),
  capabilities: [
    { tag: TmAuthority, argsOf: tmAuthorityArgsOfTask },
    { tag: ConversationInTask, argsOf: conversationInTaskArgsOfPair },
  ] as const,
})
TM-only: reverse of task/conversation/archive.

TaskConversationUnarchivedNotification

TypeAlias
export type TaskConversationUnarchivedNotification = Static<
  typeof TaskConversationUnarchivedNotificationSchema
>;

TaskConversationUnarchivedNotificationDefinition

Variable
export const TaskConversationUnarchivedNotificationDefinition =
  defineNotification({
    name: "task/conversation/unarchived",
    params: TaskConversationUnarchivedNotificationSchema,
  })

TaskCreatedNotificationDefinition

Variable
export const TaskCreatedNotificationDefinition = defineNotification({
  name: "task/created",
  params: TaskCreatedNotificationSchema,
})
Pushed to the task initiator + invited participants after the TM accepts via the task/create wire callback and the task transitions from waiting to active. Carries the full Task row (matching task/closed’s shape) so subscribers don’t need a second read to discover the post-transition state.

TaskFailedNotificationDefinition

Variable
export const TaskFailedNotificationDefinition = defineNotification({
  name: "task/failed",
  params: TaskFailedNotificationSchema,
})
Pushed when a task fails before becoming ready.

TaskId

TypeAlias
export const TaskId = brandedId("TaskId");

TaskId

Variable
export const TaskId = brandedId("TaskId")

TaskLeave

Variable
export const TaskLeave = defineRpc({
  name: "task/leave",
  params: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
  result: Type.Object({}, { additionalProperties: false }),
})
Self-only: caller removes themselves from task_participants AND every conversation_participants row under the task. See spec body Goal 2 for the atomicity, idempotency, and last-participant-task-closure contract. Notification emission for each conversation the caller leaves uses TaskConversationParticipantsRemovedNotificationDefinition with reason: "task_leave". If removal empties task_participants the task transitions to status = 'closed' and TaskClosedNotificationDefinition fires alongside in the same transaction.

TaskList

Variable
export const TaskList = defineRpc({
  name: "task/list",
  params: Type.Object(
    {
      limit: ListLimitSchema,
      cursor: Type.Optional(listCursorSchema()),
    },
    { additionalProperties: false },
  ),
  result: Type.Object(
    {
      tasks: Type.Array(TaskSchema),
      nextCursor: Type.Optional(listCursorSchema()),
    },
    { additionalProperties: false },
  ),
})

taskNotifications

Variable
export const taskNotifications = [
  MessageReceivedNotificationDefinition,
  TaskClosedNotificationDefinition,
  TaskCreatedNotificationDefinition,
  TaskFailedNotificationDefinition,
  // Spec D3 canonical: only the task/conversation/* set survives the
  // `conversations/*` notification deletion.
  TaskConversationCreatedNotificationDefinition,
  TaskConversationArchivedNotificationDefinition,
  TaskConversationUnarchivedNotificationDefinition,
  TaskConversationParticipantsAddedNotificationDefinition,
  TaskConversationParticipantsRemovedNotificationDefinition,
] as const

TaskParticipant

TypeAlias
export type TaskParticipant = Static<typeof TaskParticipantSchema>;

TaskRejectedError

Class
export class TaskRejectedError extends Data.TaggedError(
  "TaskRejected",
)<RpcErrorPayload> {
  static readonly code = -32024;
  static readonly message = "Task request was rejected by the task manager";
}
task/request failed because the bound TM rejected the server-initiated task/create callback (or the fail-closed envelope synthesized a reject on timeout / RPC error / decode failure). The tag lets a requester distinguish “my task was rejected by the moderator” — an expected, actionable outcome — from an opaque internal error. The TM’s reason rides in the data arm when present.

TaskRemoveParticipant

Variable
export const TaskRemoveParticipant = defineRpc({
  name: "task/removeParticipant",
  params: Type.Object(
    {
      taskId: TaskId,
      agentId: AgentId,
    },
    { additionalProperties: false },
  ),
  result: Type.Object({}, { additionalProperties: false }),
  capabilities: [
    {
      tag: TmAuthority,
      argsOf: (params: unknown, ctx: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as { readonly taskId: TaskId };
        const c = ctx as CallerConnIdCtx;
        return {
          taskId: p.taskId,
          callerConnId: c.connection.id,
        };
      },
    },
  ] as const,
})

TaskRequest

Variable
export const TaskRequest = defineRpc({
  name: "task/request",
  params: Type.Object(
    {
      appId: AppId,
      invitedAgentIds: Type.Array(AgentId),
      initialConversation: Type.Optional(InitialConversationSchema),
    },
    { additionalProperties: false },
  ),
  result: Type.Object(
    {
      task: TaskSchema,
      conversation: Type.Union([ConversationSchema, Type.Null()]),
    },
    { additionalProperties: false },
  ),
  // Contact-policy gate. The dispatcher auto-provisions this before the
  // app-layer handler runs; the handler drains it as a precondition of
  // creating the task. Empty `invitedAgentIds` provisions a no-op proof
  // (zero targets short-circuit the obtain helper). The descriptor
  // declares the gate so the wire surface reflects the authorization
  // need even though `task/request` is bound via `defineAppMethod`.
  capabilities: [
    {
      tag: ContactPolicyAllowsReach,
      argsOf: (params: unknown, ctx: unknown) => {
        // #ignore-sloppy-code-next-line[params-cast]: descriptor argsOf re-imposes per-method param type (dispatcher-boundary erasure carve-out — params arrives as `unknown` from the type-erased dispatcher)
        const p = params as {
          readonly invitedAgentIds: ReadonlyArray<AgentId>;
        };
        const c = ctx as { readonly auth: { readonly agentId: AgentId } };
        return {
          creatorAgentId: c.auth.agentId,
          targetAgentIds: [...p.invitedAgentIds],
        };
      },
    },
  ] as const,
})
Open to any authenticated agent. Returns { task, conversation } where conversation is null when initialConversation is omitted. Dedup is a client-side concern: clients that want “one DM per participant set” semantics list their tasks and filter locally before creating a new one. NOTE (#683): the agent-facing entry RPC is task/request; the TM-facing wire callback task/create lives in packages/protocol/src/app/methods.ts. The server forks task/create to the bound TM after inserting the task in waiting; the TM’s verdict drives the lifecycle (accept → active
  • task/created; reject → failed + task/failed). The synchronous { task, conversation } result is returned after the verdict resolves (the handler awaits it). A future ack-then-notify variant could return { taskId } immediately and let task/created / task/failed carry the outcome; that is not the current shape.

taskRpcMethods

Variable
export const taskRpcMethods = [
  MessagesSend,
  MessagesList,
  TaskRequest,
  TaskLeave,
  TaskList,
  TaskClose,
  TaskAddParticipant,
  TaskRemoveParticipant,
  TaskConversationCreate,
  TaskConversationList,
  TaskConversationArchive,
  TaskConversationUnarchive,
  TaskConversationAddParticipant,
  TaskConversationRemoveParticipant,
] as const

TaskStatus

TypeAlias
export type TaskStatus = Static<typeof TaskStatusEnum>;

TmDecision

TypeAlias
export type TmDecision = Static<typeof TmDecisionSchema>;

tmDecisionSchema

Function
export function tmDecisionSchema(): typeof TmDecisionSchema

tmOnlyTaskRpcMethods

Variable
export const tmOnlyTaskRpcMethods = [
  TaskClose,
  TaskAddParticipant,
  TaskRemoveParticipant,
  TaskConversationCreate,
  TaskConversationArchive,
  TaskConversationUnarchive,
  TaskConversationAddParticipant,
  TaskConversationRemoveParticipant,
] as const

validateMessage

Variable
export const validateMessage = ajv.compile(MessageSchema) as (
  value: unknown,
)

validateTextPart

Variable
export const validateTextPart = ajv.compile(TextPartSchema) as (
  value: unknown,
)

validateTmDecision

Variable
export const validateTmDecision = ajv.compile(TmDecisionSchema) as (
  value: unknown,
)

Files

  • conversations.ts
  • ids.ts
  • messages.ts
  • methods.ts
  • tasks.ts