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
33 changes: 32 additions & 1 deletion benchmarks/harbor/tests/test_atif_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from pathlib import Path

import pytest
from harbor.utils.trajectory_validator import TrajectoryValidator
from nanopycodeagent.atif import project_atif
from nanopycodeagent.event_journal import EventJournal
from nanopycodeagent.event_journal import EventJournal, NativeEvent


def test_projector_output_passes_harbor_atif_validator():
Expand All @@ -13,3 +14,33 @@ def test_projector_output_passes_harbor_atif_validator():
validator = TrajectoryValidator()

assert validator.validate(trajectory), validator.get_errors()


@pytest.mark.parametrize("content", [
[{"type": "text", "text": "Partial answer"}],
[{"type": "extension", "namespace": "anthropic", "source_type": "thinking",
"value": {"type": "thinking", "thinking": "Still analyzing", "signature": ""}}],
[{"type": "tool_call", "tool_call_id": "call-1", "tool_name": "write", "input": {}}],
[],
])
def test_truncated_v2_journal_passes_harbor_atif_validator(tmp_path, content):
fixture = Path(__file__).parent / "fixtures" / "atif-journal-v1.jsonl"
with EventJournal.create("run-truncated", directory=tmp_path) as journal:
for entry in EventJournal.replay(fixture):
if entry.type.startswith("tool."):
continue
payload = entry.payload
if entry.type == "model.completed":
payload = payload | {
"stop_reason": "max_tokens", "content": content,
"tool_calls": [block for block in content if block["type"] == "tool_call"],
}
elif entry.type == "run.completed":
payload = payload | {"outcome": "response_truncated"}
journal.append(NativeEvent(entry.type, payload))

trajectory = project_atif(EventJournal.replay(journal.path))
validator = TrajectoryValidator()
assert validator.validate(trajectory), validator.get_errors()
assert trajectory["extra"]["terminal"]["outcome"] == "response_truncated"
assert "observation" not in trajectory["steps"][1]
7 changes: 7 additions & 0 deletions docs/changelogs/0.8.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes in the **0.8.x** release series are documented here.

## [Unreleased]

### Fixed
- Stop explicitly when a model response reaches `max_tokens`, reporting
`response_truncated` in Event Journals and ATIF trajectories instead of task
completion. Preserve partial output, usage, and cost accounting; skip tools
from the truncated reply and keep subsequent interactive requests valid.
New journals use schema v2, with v1 replay still supported.

### Changed
- Moved detailed CLI and configuration guidance out of the bilingual READMEs
into dedicated English and Chinese user references, keeping the READMEs
Expand Down
3 changes: 3 additions & 0 deletions docs/dev_docs/en/event-journal-protocol-v1.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Event Journal Implementation Protocol v1

> This page preserves the historical v1 contract. The current writer uses
> [v2](event-journal-protocol-v2.md); the reader still supports v1.

> Generated from the Chinese source
> [`../zh-CN/event-journal-protocol-v1.md`](../zh-CN/event-journal-protocol-v1.md).
> Do not edit by hand.
Expand Down
80 changes: 80 additions & 0 deletions docs/dev_docs/en/event-journal-protocol-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Event Journal Implementation Protocol v2

> Generated from the Chinese source
> [`../zh-CN/event-journal-protocol-v2.md`](../zh-CN/event-journal-protocol-v2.md).
> Do not edit by hand.

v2 is implemented and is the internal Journal protocol used by the current
writer, with `schema_version = 2`. This document defines all changes relative
to [v1](event-journal-protocol-v1.md). Envelope, event types, fields, validation,
ordering, persistence, and projection rules not listed here follow v1. Public
trajectories remain ATIF-v1.7.

## Response truncation outcome

`run.completed.payload.outcome` accepts these values:

| Value | Meaning |
| --- | --- |
| `completed` | The model ended its reply; this does not establish verifier success. |
| `max_turns_exhausted` | The final reply still requests tools, but no reply budget remains, so those tools are skipped. |
| `response_truncated` | The model returned `stop_reason="max_tokens"`, reaching its generation length limit, and the run stopped. |

`model.completed` means an API call returned its final message and usage. It
does not guarantee that the model finished its reply. Truncated replies still
produce this event, preserving `stop_reason="max_tokens"`, original content,
tool calls, usage, and available provider identifiers. Normal cost
reconciliation still runs during finalization.

The core recognizes truncation before checking the turn limit or executing
tools. A reply that also spends the last turn therefore records
`response_truncated`. All tools in that reply are skipped: there are no
`tool.started` or `tool.completed` events and no fabricated observations.
Existing text remains on stdout, the truncation diagnostic goes to stderr,
and headless mode exits `0`. The current policy stops without automatically
continuing, retrying, or raising the 8192-token limit.

This is a budget outcome with an explicit reason, represented by
`run.completed` rather than `run.failed`. ATIF's `extra.terminal.status`
remains `completed`, meaning the run finalized normally;
`extra.terminal.outcome = "response_truncated"` carries the specific result.
The corresponding model step has `extra.stop_reason = "max_tokens"`.
Consumers MUST inspect the outcome to identify truncation rather than infer
task completion from status alone.

Interactive mode returns to the input prompt. Request history retains only
the reply's text and an explicit truncation notice, removing unexecuted tool
calls and non-text blocks such as potentially unfinished thinking. This keeps
unmatched tool calls and incomplete signatures out of the next request. The
original model reply is still stored under the Journal persistence rules;
history repair does not rewrite runtime facts or invent tool execution events.

## Compatibility

- v1 has a closed outcome enum. Adding a value requires a schema increment,
rather than a v1 optional extension.
- The new writer emits `schema_version = 2` for all runs. The new reader/replay
implementation and ATIF projector support both v1 and v2. Existing v1
journals are not rewritten, and historical `completed` outcomes are not
reclassified retroactively.
- A v1 record containing `response_truncated` is rejected. Old readers
explicitly reject the v2 schema.
- Other unknown schemas are still rejected. The remaining v1 compatibility
rules continue to apply.
- The top-level `truncation` field retains its meaning for Journal string
persistence limits, which are independent of model generation length limits.

## Implementation and validation

- [`agent.py`](../../../src/nanopycodeagent/agent.py): explicit RunOutcome,
truncation termination, text diagnostics, and interactive history handling.
- [`event_journal.py`](../../../src/nanopycodeagent/event_journal.py): v2 writer,
v1/v2 replay, and outcome validation.
- [`atif.py`](../../../src/nanopycodeagent/atif.py): projection from both Journal
versions to ATIF-v1.7.
- [`test_truncation.py`](../../../tests/test_truncation.py): text, thinking,
empty replies, partial tool JSON, the final turn, costs, trajectories, and
the next interactive turn.
- [Harbor compatibility tests](../../../benchmarks/harbor/tests/test_atif_compatibility.py):
the old v1 fixture and new v2 truncated trajectories pass the pinned official
ATIF validator.
2 changes: 2 additions & 0 deletions docs/dev_docs/zh-CN/event-journal-protocol-v1.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Event Journal 实现协议 v1

> 此页保留历史 v1 契约;当前 writer 使用 [v2](event-journal-protocol-v2.md),reader 仍支持 v1。

> 本文件为**中文源文件**(source of truth);英文版
> [`../en/event-journal-protocol-v1.md`](../en/event-journal-protocol-v1.md)
> 由其生成。
Expand Down
60 changes: 60 additions & 0 deletions docs/dev_docs/zh-CN/event-journal-protocol-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Event Journal 实现协议 v2

> 本文件为**中文源文件**(source of truth);英文版
> [`../en/event-journal-protocol-v2.md`](../en/event-journal-protocol-v2.md) 由其生成。

v2 已实现,是当前 writer 使用的内部 Journal 协议,`schema_version = 2`。
本文完整定义相对 [v1](event-journal-protocol-v1.md) 的变化;未列出的 envelope、
事件类型、字段、校验、排序、持久化与投影规则沿用 v1。公开 trajectory 仍为 ATIF-v1.7。

## 回复截断终态

`run.completed.payload.outcome` 的允许值为:

| 值 | 含义 |
| --- | --- |
| `completed` | 模型结束回复;不代表任务通过 verifier。 |
| `max_turns_exhausted` | 最后一轮仍请求工具,已无下一轮预算,不执行这些工具。 |
| `response_truncated` | 模型返回 `stop_reason="max_tokens"`,本次生成达到长度上限,run 停止。 |

`model.completed` 表示一次 API 调用已返回最终消息与 usage,不保证模型完成了回复。
截断回复仍产生该事件,保留 `stop_reason="max_tokens"`、原始 content、tool calls、
usage 和已有 provider 标识。正常费用补查仍在 run 收尾时执行。

core 在检查轮数上限和执行工具之前识别截断,即使该回复恰好用尽最后一轮,也记录
`response_truncated`。该回复中的工具一律不执行,不产生 `tool.started` 或
`tool.completed`,也不伪造 observation。已有文本继续保留在 stdout,截断提示写入
stderr;headless 退出码为 `0`。当前策略是停止,不自动续写、重试或增大 8192 上限。

这属于有明确原因的预算终态,使用 `run.completed`,而非 `run.failed`。
ATIF 的 `extra.terminal.status` 仍为 `completed`,表示 run 已正常收尾;
`extra.terminal.outcome = "response_truncated"` 才是具体结果。
对应模型 step 的 `extra.stop_reason` 为 `max_tokens`。消费者判断是否截断时必须
读取 outcome,不能仅凭 status 推断任务完成。

交互模式返回输入提示符。供下一次请求使用的会话历史仅保留该回复的 text 与明确的
截断提示,移除未执行的工具调用和可能未完成的 thinking 等非文本 block,避免下一次
请求携带无对应结果的工具调用或不完整签名。原始模型回复仍按 Journal 持久化规则保存;
该历史修整不改写运行事实,不产生虚构的工具执行事件。

## 兼容性

- v1 的 outcome 枚举是封闭的,新增值需要提升 schema,而不是作为 v1 可选扩展。
- 新 writer 对所有 run 写入 `schema_version = 2`;新 reader/replay 与 ATIF projector
同时支持 v1 和 v2。已有 v1 Journal 不改写,历史 `completed` 也不会被追溯重分类。
- v1 记录中出现 `response_truncated` 会被拒绝;旧 reader 会明确拒绝 v2 schema。
- 其他未知 schema 仍被拒绝;v1 的其余兼容性规则继续适用。
- Journal 字符串持久化截断的顶层 `truncation` 字段保持原义,与模型生成长度上限
是两个独立概念。

## 实现与验证

- [`agent.py`](../../../src/nanopycodeagent/agent.py):显式 RunOutcome、截断停止、
文本提示与交互历史处理。
- [`event_journal.py`](../../../src/nanopycodeagent/event_journal.py):v2 writer、v1/v2
replay 与 outcome 校验。
- [`atif.py`](../../../src/nanopycodeagent/atif.py):两种 Journal 版本到 ATIF-v1.7 的投影。
- [`test_truncation.py`](../../../tests/test_truncation.py):文本、thinking、空回复、部分
工具 JSON、最后一轮、费用、轨迹与交互下一轮。
- [Harbor 兼容性测试](../../../benchmarks/harbor/tests/test_atif_compatibility.py):
旧 v1 fixture 与新 v2 截断轨迹通过固定版本的官方 ATIF validator。
Loading