Skip to main content
jg-agent-hq

Examples

Worked examples

Three runnable TypeScript helpers. Each shows its steps, what you get back and the full source.

01 / Session control

Start and follow a native session

Open a Codex session, send it a task and read what comes back.

  1. Create

    thread-create

    A named session, idle and ready

  2. Send

    messagereceipt

    The task is delivered

  3. Read

    live-itemsapprovalsusage

    Output, open prompts and quota

You need

  • Node.js 24.15+ on Linux or macOS
  • Codex installed and signed in
  • The controller running in this workspace

Result

What you get back

Printed delivery state

acknowledged

Acknowledged means delivered. Turn completion and tool permissions are reported separately.

thread
The managed session.
receipt
The message action and its delivery state.
liveItems
Output the session has produced so far.
approvals
Prompts waiting for your reply.
usage
The account’s quota windows and availability.

Run it

Call the helper

Example dependencies

npm install jg-agent-hq@8.0.2 jg-utilities@1.7.0

Save the helper as control-session.ts and the caller as run-session.ts, then run node run-session.ts.

run-session.ts
import { randomUUID } from 'node:crypto';
import { resolve } from 'node:path';
import { controlSession } from './control-session.ts';

const actionIds = {
  create: randomUUID(),
  message: randomUUID(),
};
// Retain these IDs in your host before submitting.
console.log(actionIds);

const session = await controlSession({
  stateRoot: resolve('.runtime'),
  workspaceRoot: process.cwd(),
  provider: 'codex',
  name: 'build-reports',
  text: 'Explain how this repository generates reports.',
  actionIds,
});

console.log(session.receipt.delivery);

Source

Complete helper

The exact file the integration tests run. Copy it whole.

examples/control-session.ts
import { requestAgentHq } from 'jg-agent-hq';
import type { IActionReceipt, IProviderId } from 'jg-agent-hq/contracts';
import { waitUntil } from 'jg-utilities';

/**
 * Creates one native session, sends explicit input and returns actual observations.
 * Start the local controller and sign in to the selected native CLI first. Retain
 * actionIds before calling; inspect those receipts after a lost client response.
 * Acknowledgement is delivery, while completion and tool permission remain native.
 */
export const controlSession = async (input: {
  stateRoot: string;
  workspaceRoot: string;
  provider: IProviderId;
  name: string;
  text: string;
  actionIds: { create: string; message: string };
}) => {
  const { stateRoot } = input;
  const status = await requestAgentHq(stateRoot, { kind: 'status' });
  if (status.kind !== 'status') throw new Error('Expected runtime status');
  const runtimeEpoch = status.result.runtimeEpoch;

  /** Reads the same admitted action; it never retries its native effect. */
  const acknowledged = (actionId: string): Promise<IActionReceipt> =>
    waitUntil(
      async () => {
        const response = await requestAgentHq(stateRoot, { kind: 'receipt', actionId });
        if (response.kind !== 'receipt') throw new Error('Expected action receipt');
        const receipt = response.result;
        if (!receipt || ['queued', 'submitted'].includes(receipt.delivery)) return;
        if (receipt.delivery !== 'acknowledged')
          throw new Error(`Inspect action ${actionId}: ${receipt.delivery}`);
        return receipt;
      },
      { timeoutSeconds: 30, intervalSeconds: 0.1 },
    );

  await requestAgentHq(stateRoot, {
    kind: 'action',
    action: {
      id: input.actionIds.create,
      runtimeEpoch,
      command: {
        kind: 'thread-create',
        provider: input.provider,
        workspaceRoot: input.workspaceRoot,
        name: input.name,
      },
    },
  });
  const created = await acknowledged(input.actionIds.create);
  if (!created.threadId) throw new Error('Creation has no managed thread');
  const threadId = created.threadId;
  const thread = await waitUntil(
    async () => {
      const response = await requestAgentHq(stateRoot, { kind: 'managed-thread', threadId });
      if (response.kind !== 'managed-thread') throw new Error('Expected managed thread');
      if (response.result?.ownership === 'uncertain' || response.result?.state === 'failed')
        throw new Error(`Inspect thread ${threadId} before sending input`);
      return response.result?.state === 'idle' ? response.result : undefined;
    },
    { timeoutSeconds: 30, intervalSeconds: 0.1 },
  );

  await requestAgentHq(stateRoot, {
    kind: 'action',
    action: {
      id: input.actionIds.message,
      runtimeEpoch,
      command: { kind: 'message', threadId, input: { kind: 'text', text: input.text } },
    },
  });
  const receipt = await acknowledged(input.actionIds.message);
  const [liveItems, approvals, usage] = await Promise.all([
    requestAgentHq(stateRoot, { kind: 'live-items', threadId }),
    requestAgentHq(stateRoot, { kind: 'approvals', threadId, state: 'pending' }),
    requestAgentHq(stateRoot, { kind: 'usage', provider: input.provider }),
  ]);
  if (liveItems.kind !== 'live-items' || approvals.kind !== 'approvals' || usage.kind !== 'usage')
    throw new Error('Expected session observations');
  return {
    thread,
    receipt,
    liveItems: liveItems.result,
    approvals: approvals.result.data,
    usage: usage.result,
  };
};

02 / Agent review

Have another agent review the work

A reviewer checks the work, HQ keeps its exact report and the author assesses it.

  1. Request a review

    message-publishrequest-dispatch

    Sent once you confirm it

  2. Read the report

    request-readreport-read

    Exact findings, with ID and hash

  3. Ask for an assessment

    message-publishrequest-dispatch

    The author weighs the findings

You need

  • The controller, with both agents signed in
  • A task and stream both agents belong to
  • A host control that confirms each request
  • Optional: jg-review-session and jg-feedback, with current grants

Result

What you get back

Illustrative · either agent can use either provider
  1. Review

    • Claude Code

      Reviews the selected work

  2. Report

    • report 7f3a

      Exact findings, kept by HQ

  3. Assessment

    • Codex

      Reads the report and responds

After you confirm both requests

Report kept. Assessment requested.

The assessment may still be running when the helper returns; it waits up to 30 seconds. Findings never approve implementation.

review
The review request, its outcome and report ID, or null if declined.
feedback
The assessment request with the report reference, or null.

Host inputs

Your host supplies

Example dependencies

npm install jg-agent-hq@8.0.2 jg-utilities@1.7.0

Save the helper as review-handoff.ts. Connect confirmRequest to a real operator decision and handle native permissions separately.

stateRoot, operatorRef
Your controller and the signed-in operator.
taskId, streamId
An existing task and stream.
requestIds, dispatchIds
Four UUIDs saved before sending, so a retry reads the same requests.
review, feedback
Who does each step, what to read and an optional skill. Feedback gets the report reference.
confirmRequest
Shows each request to the operator and returns their decision.

Source

Complete helper

The exact file the integration tests run. Copy it whole.

examples/review-handoff.ts
import { requestAgentHq } from 'jg-agent-hq';
import type {
  ICollaborationCommand,
  ICollaborationRequestRecord,
  ICollaborationResponse,
  IRequestSpecification,
} from 'jg-agent-hq/contracts';
import { waitUntil } from 'jg-utilities';

/**
 * Requests a review and returns its exact report reference for author assessment.
 * The host has already created the task, stream, memberships and any selected skill grants.
 * Either agent may use any provider. A null skill selection requires no installed skills.
 * Connect confirmRequest to an actual operator decision; native permissions stay separate.
 */
export const reviewHandoff = async (input: {
  stateRoot: string;
  operatorRef: string;
  taskId: string;
  streamId: string;
  requestIds: { review: string; feedback: string };
  dispatchIds: { review: string; feedback: string };
  review: IRequestSpecification;
  feedback: Omit<IRequestSpecification, 'targetReferences'>;
  confirmRequest: (
    phase: 'review' | 'feedback',
    specification: IRequestSpecification,
  ) => Promise<boolean>;
}): Promise<{
  review: ICollaborationRequestRecord | null;
  feedback: ICollaborationRequestRecord | null;
}> => {
  const call = async (command: ICollaborationCommand): Promise<ICollaborationResponse> => {
    const response = await requestAgentHq(input.stateRoot, {
      kind: 'collaboration',
      actor: { kind: 'host', operatorRef: input.operatorRef },
      command,
    });
    if (response.kind !== 'collaboration') throw new Error('Expected collaboration response');
    return response.result;
  };
  const read = async (requestId: string): Promise<ICollaborationRequestRecord> => {
    const result = await call({ kind: 'request-read', taskId: input.taskId, requestId });
    if (result.kind !== 'request') throw new Error('Expected request');
    return result.request;
  };
  const send = async (phase: 'review' | 'feedback', specification: IRequestSpecification) => {
    if (!(await input.confirmRequest(phase, specification))) return null;
    const id = input.requestIds[phase];
    await call({
      kind: 'message-publish',
      id,
      taskId: input.taskId,
      streamId: input.streamId,
      messageKind: 'request',
      summary: phase === 'review' ? 'Review the selected work' : 'Assess the review findings',
      body: specification.arguments,
      references: specification.targetReferences,
      replyToMessageId: null,
      request: specification,
    });
    await call({
      kind: 'request-dispatch',
      id: input.dispatchIds[phase],
      taskId: input.taskId,
      requestId: id,
    });
    return read(id);
  };
  let review = await send('review', input.review);
  if (review === null) return { review: null, feedback: null };
  // These are bounded host reads, with no inference or replacement request on timeout.
  review = await waitUntil(
    async () => {
      const current = await read(input.requestIds.review);
      return ['resolved', 'canceled', 'interrupted', 'waiting-input'].includes(current.state)
        ? current
        : undefined;
    },
    { timeoutSeconds: 30, intervalSeconds: 0.1 },
  );
  if (review.state !== 'resolved' || review.outcome !== 'completed' || !review.lastReportId)
    return { review, feedback: null };
  const report = await call({
    kind: 'report-read',
    taskId: input.taskId,
    reportId: review.lastReportId,
  });
  if (report.kind !== 'report') throw new Error('Expected report reference');
  const feedback = await send('feedback', {
    ...input.feedback,
    targetReferences: [
      { kind: 'report', reportId: report.result.report.id, sha256: report.result.report.sha256 },
    ],
  });
  return { review, feedback };
};

03 / Plan revision

Change the plan. Keep verified work.

Add a requirement mid-plan without losing finished milestones.

  1. Pause

    begin-revision

    Execution stops and running work is checked

  2. Revise

    messageapproverevision-invoke

    jg-plan and jg-breakdown rewrite the plan

  3. Approve

    finalizeapproveauthorize

    Verified work kept, new scope authorized

You need

  • A finalized plan with verified milestones
  • A free planning session in the same workspace
  • jg-plan, jg-breakdown and the project’s jg-plans CLI
  • Host controls for each approval

Result

What you get back

Illustrative revision · one requirement added
  1. Before

    • Greeting

      Verified

  2. After

    • Greeting result

      Verified · same ID and evidence

    • Report

      Pending · new milestone

Illustrative · every decision accepted

Verified work kept. New scope authorized.

A milestone keeps its evidence when jg-breakdown keeps its ID. Work the new breakdown reopens needs a new completion. Editing a status in the file never counts as verified.

skillsBinding
Current again after the breakdown refresh.
coordination.completionCount
Verified milestones still counted.
coordination.execution
The new authorization: milestones and mode.
coordination.revision.phase
Finished. No implementation has started.

Host inputs

Your host supplies

Example dependencies

npm install jg-agent-hq@8.0.2 jg-plans@0.3.0 jg-utilities@1.7.0

Save the helper as revise-plan.ts and connect its callbacks to real user decisions. It stops at authorization and starts no implementation.

approvePlan
Shows the revised plan and returns an approval only if the user approves.
requestBreakdown
Records the request to run jg-breakdown.
approveSequence
Shows the final milestones before approving them.
authorize
Picks the milestones to run, in supervised or auto mode.

Source

Complete helper

The exact file the integration tests run. Copy it whole.

examples/revise-plan.ts
import { requestAgentHq } from 'jg-agent-hq';
import type {
  IActionEnvelope,
  IPlanActionScope,
  IPlanCommandInput,
  IPlanCheckout,
  IPlanOperatorAction,
  IPlanStatus,
  IPlanTarget,
} from 'jg-agent-hq/contracts';
import { inspectPlan } from 'jg-plans';
import { waitUntil } from 'jg-utilities';

/**
 * Revises a selected sequence through native skills and fresh operator decisions.
 * Supply an available unbound planning thread in the same workspace. The host handles
 * native permissions/questions separately, retains all action IDs before calling,
 * and presents exact source before issuing each approval action. On interruption,
 * inspect those identities and the retained revision; never restart this whole flow
 * to retry an uncertain native effect.
 */
export const revisePlan = async (input: {
  stateRoot: string;
  target: IPlanTarget;
  planningThreadId: string;
  amendments: string;
  begin: IPlanOperatorAction;
  threadIds: string[];
  checkouts: IPlanCheckout[];
  finalize: IPlanOperatorAction;
  nativeActionIds: { plan: string; resume: string; breakdown: string; close: string };
  approvePlan: (source: string, status: IPlanStatus) => Promise<IPlanOperatorAction | null>;
  requestBreakdown: (status: IPlanStatus) => Promise<IPlanOperatorAction | null>;
  approveSequence: (
    source: string | null,
    status: IPlanStatus,
  ) => Promise<IPlanOperatorAction | null>;
  authorize: (status: IPlanStatus) => Promise<{
    action: IPlanOperatorAction;
    mode: 'supervised' | 'auto';
    milestoneIds: string[];
  } | null>;
  planningTimeoutSeconds?: number;
}): Promise<IPlanStatus> => {
  const { stateRoot, target, planningThreadId: threadId } = input;
  const runtime = await requestAgentHq(stateRoot, { kind: 'status' });
  if (runtime.kind !== 'status') throw new Error('Expected runtime status');
  const runtimeEpoch = runtime.result.runtimeEpoch;
  const waitOptions = { timeoutSeconds: input.planningTimeoutSeconds ?? 300, intervalSeconds: 0.2 };

  /** Reads current authority without treating a native turn as approval. */
  const status = async (): Promise<IPlanStatus> => {
    const response = await requestAgentHq(stateRoot, {
      kind: 'plan',
      command: { kind: 'status', target },
    });
    if (response.kind !== 'plan' || response.result.kind !== 'status')
      throw new Error('Expected plan status');
    return response.result.result;
  };
  /** Submits one explicit operator command with the current semantic revision. */
  const plan = (command: IPlanCommandInput) => requestAgentHq(stateRoot, { kind: 'plan', command });
  /** Waits for actual native delivery/completion; permission replies remain with the host. */
  const native = async (action: IActionEnvelope, completed = false) => {
    await requestAgentHq(stateRoot, { kind: 'action', action });
    return waitUntil(async () => {
      const response = await requestAgentHq(stateRoot, { kind: 'receipt', actionId: action.id });
      if (response.kind !== 'receipt') throw new Error('Expected action receipt');
      const receipt = response.result;
      if (!receipt) return;
      if (
        ['uncertain', 'failed', 'canceled'].includes(receipt.delivery) ||
        (receipt.completion && receipt.completion !== 'completed')
      )
        throw new Error(`Inspect native action ${action.id} before continuing`);
      return receipt.delivery === 'acknowledged' &&
        (!completed || receipt.completion === 'completed')
        ? receipt
        : undefined;
    }, waitOptions);
  };
  /** Uses the exact native inventory identity, never a fabricated skill name as input. */
  const invoke = async (name: 'jg-plan' | 'jg-breakdown', scope: IPlanActionScope, id: string) => {
    const catalog = await requestAgentHq(stateRoot, {
      kind: 'skills',
      threadId,
      search: name,
      limit: 128,
    });
    if (
      catalog.kind !== 'skills' ||
      catalog.result.state === 'unavailable' ||
      catalog.result.nextCursor
    )
      throw new Error('Select a complete available native skill inventory');
    const skills = catalog.result.data.filter(
      (skill) => skill.name === name && skill.isEnabled && skill.kind !== 'unavailable',
    );
    if (skills.length !== 1) throw new Error(`Expected one available ${name} skill`);
    await native(
      {
        id,
        runtimeEpoch,
        plan: scope,
        command: {
          kind: 'message',
          threadId,
          input: {
            kind: 'skill',
            skillId: skills[0]!.id,
            arguments: `Use the exact selected directory ${JSON.stringify(target.directory)}. ${name === 'jg-plan' ? input.amendments : 'Refresh coverage while retaining verified work and milestone identities.'}`,
          },
        },
      },
      true,
    );
  };

  await plan({
    kind: 'begin-revision',
    target,
    action: input.begin,
    expected: (await status()).revision,
    intent: 'plan',
    threadIds: input.threadIds,
    checkouts: input.checkouts,
  });
  await waitUntil(async () => {
    const current = await status();
    if (current.coordination.revision?.phase === 'blocked')
      throw new Error('Inspect the blocked revision');
    return current.coordination.revision?.phase === 'editing' ? current : undefined;
  }, waitOptions);
  await invoke(
    'jg-plan',
    { kind: 'planning', revisionId: input.begin.id, invocationId: input.begin.id },
    input.nativeActionIds.plan,
  );
  let current = await status();
  const observed = await inspectPlan(target.directory);
  if (observed.plan.state !== 'readable' || observed.plan.sha256 !== current.inspection.plan)
    throw new Error('Present the current plan again after the source change');
  const approval = await input.approvePlan(observed.plan.value.source, current);
  if (!approval) return current;
  await plan({
    kind: 'approve',
    target,
    action: approval,
    expected: current.revision,
    scope: 'plan',
    presentationHash: observed.plan.sha256,
  });

  current = await status();
  const breakdown = await input.requestBreakdown(current);
  if (!breakdown) return current;
  await plan({
    kind: 'revision-invoke',
    target,
    action: breakdown,
    expected: current.revision,
    revisionId: input.begin.id,
    skill: 'jg-breakdown',
  });
  await waitUntil(async () => {
    const current = await status();
    if (current.coordination.revision?.phase === 'blocked')
      throw new Error('Inspect the blocked revision');
    return current.coordination.revision?.phase === 'editing' ? current : undefined;
  }, waitOptions);
  const scope = {
    kind: 'planning' as const,
    revisionId: input.begin.id,
    invocationId: breakdown.id,
  };
  await native({
    id: input.nativeActionIds.resume,
    runtimeEpoch,
    plan: scope,
    command: { kind: 'thread-resume', threadId, operatorConfirmation: breakdown.source.reference },
  });
  await invoke('jg-breakdown', scope, input.nativeActionIds.breakdown);
  await native({
    id: input.nativeActionIds.close,
    runtimeEpoch,
    command: { kind: 'thread-close', threadId },
  });

  current = await status();
  // finalization records jg-breakdown's milestone IDs and carries completion evidence by ID
  const finalized = await plan({
    kind: 'finalize',
    target,
    action: input.finalize,
    expected: current.revision,
  });
  if (finalized.kind !== 'plan' || finalized.result.kind !== 'finalized')
    throw new Error('Expected finalized presentation');
  current = finalized.result.result;
  const sequenceApproval = await input.approveSequence(finalized.result.content, current);
  if (!sequenceApproval) return current;
  if (finalized.result.content !== null)
    await plan({
      kind: 'approve',
      target,
      action: sequenceApproval,
      expected: current.revision,
      scope: 'sequence',
      presentationHash: current.inspection.milestones!,
    });
  current = await status();
  const execution = await input.authorize(current);
  if (!execution) return current;
  await plan({ kind: 'authorize', target, expected: current.revision, ...execution });
  return status();
};

Every command in the API reference