Merge remote-tracking branch 'origin/master' into codex/pr48-repo-hardening-rfcs

# Conflicts:
#	docs/adr/README.md
#	docs/rfc/009-session-persistence-and-resumability.md
#	docs/rfc/README.md
#	docs/rfc/implemented/2026-06-11-doc-sync-enforcement.md
#	docs/rfc/proposed/2026-06-14-acp-agent-client-protocol.md
#	examples/acp-agent/tests/acp.e2e.ts
#	packages/acp/README.md
#	packages/acp/src/index.ts
#	packages/acp/tests/stream-update.spec.ts
#	packages/agent-loop/src/loop.ts
#	packages/tools/src/index.ts
This commit is contained in:
Tianyi Cui
2026-06-18 23:41:14 +08:00
104 changed files with 2586 additions and 590 deletions

View File

@@ -1,7 +1,7 @@
/**
* JSON-serializability validation for session event data.
*
* The session event log is the durable source of truth (ADR 0003/0018): every
* The session event log is the durable source of truth (the event-sourcing / session-persistence RFCs): every
* `event.data` must round-trip losslessly through JSON so any persistence
* backend can store and reload it byte-identically. This invariant belongs to
* the log itself — `Session.append` enforces it at the source, so a

View File

@@ -16,7 +16,7 @@
* 3. a `turn/end` carrying the merge-extensible `{ kind: 'interrupted' }` reason.
*
* The marker records that the turn was cut short by a crash, not completed by
* the model. See ADR 0018.
* the model. See the session-persistence RFC.
*
* Why the synthetic tool results matter: `deriveMessages()` renders the
* `tool-call` blocks inside a durable `assistant/message` but only emits a
@@ -79,6 +79,7 @@ export function interruptedTurnClosers(events: readonly SessionEvent[]): Session
openStep = event.data.step
break
case 'step/end':
pendingCalls.clear()
openStep = null
break
case 'assistant/message':

View File

@@ -102,7 +102,7 @@ export type TurnTrigger = TurnTriggerMap[keyof TurnTriggerMap]
* still wins). It is distinct from `completed` so a consumer (e.g. the ACP
* bridge mapping to `StopReason: 'max_tokens'`) can tell a clean stop from a
* truncated one. The next variants to add — when an adapter/loop first emits
* them — are `refusal` and `max_turn_requests` (both named by RFC 010 as ACP
* them — are `refusal` and `max_turn_requests` (both named by the ACP RFC as ACP
* stop reasons); no current adapter produces a `refusal` finish (unknown
* DeepSeek finish reasons collapse to `error`), so it is deliberately omitted
* until one does.
@@ -121,7 +121,7 @@ export interface TurnEndReasonMap {
* the crash) and are PRESERVED, not discarded: a single turn can be huge in a
* long-horizon task (many steps, large tool output), so truncating it would
* lose real work. The marker records that the turn was cut short, not that the
* model completed it. See ADR 0018.
* model completed it. See the session-persistence RFC.
*/
interrupted: { kind: 'interrupted' }
}

View File

@@ -1,5 +1,5 @@
/**
* Property-based tests for the Session event log (RFC 001 → ADR 0013).
* Property-based tests for the Session event log (the property-testing RFC).
*
* Generates arbitrary event logs and asserts the derivation invariants the
* agent loop and replay depend on: deriveMessages is deterministic and

View File

@@ -82,6 +82,21 @@ describe('interruptedTurnClosers', () => {
expect(closers.map(e => e.type)).toEqual(['step/end', 'turn/end'])
})
it('does NOT synthesize a result after the owning step already closed', () => {
const events: SessionEvent[] = [
userTurnStart(2, 0),
{ type: 'step/start', seq: 1, time: 1, data: { turn: 2, step: 1 } },
{ type: 'assistant/message', seq: 2, time: 2, data: { turn: 2, step: 1, content: [
{ type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' },
] } },
{ type: 'step/end', seq: 3, time: 3, data: { turn: 2, step: 1 } },
]
const closers = interruptedTurnClosers(events)
expect(closers.map(e => e.type)).toEqual(['turn/end'])
expect(closers[0]?.seq).toBe(4)
})
it('synthesizes results only for the still-open turn, not a committed earlier turn', () => {
// Turn 1 completed with its own tool call+result (balanced). Turn 2 crashed
// with an unanswered call. Only turn 2's call must get a synthetic result.