Docs / Human input

Human input

Use session input when an agent run needs a human decision, follow-up message, webhook reply, or operator correction. The task waits on a named input stream. Helmr parks the run durably, keeps the session history attached, and resumes the run when matching input arrives.

import { streams, task } from "@helmr/sdk"
import { z } from "zod"

const approval = streams.input("approval", {
  schema: z.object({
    decision: z.enum(["approve", "reject", "edit"]),
    note: z.string().optional(),
  }),
})

export const publishReview = task({
  id: "publish-review",
  run: async (event, ctx) => {
    await sendSlackApproval({
      sessionId: ctx.session.id,
      stream: approval.id,
      correlationId: `github:${event.owner}/${event.repo}#${event.prNumber}`,
    })

    const decision = await approval.wait({
      timeout: "30m",
      correlationId: `github:${event.owner}/${event.repo}#${event.prNumber}`,
    }).unwrap()

    if (decision.decision !== "approve") return { status: "skipped" }
    await postReview()
    return { status: "posted" }
  },
})

Send input from a trusted app server, webhook handler, or operator tool:

await client.sessions.open(sessionId).input(approval.id).send(
  {
    decision: "approve",
    note: "Looks good.",
  },
  {
    correlationId: `github:${owner}/${repo}#${prNumber}`,
  },
)

Use correlationId when the same stream can carry more than one pending decision for a session. The waiting run only resumes from input that matches the wait’s stream and correlation id.

Browser Actions

For browser UI, create a scoped public access token that can only append to one session input stream. The browser receives the opaque token, not a Helmr API key:

import { auth } from "@helmr/sdk"

const inputToken = await auth.createPublicToken({
  scope: {
    type: "session.input.send",
    session: sessionId,
    stream: approval,
    correlationId: `github:${owner}/${repo}#${prNumber}`,
  },
  maxUses: 1,
})

The browser or action endpoint can then send the input with that bearer token:

await client.sessions.open(sessionId).input(approval.id).send(
  { decision: "approve" },
  {
    publicAccessToken: inputToken.publicAccessToken,
    correlationId: `github:${owner}/${repo}#${prNumber}`,
  },
)

External Callbacks

Use an externally completable token when the integration is naturally a callback target instead of a session input surface: for example, an email link, a third party webhook provider, or a bridge that should not know the session id and stream name.

import { tokens } from "@helmr/sdk"

const token = await tokens.create({
  timeout: "30m",
  tags: ["approval", "email"],
  metadata: { action: "publish-review" },
})

await sendApprovalEmail({
  callbackUrl: token.callbackUrl,
})

const decision = await token.wait({
  schema: approvalDecisionSchema,
}).unwrap()

Server-side bridge code can complete the token with an API key:

await client.tokens.complete(token.id, {
  approved: true,
  actor: "email:reviewer@example.com",
})

Prefer session input when the response belongs in the agent session transcript. Prefer tokens when you need a one-shot callback capability that can be completed without exposing a session stream.

Inspecting Waits

Operators can inspect the relevant session and run attempts from the CLI:

helmr session get SESSION_ID
helmr run list --session SESSION_ID

Only one blocking token, stream, or timer wait can be active at a time in a task. Await each wait before starting the next one.