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.
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_callwhosecall_idequals itsnamebecomes a step, keyed and labelled by that id. Use the executor id, such aswriter. - The marker's
addedframe marks the step running; itsdoneframe completes it with the parsedarguments. - A marker with
failure_reasonmarks 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) andrun.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.
{ "time": "HH:MM:SS", "type": "thinking" | "tool" | "result",
"tool": "<tool name or null>", "text": "<text>", "result": "<text or null>" }- Map
text_reasoningcontent to athinkingrow. - Collapse a
function_calland its laterfunction_resultonto onetoolrow keyed bycall_id, with the name, arguments and result. - Add a trailing
resultrow only whendeliveredis a short summary. Ifdeliveredis 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
Noneand the marker falls back todeliveredonly. - 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.
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:
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_nameto that step marker'sargumentsinstead of sending a second marker. - A single agent emits the same
run.renamedcontent 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.
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_callnamedrun.outcome,call_idequal toname,informational_only=True, andargumentsof{"outcome": "partial", "reason": "..."}. outcomemust be"partial". AMC ignores any other value.reasonsays 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_reasonmarker, 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:
- AMC adds a Human input step in the waiting state, shows the question, and puts the run into human intervention.
- AMC reads the question from the
questionfield of yourrequest_data(it falls back toprompt). Give the dataclass aquestionfield. - When someone answers, AMC sends a follow-up Responses call with the same
previous_response_idand afunction_call_outputcarrying the matchingcall_idand the answer text. - The host restores the paused workflow's checkpoint and your
@response_handlerreceives the answer as theresponse_typeyou asked for (herestr). - 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.
Related
Last updated 2026-10-21