resolve()¶
Submit a human review decision for an escalation. Use this after evaluate() returns status == "escalated" and a human reviewer has assessed the output.
A resolve with the SDK key is recorded as the agent's own key
resolve() calls with your agent's SDK key. The platform records that class of key on the
escalation (resolved_key_class: "sdk") and in the decision's audit-trail entry, and the
dashboard and the Console show the decision as approved by the agent's own SDK key, not by a
person. A person who approves in the Coalex Console is recorded as internal with a verified
reviewer. A decision resolved before the key class was recorded reads null: not known.
Signature¶
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
escalation_id |
str |
required | The escalation to resolve. Obtained from EvaluationDecision.escalation_id. |
decision |
str |
required | The reviewer's decision. Must be one of "approved", "rejected", or "corrected". |
corrections |
dict \| None |
None |
The corrected output. Required when decision="corrected". Must match the structure of the original output dict. |
reviewer |
str \| dict \| None |
None |
Identity of the human reviewer. Pass a string for backward compatibility, or a dict with name and email for full audit trail visibility. |
reason |
str \| None |
None |
Human-readable reason for the decision. Stored for compliance and training purposes. |
All parameters are keyword-only (enforced by *).
Returns¶
ResolutionResult¶
interface ResolutionResult {
readonly escalationId: string;
readonly status: string; // "approved" | "rejected" | "corrected"
readonly metrics: MetricResult[];
readonly resolvedAt: string; // ISO 8601 timestamp
readonly resolvedBy?: string;
readonly reviewerName?: string;
readonly reviewerEmail?: string;
}
| Field | Python | TypeScript | Description |
|---|---|---|---|
| Escalation ID | escalation_id |
escalationId |
The escalation that was resolved. |
| Status | status |
status |
The resolution status (mirrors the decision parameter). |
| Metrics | metrics |
metrics |
Per-field quality metric scores computed from the evaluation. |
| Resolved at | resolved_at |
resolvedAt |
ISO 8601 timestamp of when the resolution was recorded. |
| Resolved by | resolved_by |
resolvedBy |
Reviewer identity string (from reviewer parameter when passed as string). |
| Reviewer name | reviewer_name |
reviewerName |
Reviewer's display name (when reviewer is passed as dict). |
| Reviewer email | reviewer_email |
reviewerEmail |
Reviewer's email address (when reviewer is passed as dict). |
MetricResult¶
| Field | Type | Description |
|---|---|---|
field |
str |
Output field name (matches a key in the metrics dict from evaluate()). |
metrics |
dict[str, float] |
Metric name to score mapping. Scores are typically 0.0 to 1.0. |
Decision Types¶
"approved"¶
The human reviewer confirms the agent's output is correct as-is. No corrections needed. No metrics are computed — the human confirmed the output is correct, so there is no reference to compare against.
Reviewer as string (backward compatible)
You can still pass a plain string for backward compatibility:
However, using the dict form with name and email is recommended -- the dashboard will display the reviewer's name alongside their email in the escalation audit trail.
"rejected"¶
The human reviewer determines the agent's output is incorrect and should not be served to the user. No corrected version is provided.
"corrected"¶
The human reviewer provides a corrected version of the output. The corrections parameter is required for this decision type. Metrics are computed by comparing the original output against the provided corrections (output_vs_corrections).
result = coalex.resolve(
escalation_id="esc-003",
decision="corrected",
corrections={
"diagnosis": "Community-acquired pneumonia",
"icd_code": "J18.9",
"recommendation": "Chest X-ray and empiric antibiotics",
},
reviewer={"name": "Dr. Jones", "email": "dr.jones@hospital.org"},
reason="Changed diagnosis from bronchitis to pneumonia based on imaging.",
)
const result = await resolve({
escalationId: "esc-003",
decision: "corrected",
corrections: {
diagnosis: "Community-acquired pneumonia",
icd_code: "J18.9",
recommendation: "Chest X-ray and empiric antibiotics",
},
reviewer: { name: "Dr. Jones", email: "dr.jones@hospital.org" },
reason: "Changed diagnosis from bronchitis to pneumonia based on imaging.",
});
Role-gated decisions¶
A decision that names a role cannot be resolved from an SDK
If the escalation was created with resolver="role:manager" (or any other role: value),
resolve() will fail with a 403 no matter which API key you use. This is not an
authentication problem and rotating the key will not change it.
Closing a role-gated decision requires a reviewer assertion: a short-lived, signed statement that the person resolving it holds the role the policy demands. The Coalex Console (console-api) is the only thing that issues one. An SDK cannot, and deliberately: an API key identifies the workload that is calling, and the entire purpose of a role gate is to record which person approved. An SDK able to mint its own assertion would be able to vouch for itself, and the audit row would say a manager approved when no manager was involved.
Two ways to close one:
- In the Console. A reviewer with the role opens the decision and approves or rejects it. The Console attaches the assertion, and the resolution is recorded as verified.
- Through the agent's resume hook. The agent stays paused; when the Console resolves the
decision, the platform calls the
resume_hookgiven atevaluate()time with the outcome. The agent never callsresolve()itself.
resolver="self" decisions have no role gate and resolve through this function normally.
The 403 says all of this in the error itself:
resolve() was refused for escalation esc_123: HTTP 403 — This decision requires the manager
approval role. — A decision whose resolver names a role (resolver="role:...") can only be closed
with a reviewer assertion: ...
Validation Rules¶
| Rule | Error |
|---|---|
decision not in {"approved", "rejected", "corrected"} |
ValueError |
decision="corrected" but corrections is None or empty |
ValueError |
register() not called before resolve() |
RuntimeError |
| The escalation is role-gated | 403 — not resolvable from an SDK |
| The escalation is already resolved | 409 |
Full Workflow Example¶
import coalex
# Step 1: Evaluate the agent's output
decision = coalex.evaluate(
request_id="req-123",
input={"question": "What medication is recommended?"},
output={"answer": "Take 500mg of ibuprofen twice daily."},
metrics={"answer": ["f1", "rouge_l", "contains"]},
)
# Step 2: Handle based on status
if decision.status == "auto_approved":
# Serve the output to the user
serve_response(output)
elif decision.status == "escalated":
# Route to human reviewer (your UI, Slack, email, etc.)
human_decision = await get_human_review(decision.escalation_id)
# Step 3: Resolve with human decision
result = coalex.resolve(
escalation_id=decision.escalation_id,
decision=human_decision.decision,
corrections=human_decision.corrections,
reviewer={"name": human_decision.reviewer_name, "email": human_decision.reviewer_email},
reason=human_decision.reason,
)
print(f"Resolved: {result.status}")
print(f"Resolved at: {result.resolved_at}")
print(f"Reviewer: {result.reviewer_name} <{result.reviewer_email}>")
# Inspect per-field metrics
for m in result.metrics:
print(f" {m.field}: {m.metrics}")
# e.g., "answer: {'f1': 0.72, 'rouge_l': 0.85, 'contains': 0.0}"
elif decision.status == "rejected":
# Do not serve the output
return fallback_response()
import { evaluate, resolve } from "@coalex-ai/sdk";
// Step 1: Evaluate the agent's output
const decision = await evaluate({
requestId: "req-123",
input: { question: "What medication is recommended?" },
output: { answer: "Take 500mg of ibuprofen twice daily." },
metrics: { answer: ["f1", "rouge_l", "contains"] },
});
// Step 2: Handle based on status
if (decision.status === "auto_approved") {
// Serve the output to the user
serveResponse(output);
} else if (decision.status === "escalated") {
// Route to human reviewer (your UI, Slack, email, etc.)
const humanDecision = await getHumanReview(decision.escalationId!);
// Step 3: Resolve with human decision
const result = await resolve({
escalationId: decision.escalationId!,
decision: humanDecision.decision,
corrections: humanDecision.corrections,
reviewer: { name: humanDecision.reviewerName, email: humanDecision.reviewerEmail },
reason: humanDecision.reason,
});
console.log(`Resolved: ${result.status}`);
console.log(`Resolved at: ${result.resolvedAt}`);
console.log(`Reviewer: ${result.reviewerName} <${result.reviewerEmail}>`);
// Inspect per-field metrics
for (const m of result.metrics) {
console.log(` ${m.field}: ${JSON.stringify(m.metrics)}`);
// e.g., "answer: {"f1":0.72,"rouge_l":0.85,"contains":0.0}"
}
} else if (decision.status === "rejected") {
// Do not serve the output
return fallbackResponse();
}
Notes¶
- Resolution data is stored permanently for compliance and audit purposes (EU AI Act, ISO 42001).
- Corrections submitted with
decision="corrected"feed back into the evaluation pipeline and can be used for fine-tuning or policy updates. - The
reviewerfield is optional but strongly recommended for audit trails in regulated industries. - The
resolved_attimestamp is set server-side by the Coalex platform, not by the client. - Network errors from the Coalex API raise
httpx.HTTPStatusError.
API Reference¶
coalex.resolve ¶
SDK resolve() -- submit human review for an escalation.
Classes¶
MetricResult
dataclass
¶
ResolutionResult
dataclass
¶
Result of a resolve() call.
Source code in coalex/resolve.py
Functions:¶
resolve ¶
resolve(
*,
escalation_id: str,
decision: str,
corrections: dict[str, Any] | None = None,
reviewer: str | dict[str, Any] | None = None,
reason: str | None = None,
_config: CoalexConfig | None = None,
_api_key: str | None = None,
) -> ResolutionResult
Submit human review for an escalation. BYO HITL or Coalex dashboard.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
escalation_id
|
str
|
The escalation to resolve. |
required |
decision
|
str
|
Either vocabulary. MCP names — "accept", "accept-with-content", "decline", "cancel" — or the original "approved", "rejected", "corrected", "expired". They mean the same things; the MCP names are the ones the platform is standardising on. "cancel"/"expired" = the HITL card timed out with no human response — an operational, score-neutral outcome (not a quality rejection). |
required |
corrections
|
dict[str, Any] | None
|
Required when decision="corrected". The corrected output. |
None
|
reviewer
|
str | dict[str, Any] | None
|
Optional reviewer identity. Pass a string (backwards compat)
or a dict with |
None
|
reason
|
str | None
|
Optional human-readable reason. |
None
|
_config
|
CoalexConfig | None
|
Override config (testing only). |
None
|
_api_key
|
str | None
|
Override API key (testing only). |
None
|
Raises:
| Type | Description |
|---|---|
HTTPStatusError
|
The platform refused the resolve. A 403 means the decision is role-gated and this call cannot close it from an SDK — see the note below; the error message says the same thing at the moment it happens. |
Note
A role-gated decision is not resolvable from an SDK. If the escalation was created with
resolver="role:manager", closing it requires a reviewer assertion — a signed proof of
the resolver's role, issued by the Coalex Console. This SDK has no way to produce one, and
should not: an API key identifies the workload, and the whole point of the role gate is to
record which person approved. Resolve it in the Console, or have the agent wait on its
resume hook. resolver="self" decisions resolve here normally.
Source code in coalex/resolve.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |