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
37 changes: 27 additions & 10 deletions docs/execution-evidence-spine.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Execution Identity and Evidence Spine

Status: Phase 0 contract, Phase 1 Runtime-to-Task lineage, and Phase 2A Runtime provenance for compact task evidence. See [issue #948](https://github.com/maka-agent/maka-agent/issues/948).
Status: Phase 0 contract, Phase 1 Runtime-to-Task lineage, and Phase 2A-2C evidence freshness and Compaction coverage. See [issue #948](https://github.com/maka-agent/maka-agent/issues/948).

Maka already records the facts needed to explain an execution. Runtime Events preserve model and tool interaction facts, AgentRun records operational lifecycle, and Task Events preserve durable task-control decisions. The missing piece is a shared way to reference those facts across subsystem boundaries.

Expand Down Expand Up @@ -41,7 +41,23 @@ Phase 2A binds compact heavy-task evidence back to executor-owned Runtime facts:

System-created artifacts, external verifier artifacts, and replay-derived Self-check envelopes keep their existing authority records. Phase 2A does not invent a Runtime source for facts that were not durably recorded from a Runtime tool call.

The current delivery still does not add Task Event cursors, Self-check freshness or workspace invalidation, Compaction integration, AHE lineage, or a general inspection command.
Phase 2B makes accepted heavy-task Self-check freshness replay-derived:

- Task Event stores expose stable zero-based append cursors;
- system-owned provenance binds the Self-check to its exact Runtime call/result range, Task Event high water, and post-run workspace manifest revision;
- replay reports `current`, `stale`, or backward-compatible `unknown` freshness;
- later mutation evidence invalidates a bound Self-check until a matching workspace observation revalidates it;
- the completion gate rejects explicitly stale Self-checks without pretending that legacy evidence is fresh.

Phase 2C binds each newly written Compaction checkpoint to the exact ordered Runtime Event projection it summarizes:

- `maka.compactable_runtime_event_projection.v1` names the session-scoped ordering and filtering policy;
- inclusive low/high cursors identify the first and last source Runtime Event, while the existing digest still validates the complete ordered prefix;
- prefix matching returns the successor Runtime Events that remain outside the lossy projection;
- legacy V2 checkpoints remain readable, but a source-bound checkpoint cannot be replaced or recovered behind a legacy checkpoint that cannot prove cursor semantics;
- the canonical Runtime Event ledger remains untouched and authoritative.

The current delivery still does not add AHE lineage, recovery modeling for ambiguous external side effects, or a general inspection command.

## Existing authorities

Expand Down Expand Up @@ -113,7 +129,7 @@ An ordered cursor has three required coordinates:

```ts
interface ExecutionLogCursor {
ledger: 'runtime_event' | 'task_event';
ledger: 'runtime_event' | 'runtime_event_projection' | 'task_event';
streamId: string;
sequence: number;
eventId?: string;
Expand All @@ -133,9 +149,10 @@ The planned stream bindings are:
| Ledger | `streamId` |
| --- | --- |
| `runtime_event` | `execution.agentRunId` |
| `runtime_event_projection` | session id, interpreted only with its adjacent projection policy version |
| `task_event` | `task.taskRunId` |

Phase 0 defines these semantics but does not claim that current stores already persist the ordinal. Existing Runtime Event ids are best-effort unique identifiers, not durable ordered cursors. Existing Compaction `highWaterSeq` values remain policy-local until a later integration explicitly maps them to source-log coverage.
Canonical Runtime and Task cursors use physical append ordinals. A `runtime_event_projection` cursor is intentionally different: it is an ordinal in a named, versioned projection whose owner must publish the ordering and filtering policy beside the cursor. Cursors from that projection are therefore incomparable with canonical `runtime_event` cursors even when their boundary `eventId` values happen to match. Existing Runtime Event ids remain audit pointers, not ordering fields, and Compaction's legacy `highWaterSeq` remains a checkpoint-local value rather than a source-log cursor.

For Phase 1 headless lineage, the persisted AgentRun Runtime Event JSONL is the ordered stream. Mutable partial snapshot files are excluded, while every physical JSONL row—including a lifecycle row that may carry `partial: true`—retains its append position. Those immutable positions are materialized as zero-based cursor sequences. A completed invocation therefore records coverage such as:

Expand All @@ -149,6 +166,8 @@ The Task Event stores only these references and boundary event ids. Model messag

Phase 2A applies the same rule at evidence granularity. A compact Task evidence envelope may display a bounded summary, but its `provenance` points to the immutable Runtime call/result range that owns the exact request and response. Maka requires the canonical `function_response` before creating that link; a planned or interrupted call alone is not proof of an executor result.

Phase 2C applies the cursor contract without falsely describing the Compaction input as one AgentRun append log. Compaction operates on the session/model-context Runtime Event projection, so its checkpoint carries `runtime_event_projection` cursors plus `maka.compactable_runtime_event_projection.v1`. The policy fixes the projection's ordering/filtering semantics; the boundary ids and source digest then fail closed if replay no longer presents the exact covered prefix. Events after the high-water cursor are returned as explicit successor facts and remain raw in provider-visible context.

Coverage is an inclusive range within one stream:

```ts
Expand Down Expand Up @@ -218,12 +237,10 @@ This object says where evidence came from. It does not assert that the evidence

Later phases should extend the contract without changing fact ownership:

1. Materialize stable append ordinals for Task Event streams, including backward-compatible reads.
2. Bind workspace observations and artifacts produced outside Runtime tool calls to their appropriate source authorities.
3. Bind Self-check to Runtime and Task high waters plus a workspace revision, then define deterministic staleness rules.
4. Map Compaction source coverage to the shared cursor contract while retaining explicit source-event validation.
5. Carry target snapshot and execution lineage through AHE exports.
6. Add human-readable and machine-readable inspection surfaces that expose missing, stale, conflicting, or ambiguous lineage.
1. Bind workspace observations and artifacts produced outside Runtime tool calls to their appropriate source authorities.
2. Carry target snapshot and execution lineage through AHE exports.
3. Represent uncertain external-side-effect/commit windows in durable recovery lineage.
4. Add human-readable and machine-readable inspection surfaces that expose missing, stale, conflicting, or ambiguous lineage.

The guiding invariant is simple:

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/__tests__/execution-evidence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ describe('execution evidence spine contract', () => {
assert.equal(compareExecutionLogCursors(first, { ...first, eventId: 'other-event' }), 'conflict');
assert.equal(compareExecutionLogCursors(first, { ...first, streamId: 'run-2' }), 'incomparable');
assert.equal(compareExecutionLogCursors(first, taskCursor(1, 'task-event-1')), 'incomparable');
assert.equal(compareExecutionLogCursors(first, {
...first,
ledger: 'runtime_event_projection',
}), 'incomparable');
});
});

Expand Down
16 changes: 11 additions & 5 deletions packages/core/src/execution-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

export const EXECUTION_EVIDENCE_REF_SCHEMA_VERSION = 'maka.execution_evidence_ref.v1' as const;

export const EXECUTION_LOG_LEDGERS = ['runtime_event', 'task_event'] as const;
export const EXECUTION_LOG_LEDGERS = [
'runtime_event',
'runtime_event_projection',
'task_event',
] as const;
export type ExecutionLogLedger = typeof EXECUTION_LOG_LEDGERS[number];

export const WORKSPACE_REVISION_KINDS = [
Expand Down Expand Up @@ -44,12 +48,14 @@ export interface TaskIdentityRef {
}

/**
* Ordered position in one append-only log stream.
* Ordered position in one log or explicitly versioned projection stream.
*
* `sequence` is the zero-based append ordinal within (`ledger`, `streamId`).
* It is the only ordering field. `eventId` is an optional audit/dedup pointer
* and MUST NOT be used to order events. Cursors from different streams are
* incomparable.
* For canonical Runtime/Task ledgers it is the append ordinal. A
* `runtime_event_projection` owner MUST publish the ordering/filter policy
* beside the cursor. `sequence` is the only ordering field; `eventId` is an
* optional audit/dedup pointer and MUST NOT be used to order events. Cursors
* from different streams are incomparable.
*/
export interface ExecutionLogCursor {
ledger: ExecutionLogLedger;
Expand Down
80 changes: 78 additions & 2 deletions packages/runtime/src/__tests__/history-compact-checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, test } from 'node:test';
import type { AgentRunEvent, AgentRunHeader, AgentRunStore } from '@maka/core';
import type { RuntimeEvent } from '@maka/core/runtime-event';
import {
HISTORY_COMPACT_SOURCE_POLICY_VERSION,
buildHistoryCompactCheckpoint,
canReplaceHistoryCompactCheckpoint,
historyCompactCheckpointToRuntimeEvent,
Expand All @@ -27,7 +28,25 @@ describe('history compact checkpoint', () => {
assert.equal(checkpoint.coverage.eventCount, 10_000);
assert.equal(checkpoint.coverage.turnCount, 5_000);
assert.equal(checkpoint.coverage.through.runtimeEventId, 'event-9999');
assert.equal(matchHistoryCompactCheckpointPrefix(checkpoint, events).coveredEventCount, 10_000);
assert.equal(checkpoint.source?.policyVersion, HISTORY_COMPACT_SOURCE_POLICY_VERSION);
assert.deepEqual(checkpoint.source?.coverage, {
lowWater: {
ledger: 'runtime_event_projection',
streamId: 'session-1',
sequence: 0,
eventId: 'event-0',
},
highWater: {
ledger: 'runtime_event_projection',
streamId: 'session-1',
sequence: 9_999,
eventId: 'event-9999',
},
eventCount: 10_000,
});
const prefixMatch = matchHistoryCompactCheckpointPrefix(checkpoint, [...events, textEvent(10_000)]);
assert.equal(prefixMatch.coveredEventCount, 10_000);
assert.deepEqual(prefixMatch.successorRuntimeEvents.map((event) => event.id), ['event-10000']);
const replay = applyRuntimeEventHistoryCompact([...events, textEvent(10_000)], {
maxHistoryEstimatedTokens: 1_000_000,
charsPerToken: 1,
Expand All @@ -50,7 +69,7 @@ describe('history compact checkpoint', () => {
assert.equal(matchHistoryCompactCheckpointPrefix(checkpoint, changed).reason, 'source_hash_mismatch');
assert.equal(
matchHistoryCompactCheckpointPrefix(checkpoint, [events[1]!, events[0]!, ...events.slice(2)]).reason,
'source_hash_mismatch',
'coverage_miss',
);
});

Expand All @@ -62,6 +81,40 @@ describe('history compact checkpoint', () => {
}), /non-empty summary/);
});

test('rejects a source projection assembled from more than one session', () => {
assert.throws(() => buildHistoryCompactCheckpoint({
sessionId: 'session-1',
coveredRuntimeEvents: [textEvent(0), { ...textEvent(1), sessionId: 'session-2' }],
summary: 'mixed source',
}), /one session/);
});

test('keeps legacy V2 checkpoints readable but rejects inconsistent projection cursors', () => {
const events = [textEvent(0), textEvent(1)];
const checkpoint = buildHistoryCompactCheckpoint({
sessionId: 'session-1', coveredRuntimeEvents: events, summary: 'source-bound',
});
const { source: _source, ...legacy } = checkpoint;
assert.equal(validateHistoryCompactCheckpointShape(legacy, 'session-1'), true);
assert.equal(
matchHistoryCompactCheckpointPrefix(legacy as typeof checkpoint, events).coveredEventCount,
events.length,
);

const invalid = {
...checkpoint,
source: {
...checkpoint.source!,
coverage: {
...checkpoint.source!.coverage,
highWater: { ...checkpoint.source!.coverage.highWater, sequence: 99 },
},
},
};
assert.equal(validateHistoryCompactCheckpointShape(invalid, 'session-1'), false);
assert.equal(matchHistoryCompactCheckpointPrefix(invalid, events).reason, 'invalid_checkpoint');
});

test('only accepts an equal-coverage checkpoint as an explicit successor of the same source', () => {
const source = [textEvent(0), textEvent(1)];
const current = buildHistoryCompactCheckpoint({
Expand All @@ -83,6 +136,8 @@ describe('history compact checkpoint', () => {
assert.equal(canReplaceHistoryCompactCheckpoint(current, successor), true);
assert.equal(canReplaceHistoryCompactCheckpoint(current, stale), false);
assert.equal(canReplaceHistoryCompactCheckpoint(current, differentSource), false);
const { source: _source, ...legacySuccessor } = successor;
assert.equal(canReplaceHistoryCompactCheckpoint(current, legacySuccessor as typeof successor), false);
});

test('loads the latest valid checkpoint from the run ledger', async () => {
Expand Down Expand Up @@ -138,6 +193,27 @@ describe('history compact checkpoint', () => {
assert.equal(loaded?.checkpointId, furthest.checkpointId);
});

test('prefers source-bound recovery over a legacy checkpoint that cannot prove cursor ordering', async () => {
const sourceBound = buildHistoryCompactCheckpoint({
sessionId: 'session-1', coveredRuntimeEvents: [textEvent(0), textEvent(1)], summary: 'bound',
});
const legacyBuilt = buildHistoryCompactCheckpoint({
sessionId: 'session-1', coveredRuntimeEvents: [textEvent(0), textEvent(1), textEvent(2)], summary: 'legacy',
});
const { source: _source, ...legacy } = legacyBuilt;
const store = new StubAgentRunStore([
run('run-bound', 10),
run('run-legacy', 20),
], new Map([
['run-bound', [checkpointEvent('ledger-bound', 'run-bound', sourceBound, 10)]],
['run-legacy', [checkpointEvent('ledger-legacy', 'run-legacy', legacy as typeof legacyBuilt, 20)]],
]));

const loaded = await loadLatestHistoryCompactCheckpointFromRunLedger(store, 'session-1');

assert.equal(loaded?.checkpointId, sourceBound.checkpointId);
});

test('recovers the tip of an out-of-order same-coverage successor chain across runs', async () => {
const source = [textEvent(0), textEvent(1)];
const first = buildHistoryCompactCheckpoint({
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/ai-sdk-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2247,7 +2247,7 @@ export class AiSdkBackend implements AgentBackend {
: undefined;
const previousCheckpoint = checkpointMatch && !checkpointMatch.reason ? loadedCheckpoint : undefined;
const newlyFoldedRuntimeEvents = previousCheckpoint
? foldedRuntimeEvents.slice(checkpointMatch!.coveredEventCount)
? checkpointMatch!.successorRuntimeEvents
: foldedRuntimeEvents;
const retainedRuntimeEvents = input.priorRuntimeContext.filter((event) =>
!foldedIds.has(event.id) && !event.id.startsWith('history-compact:')
Expand Down
3 changes: 1 addition & 2 deletions packages/runtime/src/context-budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,7 @@ export function applyRuntimeEventHistoryCompact(
if (match.reason) {
increment(skippedReasonCounts, match.reason);
} else {
const uncoveredFoldedEvents = foldedEvents.slice(match.coveredEventCount);
const replayTail = [...uncoveredFoldedEvents, ...retainedEvents];
const replayTail = [...match.successorRuntimeEvents, ...retainedEvents];
const fit = evaluateHistoryCompactCheckpointReplay(checkpoint, replayTail, policy!, {
charsPerToken,
maxHistoryEstimatedTokens: maxTokens,
Expand Down
Loading
Loading