Browse the docs

Report steps, tokens and human input

How AMC turns your markers into steps, cost and detail rows, how to rename a run or mark it completed with issues, and how a request_info pause becomes a human intervention.

Before you startThe marker helpers from Harden an agent or workflow for AMC.View as Markdown

This page is the agent-side view of what AMC reads from a Foundry-hosted run. The general run, step and cost model is in the integration contract.

Steps

  • Each informational function_call whose call_id equals its name becomes a step, keyed and labelled by that id. Use the executor id, such as writer.
  • The marker's added frame marks the step running; its done frame completes it with the parsed arguments.
  • A marker with failure_reason marks the step failed, even if the host frame did not.
  • Steps are keyed by a stable id, so parallel branches are separate steps and a replayed frame updates the same step rather than adding one.
  • A single agent has one step, named after the agent.
  • Reserved names are never steps: request_info (human input), run.renamed (rename), run.outcome (completed with issues) and run.artifact. A step with any of these names silently disappears from the run.

Tokens and cost

AMC sums each step's tokens into the run total and prices each step by its model, with cached_input_tokens at the cached rate. When model_usage is present, AMC prices each model's row and sums them. A model AMC has no rate for, or a marker with no model, is priced at AMC's default rate. Model ids match without regard to case.

Detail rows with activity_log

delivered is the step's headline. activity_log adds what the agent did inside the step, which AMC shows as collapsible detail. It is optional: without it, AMC shows delivered as a single row. AMC replaces, not appends, the rows on each marker, so always send the full list.

text
{ "time": "HH:MM:SS", "type": "thinking" | "tool" | "result",
  "tool": "<tool name or null>", "text": "<text>", "result": "<text or null>" }
  • Map text_reasoning content to a thinking row.
  • Collapse a function_call and its later function_result onto one tool row keyed by call_id, with the name, arguments and result.
  • Add a trailing result row only when delivered is a short summary. If delivered is the full output, the row just duplicates it.
  • Cap argument and thinking text at 2000 characters and result text at 4000.
  • In a single agent, skip the agent's own marker (call_id == AGENT_NAME) when you build rows.
  • Never let capture break a run. Wrap it so any error returns None and the marker falls back to delivered only.
  • Gate it with an environment flag (EMIT_STEP_ACTIVITY_LOG), so turning it off is a configuration change, not a redeploy.

A workflow executor already holds a complete AgentResponse, so read rows straight from response.messages. A streaming single agent receives a tool call's arguments in fragments, so aggregate first with AgentResponse.from_updates(updates).messages.

python
def _safe_activity(messages, final_text=None):
    if os.getenv("EMIT_STEP_ACTIVITY_LOG", "true").strip().lower() in {"false", "0", "no"}:
        return None
    try:
        return _activity_rows_from_messages(messages, final_text) or None
    except Exception:
        return None

# Workflow executor, success path:
await _emit_step_marker(ctx, "writer", delivered=response.text, ...,
                        activity_log=_safe_activity(response.messages))

Rename the run

Once a run's identity is known, for example the topic it resolved, it can rename this run only. The registered default name never changes. Emit a reserved marker:

python
RUN_RENAMED_MARKER = "run.renamed"   # reserved; spell it exactly


async def _emit_rename_marker(ctx, name: str) -> None:
    clean = (name or "").strip()
    if not clean:
        return                           # blank is a no-op and never clears the name
    await ctx.yield_output(Message(role="assistant", author_name=RUN_RENAMED_MARKER, contents=[
        Content.from_function_call(call_id=RUN_RENAMED_MARKER, name=RUN_RENAMED_MARKER,
                                   arguments={"name": clean}, informational_only=True)]))
  • Or, when the rename coincides with a step, add run_name to that step marker's arguments instead of sending a second marker.
  • A single agent emits the same run.renamed content alongside its token marker.
  • Last non-empty name wins. A blank name is ignored. A rename after the run has ended is ignored. AMC keeps the name across reloads.

Completed with issues

A run that finishes normally after a failure that matters, but isn't fatal, says so with the reserved run.outcome marker. AMC then records the run's outcome as partial and shows it as Completed with issues, with the reason. The status stays completed.

python
RUN_OUTCOME_MARKER = "run.outcome"   # reserved; spell it exactly


async def _emit_outcome_marker(ctx, reason: str) -> None:
    clean = " ".join((reason or "").split())[:2000]
    if not clean:                        # a partial outcome must say what wasn't done
        return
    await ctx.yield_output(Message(role="assistant", author_name=RUN_OUTCOME_MARKER, contents=[
        Content.from_function_call(call_id=RUN_OUTCOME_MARKER, name=RUN_OUTCOME_MARKER,
                                   arguments={"outcome": "partial", "reason": clean},
                                   informational_only=True)]))
  • The shape is exact: a function_call named run.outcome, call_id equal to name, informational_only=True, and arguments of {"outcome": "partial", "reason": "..."}.
  • outcome must be "partial". AMC ignores any other value. reason says what wasn't done, up to 2000 characters.
  • In a workflow, emit it from the executor that decides the run is incomplete: it catches the exception, emits that step's failure_reason marker, doesn't re-raise, and calls _emit_outcome_marker. A single agent emits the same content alongside its token marker.
  • Emit it any time before the response completes. The last one wins.
  • Failed wins. If the run then ends response.failed, AMC ignores the marker.
  • Only the marker counts. AMC never infers Completed with issues from a step that carries failure_reason; without the marker, that run is a clean success.

Human input

When a workflow calls ctx.request_info(...), the hosted agent surfaces the pending request as a real function_call named request_info. Its call_id is the request id. Then:

  1. AMC adds a Human input step in the waiting state, shows the question, and puts the run into human intervention.
  2. AMC reads the question from the question field of your request_data (it falls back to prompt). Give the dataclass a question field.
  3. When someone answers, AMC sends a follow-up Responses call with the same previous_response_id and a function_call_output carrying the matching call_id and the answer text.
  4. The host restores the paused workflow's checkpoint and your @response_handler receives the answer as the response_type you asked for (here str).
  5. AMC records the step as completed with the answer and the run continues.

A paused turn also ends with response.completed. AMC keeps the run waiting until the answer resumes it, so you don't need to do anything special.

Last updated 2026-10-21