Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion contracts/agents-api/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ python services/core/tests/qualify_public_native.py \

| Suite | Operations | Placement |
| --- | --- | --- |
| `none` | Creation retry, foreign history rejection, two native text Turns, history recall, SSE ordering and SDK/raw Item and Turn parity | `none` |
| `none` | Initial-input creation/retry/conflict with one Turn, foreign history rejection, two native text Turns, history recall, SSE ordering, SDK/raw schema parity and nullable usage accounting | `none` |
| `pending-actions` | Query/reconnect pending calls, success/error results, cancellation, exact target rejection, retries and durable Items | Any declared placement with function tools |
| `functions` | SDK handlers, success/error, native file and Artifact bytes, continuation, pending cancellation and tenant isolation | Workspace |
| `images` | Initial and active images, image results, retry/atomic rejection, native files/Artifacts, isolation and continuation | Workspace with declared image and function support |
Expand All @@ -239,6 +239,8 @@ For `self_hosted`, choose a custom absolute `workspace_directory`. When the runn

Warm continuation is the default and records cold recovery as `unverified`. To qualify cold continuation, additionally pass `--compose-directory` with the owned installation's absolute directory and `--compose-project` with its exact project name. The runner restarts only that project's `agent-host`, confirms its container start time changed, and then runs the unchanged history assertions. This does not qualify a Core restart or a sandbox checkpoint restore. The `pending-actions` suite reconnects the public client, not the agent-host process, and rejects those restart options. Select cold recovery only where the [declaration and coverage ledger](./index.md#known-gaps) support it; unsupported recovery remains a gap, never a successful skipped check. An API rejection fails the selected suite.

The `none` suite validates required and nullable Session, Turn, Item and event fields against the pinned schema. When native Turns supply measured usage, it checks the terminal event against the stored Turn and sums the measurements into Session totals. Unknown usage remains `null` and each affected Turn is listed in the evidence’s `proof.unverified`; a passing suite does not claim native measurement support for those Turns.

Run `python -m unittest discover -s services/core/tests -p qualify_public_native_test.py` with the pinned SDK to check credential handling and the owned restart boundary without a model. Existing deterministic Core integration tests remain the authority for schema validation, atomic admission, durable receipts and rejection semantics. Real-model results qualify only the selected suite, protocol, Harness and placement. Provider lifecycle, native identity, credential isolation, deferred discovery and unselected suites need separate evidence; view-only results do not qualify the public path.

## Native version pins
Expand Down
6 changes: 4 additions & 2 deletions contracts/agents-api/zh/harness-onboarding.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "添加 Harness"
source: contracts/agents-api/harness-onboarding.md
source_hash: 32b85617efe41a23bd25768a42fd7a57019c75ae723d07d8cc5106124709f402
source_hash: 84ebd7cebed1995c89a59f8c4c71542ca41ef6ccc890b4eba96340a970ae161c
---

**Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。
Expand Down Expand Up @@ -226,7 +226,7 @@ python services/core/tests/qualify_public_native.py \

| 套件 | 操作 | 放置方式 |
| --- | --- | --- |
| `none` | 创建重试、外部历史拒绝、两个原生文本 Turn、历史回忆、SSE 顺序,以及 SDK/原始 Item 和 Turn 一致性 | `none` |
| `none` | 带初始输入的创建、重试和冲突保持单个 Turn,跨项目历史拒绝,两个原生文本 Turn,历史回忆,SSE 顺序,SDK/原始响应 schema 一致性与可空 usage 核算 | `none` |
| `pending-actions` | 查询和重连待处理调用、成功/错误结果、取消、精确目标拒绝、重试和持久化 Item | 声明支持函数工具的任意放置方式 |
| `functions` | SDK handler、成功/错误、原生文件和 Artifact 字节、继续执行、待处理调用取消和租户隔离 | 工作区 |
| `images` | 初始和活动图像、图像结果、重试/原子拒绝、原生文件/Artifact、隔离和继续执行 | 声明支持图像和函数的工作区 |
Expand All @@ -241,6 +241,8 @@ python services/core/tests/qualify_public_native.py \

默认验证热继续执行,并将冷恢复记录为 `unverified`。验证冷继续执行时,额外传入指向所拥有安装的绝对目录的 `--compose-directory`,以及指定其精确项目名称的 `--compose-project`。运行器仅重启该项目的 `agent-host`,确认容器启动时间已改变,然后执行相同的历史断言。这不能证明 Core 重启或 sandbox 检查点恢复。`pending-actions` 套件重连的是公共客户端,而非 agent-host 进程,因此拒绝这些重启选项。仅在[声明和覆盖台账](./index.md#known-gaps) 支持时选择冷恢复;不支持的恢复仍是缺口,不能把跳过的检查记为成功。API 拒绝会使所选套件失败。

`none` 套件依据固定版本 schema 校验 Session、Turn、Item 和事件的必需字段及可空字段。原生 Turn 提供用量测量时,它会对比终止事件与已存储 Turn,并将测量值求和核对 Session 总量。未知用量保留为 `null`,每个受影响 Turn 都列入证据的 `proof.unverified`;套件通过不代表这些 Turn 已验证原生用量测量支持。

使用锁定的 SDK 运行 `python -m unittest discover -s services/core/tests -p qualify_public_native_test.py`,可在不调用模型的情况下检查凭据处理和所拥有的重启边界。现有确定性 Core 集成测试仍负责 schema 验证、原子准入、持久化回执和拒绝语义。真实模型结果仅证明所选套件、协议、Harness 和放置方式。Provider 生命周期、原生身份、凭据隔离、延迟发现和未选择的套件需要独立证据;仅通过 view 测试不能证明公共调用路径。

## 原生版本固定 {#native-version-pins}
Expand Down
21 changes: 14 additions & 7 deletions services/core/tests/official_session_initial_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
from official_session_creation_stream import verify_creation_streams


def verify_initial_input_retry(sessions, raw, endpoint, headers, request, session_id):
"""Retry and conflict preserve the one Turn admitted by Session creation."""
turns = [turn.id for turn in sessions.turns.list(session_id)]
assert len(turns) == 1
reply = raw.post(endpoint, headers=headers, json=request)
assert reply.status_code == 201 and reply.json()["id"] == session_id
changed = raw.post(endpoint, headers=headers, json={**request, "input": "Changed " + uuid.uuid4().hex})
assert changed.status_code == 409
assert [turn.id for turn in sessions.turns.list(session_id)] == turns
return reply, changed


def main():
base, token, foreign, unsupported = sys.argv[1:]
headers = {"Authorization": "Bearer " + token, "OpenAI-Beta": "agents=v1"}
Expand Down Expand Up @@ -56,16 +68,11 @@ def main():
assert len(turns) == 1
items = list(sessions.items.list(session.id, order="asc"))
assert [item.content[0].text for item in items] == (["First"] if i == 0 else ["First", "Second"])
reply = raw.post(base + "/v1/agents/sessions", headers={**headers, **key},
json={**configuration, "input": initial})
assert reply.status_code == 201 and reply.json()["id"] == session.id
assert [turn.id for turn in sessions.turns.list(session.id)] == [turns[0].id]
verify_initial_input_retry(sessions, raw, base + "/v1/agents/sessions", {**headers, **key},
{**configuration, "input": initial}, session.id)
denied = raw.get(base + "/v1/agents/sessions/" + session.id,
headers={**headers, "Authorization": "Bearer " + foreign})
assert denied.status_code == 404
changed = raw.post(base + "/v1/agents/sessions", headers={**headers, **key},
json={**configuration, "input": "Changed"})
assert changed.status_code == 409
sessions.events.create(session.id, events=[{"type": "agent.session.input.cancel"}])
assert sessions.create(**configuration, input=initial, extra_headers=key).status == "idle"
assert len(list(sessions.turns.list(session.id))) == 1
Expand Down
113 changes: 93 additions & 20 deletions services/core/tests/qualify_public_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@

import httpx2
from openai import OpenAI
from jsonschema import Draft202012Validator

from official_hosted_functions_native import verify_hosted_functions
from official_hosted_structured_native import verify_hosted_structured
from official_pending_actions_native import verify_pending_actions
from official_workspace_images_native import verify_workspace_images
from official_schema import ResponseValidator
from official_session_initial_input import verify_initial_input_retry
from official_environment_composition import verify_composition
from session_cleanup import delete_session

Expand All @@ -33,46 +36,116 @@ def verify_none(client, foreign, http, agent_options, session_options, ready, re
sessions = client.beta.agents.sessions
marker = "memory-" + uuid.uuid4().hex
key = "create-" + uuid.uuid4().hex
session = sessions.create(agent=agent_options, **session_options, idempotency_key=key)
proof = {"checks": [], "session": session.id, "runs": []}
root = str(client.base_url).rstrip("/") + "/agents/sessions/" + session.id
initial = "Remember " + marker + ". Reply with exactly that string. Do not use tools."
request = {"agent": agent_options, **session_options, "input": initial}
request.update(request.pop("extra_body", {}))
endpoint = str(client.base_url).rstrip("/") + "/agents/sessions"
headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"}
validate = ResponseValidator(Path(__file__).resolve().parents[3] / "contracts/agents-api/openapi.yaml")
events_schema = Draft202012Validator({"components": validate.contract["components"],
"$ref": "#/components/schemas/SessionEvent"})
session = None
proof = {"checks": [], "runs": [], "snapshots": [], "unverified": []}
try:
assert sessions.create(agent=agent_options, **session_options, idempotency_key=key).id == session.id
ready(session)
for suffix in ("", "/items", "/turns"):
assert http.get(root + suffix, headers={**headers, "Authorization": "Bearer " + foreign.api_key}).status_code == 404
proof["checks"].append("create_retry_and_foreign_history_rejection")
for index, prompt in enumerate(("Remember " + marker + ". Reply with exactly that string. Do not use tools.",
"Recall the string from our previous turn. Reply with exactly that string. Do not use tools.")):
if index and restart is not None:
restart()
with sessions.stream(session.id, input=prompt, idempotency_key=key + str(index), timeout=240) as stream:
events = [event.to_dict() for event in stream]
for index in range(2):
if index == 0:
stream = sessions.create(agent=agent_options, **session_options, input=initial,
stream=True, extra_headers={"Idempotency-Key": key}, timeout=240)
else:
if restart is not None:
restart()
stream = sessions.stream(session.id,
input="Recall the string from our previous turn. Reply with exactly that string. Do not use tools.",
idempotency_key=key + "-continue", timeout=240)
events = []
proof["runs"].append(events)
with stream:
for event in stream:
value = event.to_dict()
events.append(value)
if index == 0 and len(events) == 1:
assert event.type == "agent.session.created"
session = event.session
proof["session"] = session.id
events_schema.validate(value)
ready(session)
root = endpoint + "/" + session.id
# Admission has committed before the first creation event.
for response in verify_initial_input_retry(sessions, http, endpoint,
{**headers, "Idempotency-Key": key}, request, session.id):
validate(response)
assert sessions.create(agent=agent_options, **session_options, input=initial,
extra_headers={"Idempotency-Key": key}).id == session.id
for suffix in ("", "/items", "/turns"):
response = http.get(root + suffix, headers={**headers, "Authorization": "Bearer " + foreign.api_key})
assert response.status_code == 404
validate(response)
else:
events_schema.validate(value)
record(proof)
types = [event["type"] for event in events]
terminals = [event for event in events if event["type"] in {
"agent.session.turn.completed", "agent.session.turn.failed", "agent.session.turn.cancelled"}]
assert len(terminals) == 1 and terminals[0]["type"] == "agent.session.turn.completed"
terminal = terminals[0]
assert types[-1] == "agent.session.idle"
assert types.count("agent.session.turn.created") == 1
assert types.index("agent.session.turn.created") < types.index("agent.session.turn.completed") < len(types) - 1
assert len({event["event_id"] for event in events}) == len(events)
assert all("usage" not in event for event in events if event is not terminal)
snapshots = {}
for name in ("items", "turns"):
response = http.get(root + "/" + name, headers=headers, params={"limit": 100, "order": "asc"})
assert response.status_code == 200 and not response.json()["has_more"]
validate(response)
expected = getattr(sessions, name).list(session.id, limit=100, order="asc").to_dict()
assert response.json() == expected
items = list(sessions.items.list(session.id, limit=100, order="asc"))
answers = [item.to_dict() for item in items if item.type == "message" and item.role == "assistant"]
snapshots[name] = response.json()["data"]
response = http.get(root, headers=headers)
assert response.status_code == 200
validate(response)
current = response.json()
assert current == sessions.retrieve(session.id).to_dict()
assert current["status"] == "idle" and current["required_actions"] == [] and current["error"] is None
turns = snapshots["turns"]
assert len(turns) == index + 1
assert all(turn["status"] == "completed" and turn["error"] is None and turn["subagent_id"] is None for turn in turns)
assert terminal["turn_id"] == turns[-1]["id"]
assert terminal["turn"] == turns[-1] and terminal["usage"] == turns[-1]["usage"]
answers = [item for item in snapshots["items"] if item["type"] == "message" and item["role"] == "assistant"]
assert marker in "".join(part.get("text", "") for part in answers[-1]["content"])
assert len(sessions.turns.list(session.id).data) == index + 1
assert sessions.retrieve(session.id).required_actions == []
proof["checks"].append(("cold_agent_host" if restart is not None else "warm") + "_text_history_sse_and_sdk_raw_parity")
measured = [turn["usage"] for turn in turns if turn["usage"] is not None]
for usage in measured:
assert usage["input_tokens"] > 0 and usage["output_tokens"] > 0
assert usage["total_tokens"] == usage["input_tokens"] + usage["output_tokens"]
assert 0 <= usage["input_tokens_details"]["cached_tokens"] <= usage["input_tokens"]
assert 0 <= usage["output_tokens_details"]["reasoning_tokens"] <= usage["output_tokens"]
if len(measured) == len(turns):
totals = {field: sum(usage[field] for usage in measured)
for field in ("input_tokens", "output_tokens", "total_tokens")}
for field, detail in (("input_tokens_details", "cached_tokens"), ("output_tokens_details", "reasoning_tokens")):
totals[field] = {detail: sum(usage[field][detail] for usage in measured)}
assert current["usage"] == totals
else:
assert current["usage"] is None
for turn in turns:
if turn["usage"] is None:
gap = "Native measured usage unavailable for Turn " + turn["id"]
if gap not in proof["unverified"]:
proof["unverified"].append(gap)
proof["snapshots"].append({**snapshots, "session": current})
if index == 0:
for response in verify_initial_input_retry(sessions, http, endpoint,
{**headers, "Idempotency-Key": key}, request, session.id):
validate(response)
proof["checks"].append("initial_input_create_retry_conflict_one_native_turn_and_foreign_history_rejection")
proof["checks"].append("native_usage_measurements_or_explicit_unknown_and_session_totals")
proof["checks"].append(("cold_agent_host" if restart is not None else "warm") + "_text_history_sse_and_sdk_raw_schema_parity")
return proof["checks"]
finally:
record(proof)
delete_session(sessions, session.id)
if session is not None:
delete_session(sessions, session.id)


def main():
Expand Down
Loading