@openwop/openwop-conformance 2.40.3 → 2.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-context-budget-live.json +44 -0
- package/fixtures/conformance-context-budget-multiturn.json +1 -1
- package/fixtures.md +2 -1
- package/package.json +2 -2
- package/requirements.json +48 -48
- package/scenario-majors.json +5 -3
- package/schemas/CORPUS-STAMP.json +10 -10
- package/src/lib/context-budget.ts +198 -0
- package/src/lib/host-callback.ts +8 -0
- package/src/scenarios/context-budget-transcript-bound.test.ts +85 -183
- package/src/scenarios/context-summarization-replay.test.ts +81 -81
- package/src/scenarios/v2-bound-id-kinds.test.ts +2 -0
- package/src/scenarios/v2-durability-recovery.test.ts +2 -0
- package/src/scenarios/v2-terminal-event-once.test.ts +2 -0
- package/src/scenarios/v2-webhook-delivery-isolation.test.ts +2 -0
- package/src/scenarios/v2-webhook-delivery-shape.test.ts +2 -0
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
- package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +2 -0
- package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
* The RFC defers reference-host implementation; the witness comes from a host
|
|
24
24
|
* that runs real orchestrator turns and summarizes.
|
|
25
25
|
*
|
|
26
|
+
* STRENGTHENED 2.41.0 (the RFC 0111 correction on record, 2026-09-26). Equal
|
|
27
|
+
* `summaryRef`s prove the fork cited the same artifact; they do not prove the
|
|
28
|
+
* MODEL saw the same summary. When the transcript-window seam serves
|
|
29
|
+
* `entries[]` for both runs, the summary texts the host fed on the fork MUST be
|
|
30
|
+
* the source's, byte for byte — the replay MUST NOT re-summarize, on the text.
|
|
31
|
+
* Both majors: major 2 reads `summarization` by presence (the v2 schema has no
|
|
32
|
+
* `supported` seat) and records `inapplicable`, never a strict-mode failure,
|
|
33
|
+
* when it is absent. Gated on the live fixture `conformance-context-budget-live`.
|
|
34
|
+
*
|
|
26
35
|
* @see RFCS/0111-context-economy.md
|
|
27
36
|
* @see spec/v1/multi-agent-execution.md §"Context economy (RFC 0111)"
|
|
28
37
|
*/
|
|
@@ -36,122 +45,113 @@ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
|
36
45
|
import { queryTestEvents, type TestEvent } from '../lib/event-log-query.js';
|
|
37
46
|
import { req } from '../lib/requirement-ids.js';
|
|
38
47
|
import { softSkip } from '../lib/soft-skip.js';
|
|
48
|
+
import { seamsProfileAdvertised, targetMajor } from '../lib/seams.js';
|
|
49
|
+
import { familyAdvertised, v2Discovery } from '../lib/v2.js';
|
|
50
|
+
import { runsPath } from '../lib/memoryAttribution.js';
|
|
51
|
+
import { parseTranscriptWindow, summarizationDeclared, summaryTexts, type LogEvent } from '../lib/context-budget.js';
|
|
39
52
|
|
|
40
|
-
const FIXTURE = 'conformance-context-budget-
|
|
53
|
+
const FIXTURE = 'conformance-context-budget-live';
|
|
41
54
|
const PROFILE = 'openwop-context-summarization';
|
|
55
|
+
const ID = 'openwop.it.context-summarization-replay.replay-reuses-the-recorded-context-summarized-summaryref-never-re-summarizes';
|
|
56
|
+
const MAX_ITERATIONS_PROBED = 16;
|
|
42
57
|
|
|
43
|
-
interface SummarizationCap {
|
|
44
|
-
readonly supported?: boolean;
|
|
45
|
-
}
|
|
46
|
-
interface ContextBudgetCap {
|
|
47
|
-
readonly summarization?: SummarizationCap;
|
|
48
|
-
}
|
|
49
|
-
interface ExecutionModelCap {
|
|
50
|
-
readonly contextBudget?: ContextBudgetCap;
|
|
51
|
-
}
|
|
52
|
-
interface MultiAgentCap {
|
|
53
|
-
readonly executionModel?: ExecutionModelCap;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ── cast-free typed accessors (no `as`) ──────────────────────────────────
|
|
57
58
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
58
59
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
59
60
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return isString(v) ? v : undefined;
|
|
61
|
+
function recordAt(v: unknown, ...keys: string[]): Record<string, unknown> | undefined {
|
|
62
|
+
let cur: unknown = v;
|
|
63
|
+
for (const k of keys) cur = isRecord(cur) ? cur[k] : undefined;
|
|
64
|
+
return isRecord(cur) ? cur : undefined;
|
|
65
65
|
}
|
|
66
66
|
function stringArrayOf(v: unknown): string[] | undefined {
|
|
67
|
-
return Array.isArray(v) && v.every(
|
|
67
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string') ? v : undefined;
|
|
68
68
|
}
|
|
69
69
|
function runIdOf(v: unknown): string | undefined {
|
|
70
|
-
|
|
70
|
+
const r = isRecord(v) ? v['runId'] : undefined;
|
|
71
|
+
return typeof r === 'string' ? r : undefined;
|
|
71
72
|
}
|
|
72
73
|
function replayModesOf(v: unknown): string[] {
|
|
73
|
-
|
|
74
|
-
const replay = v['replay'];
|
|
75
|
-
if (!isRecord(replay)) return [];
|
|
76
|
-
return stringArrayOf(replay['modes']) ?? [];
|
|
74
|
+
return stringArrayOf(recordAt(v, 'replay')?.['modes']) ?? [];
|
|
77
75
|
}
|
|
78
|
-
|
|
79
76
|
/** A summary fingerprint: summaryRef plus the (ordered) replaced-turn ids. */
|
|
80
|
-
function summaryFingerprint(e: TestEvent): string | undefined {
|
|
81
|
-
const ref = stringOf(e.payload['summaryRef']);
|
|
82
|
-
const replaced = stringArrayOf(e.payload['replacedTurns']);
|
|
83
|
-
if (ref === undefined || replaced === undefined) return undefined;
|
|
84
|
-
return `${ref}::${replaced.join(',')}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
77
|
function summaryFingerprints(events: readonly TestEvent[]): string[] {
|
|
88
78
|
const out: string[] = [];
|
|
89
79
|
for (const e of events) {
|
|
90
80
|
if (e.type !== 'context.summarized') continue;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
const ref = e.payload['summaryRef'];
|
|
82
|
+
const replaced = stringArrayOf(e.payload['replacedTurns']);
|
|
83
|
+
expect(typeof ref === 'string' && replaced !== undefined, req(ID, 'RFC 0111', 'a context.summarized event MUST carry summaryRef + replacedTurns')).toBe(true);
|
|
84
|
+
if (typeof ref === 'string' && replaced !== undefined) out.push(`${ref}::${replaced.join(',')}`);
|
|
94
85
|
}
|
|
95
86
|
return out.sort();
|
|
96
87
|
}
|
|
88
|
+
const toLog = (events: readonly TestEvent[]): LogEvent[] => events.map((e) => ({ eventId: e.eventId, type: e.type, sequence: e.sequence, payload: e.payload }));
|
|
89
|
+
|
|
90
|
+
/** The summary texts the host fed across a run's iterations, via the seam; null when the seam or entries[] are not served. */
|
|
91
|
+
async function fedSummaryTexts(runId: string, log: readonly LogEvent[]): Promise<string[] | null> {
|
|
92
|
+
const out: string[] = [];
|
|
93
|
+
let served = false;
|
|
94
|
+
for (let iteration = 1; iteration <= MAX_ITERATIONS_PROBED; iteration += 1) {
|
|
95
|
+
const res = await driver.get(`/v1/host/sample/agent/transcript-window?runId=${encodeURIComponent(runId)}&iteration=${iteration}`);
|
|
96
|
+
if (res.status !== 200) break;
|
|
97
|
+
const w = parseTranscriptWindow(res.json);
|
|
98
|
+
if (w?.entries === undefined) return null;
|
|
99
|
+
served = true;
|
|
100
|
+
out.push(...summaryTexts(w, log));
|
|
101
|
+
}
|
|
102
|
+
return served ? out : null;
|
|
103
|
+
}
|
|
97
104
|
|
|
98
105
|
describe('context-summarization-replay (RFC 0111 §"Replay determinism")', () => {
|
|
99
106
|
it('replay reuses the recorded context.summarized summaryRef — never re-summarizes', async () => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
107
|
+
const major = targetMajor();
|
|
108
|
+
let wellKnown: unknown;
|
|
109
|
+
if (major === 2) {
|
|
110
|
+
const doc = await v2Discovery();
|
|
111
|
+
if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
|
|
112
|
+
const cb = recordAt(await familyAdvertised('multiAgent'), 'executionModel', 'contextBudget');
|
|
113
|
+
if (!summarizationDeclared(cb, 2)) return softSkip('inapplicable', 'multiAgent.executionModel.contextBudget.summarization is not advertised at major 2');
|
|
114
|
+
if (!seamsProfileAdvertised(doc)) return softSkip('inapplicable', 'the run event-log is read through the seams profile — conformance.seamsProfile is not openwop-conformance-seams-v2');
|
|
115
|
+
wellKnown = doc;
|
|
116
|
+
} else {
|
|
117
|
+
const cb = recordAt(await readCapabilityFamily<Record<string, unknown>>('multiAgent'), 'executionModel', 'contextBudget');
|
|
118
|
+
if (!behaviorGate(PROFILE, summarizationDeclared(cb, 1))) return;
|
|
119
|
+
wellKnown = (await driver.get('/.well-known/openwop')).json;
|
|
120
|
+
}
|
|
121
|
+
if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', `the live fixture ${FIXTURE} is not advertised — RFC 0111 §Scope forbids a mock supervisor from advertising contextBudget`);
|
|
104
122
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
expect(create.status).toBe(201);
|
|
123
|
+
const create = await driver.post(runsPath(), { workflowId: FIXTURE });
|
|
124
|
+
expect(create.status, req(ID, 'RFC 0111', `POST ${runsPath()} MUST create the live-fixture run`)).toBe(201);
|
|
108
125
|
const sourceRunId = runIdOf(create.json);
|
|
109
|
-
expect(sourceRunId, req(
|
|
110
|
-
if (sourceRunId === undefined) return softSkip('blocked', '
|
|
126
|
+
expect(sourceRunId, req(ID, 'rest-endpoints.md POST /v1/runs', 'the create response MUST carry a runId')).toBeDefined();
|
|
127
|
+
if (sourceRunId === undefined) return softSkip('blocked', 'no runId');
|
|
111
128
|
await pollUntilTerminal(sourceRunId);
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (!sourceQ.ok) return softSkip('blocked', 'precondition not met — `!sourceQ.ok` returned early (event-log seam unwired — soft-skip) (seam, prior step, or fixture unavailable)'); // event-log seam unwired — soft-skip
|
|
130
|
+
const sourceQ = await queryTestEvents(sourceRunId);
|
|
131
|
+
if (!sourceQ.ok) return softSkip('blocked', 'the run event-log seam is unavailable');
|
|
116
132
|
const sourceFingerprints = summaryFingerprints(sourceQ.events);
|
|
117
133
|
if (sourceFingerprints.length === 0) {
|
|
118
|
-
|
|
119
|
-
// to prove about reuse. Honest soft-skip; not a vacuous pass of the MUST.
|
|
120
|
-
// eslint-disable-next-line no-console
|
|
121
|
-
console.warn(`[${PROFILE}] run produced no context.summarized events; replay-reuse leg soft-skipped`);
|
|
122
|
-
return softSkip('blocked', 'precondition not met — `sourceFingerprints.length === 0` returned early ([…] run produced no context.summarized events; replay-reuse leg soft-skipped) (seam, prior step, or fixture unavailable)');
|
|
134
|
+
return softSkip('blocked', 'the live run produced no context.summarized event — a host advertising summarization must summarize this fixture for reuse to be observable');
|
|
123
135
|
}
|
|
136
|
+
if (!replayModesOf(wellKnown).includes('replay')) return softSkip('inapplicable', 'the host advertises no replay fork mode');
|
|
124
137
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const fork = await driver.post(
|
|
130
|
-
`/v1/runs/${encodeURIComponent(sourceRunId)}:fork`,
|
|
131
|
-
{ fromSeq: 0, mode: 'replay' },
|
|
132
|
-
);
|
|
133
|
-
if (fork.status === 501 || fork.status === 404) return softSkip('blocked', 'precondition not met — `fork.status === 501 || fork.status === 404` returned early (replay not implemented for this run — soft-skip) (seam, prior step, or fixture unavailable)'); // replay not implemented for this run — soft-skip
|
|
134
|
-
expect(
|
|
135
|
-
fork.status,
|
|
136
|
-
req('openwop.it.context-summarization-replay.replay-reuses-the-recorded-context-summarized-summaryref-never-re-summarizes', 'rest-endpoints.md POST /v1/runs/{runId}:fork', 'replay fork MUST return 201'),
|
|
137
|
-
).toBe(201);
|
|
138
|
+
const fork = await driver.post(`${runsPath()}/${encodeURIComponent(sourceRunId)}:fork`, { fromSeq: 0, mode: 'replay' });
|
|
139
|
+
if (fork.status === 501 || fork.status === 404) return softSkip('blocked', `replay fork answered ${fork.status}`);
|
|
140
|
+
expect(fork.status, req(ID, 'rest-endpoints.md POST /v1/runs/{runId}:fork', 'replay fork MUST return 201')).toBe(201);
|
|
138
141
|
const forkRunId = runIdOf(fork.json);
|
|
139
|
-
expect(forkRunId, req(
|
|
140
|
-
if (forkRunId === undefined) return softSkip('blocked', '
|
|
142
|
+
expect(forkRunId, req(ID, 'rest-endpoints.md POST /v1/runs/{runId}:fork', 'replay fork MUST return a runId')).toBeDefined();
|
|
143
|
+
if (forkRunId === undefined) return softSkip('blocked', 'no fork runId');
|
|
141
144
|
await pollUntilTerminal(forkRunId);
|
|
142
145
|
|
|
143
|
-
const forkQ = await queryTestEvents(forkRunId
|
|
144
|
-
if (!forkQ.ok) return softSkip('blocked', '
|
|
145
|
-
|
|
146
|
+
const forkQ = await queryTestEvents(forkRunId);
|
|
147
|
+
if (!forkQ.ok) return softSkip('blocked', 'the event-log seam is unavailable for the fork');
|
|
148
|
+
expect(summaryFingerprints(forkQ.events), req(ID, 'RFC 0111 §"Replay determinism"', 'a replay fork MUST reuse the recorded context.summarized summaryRef (never re-summarize to a different transcript)')).toEqual(sourceFingerprints);
|
|
146
149
|
|
|
147
|
-
// The
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
'a replay fork MUST reuse the recorded context.summarized summaryRef (never re-summarize to a different transcript)',
|
|
154
|
-
),
|
|
155
|
-
).toEqual(sourceFingerprints);
|
|
150
|
+
// The model-facing half: the summary TEXT the host fed on the fork.
|
|
151
|
+
const sourceTexts = await fedSummaryTexts(sourceRunId, toLog(sourceQ.events));
|
|
152
|
+
const forkTexts = await fedSummaryTexts(forkRunId, toLog(forkQ.events));
|
|
153
|
+
if (sourceTexts === null || forkTexts === null) return softSkip('inapplicable', 'the transcript-window seam serves no entries[] for these runs, so the model-facing summary text was not compared (summaryRef reuse was)');
|
|
154
|
+
expect(sourceTexts.length, req(ID, 'RFC 0111 §"Replay determinism"', 'the source run summarized, so its transcript windows MUST carry the summary text it fed')).toBeGreaterThan(0);
|
|
155
|
+
expect(forkTexts, req(ID, 'RFC 0111 §"Replay determinism"', 'the replay MUST feed the model the recorded summary text, byte for byte — never a re-summarization')).toEqual(sourceTexts);
|
|
156
156
|
});
|
|
157
157
|
});
|
|
@@ -36,6 +36,8 @@ import { scaledTimeoutMs } from '../lib/polling.js';
|
|
|
36
36
|
import { req } from '../lib/requirement-ids.js';
|
|
37
37
|
import { projectBoundId, BOUND_ID } from '../lib/bound-id.js';
|
|
38
38
|
|
|
39
|
+
export const REQUIRES_HOST_CALLBACK = 'the host POSTs a webhook delivery to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
|
|
40
|
+
|
|
39
41
|
const DOC = 'spec/v2/core/identity.md §5 (RFC 0187 §A)';
|
|
40
42
|
const WEBHOOK_ID = 'openwop.requirement.0187.bound-id-kinds.webhook';
|
|
41
43
|
const PER_KIND = 'openwop.requirement.0187.bound-id-kinds.per-kind';
|
|
@@ -75,6 +75,8 @@ import { startEffectReceiver, waitForFirstArrival } from '../lib/effect-receiver
|
|
|
75
75
|
import { watchForResumption, type Observation, type Watch } from '../lib/durability-watch.js';
|
|
76
76
|
import { noteEvidence, parseRecoveryBounds, EVIDENCE_NAME_PATTERN } from '../lib/durability-evidence.js';
|
|
77
77
|
|
|
78
|
+
export const REQUIRES_HOST_CALLBACK = 'the host makes an outbound effect call to the suite-owned effect receiver (OPENWOP_WEBHOOK_RECEIVER_PORT)';
|
|
79
|
+
|
|
78
80
|
const FIXTURE = 'conformance-noop';
|
|
79
81
|
const FAILURE_FIXTURE = 'conformance-failure';
|
|
80
82
|
|
|
@@ -39,6 +39,8 @@ import { req } from '../lib/requirement-ids.js';
|
|
|
39
39
|
import { startEffectReceiver, waitForFirstArrival } from '../lib/effect-receiver.js';
|
|
40
40
|
import { terminalShapeViolation, TERMINAL_RUN_EVENTS } from '../lib/terminal-shape.js';
|
|
41
41
|
|
|
42
|
+
export const REQUIRES_HOST_CALLBACK = 'the host makes an outbound effect call to the suite-owned effect receiver (OPENWOP_WEBHOOK_RECEIVER_PORT)';
|
|
43
|
+
|
|
42
44
|
const ONCE = 'openwop.requirement.0194.terminal-once';
|
|
43
45
|
const DUP = 'openwop.requirement.0194.terminal-once.duplicate-delivery';
|
|
44
46
|
const DOC = 'spec/v2/core/events.md §The terminal event (RFC 0194 §A)';
|
|
@@ -55,6 +55,8 @@ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
|
55
55
|
import { req } from '../lib/requirement-ids.js';
|
|
56
56
|
import { RETRY_WAIT_FLOOR_MS } from '../lib/webhook-retry-window.js';
|
|
57
57
|
|
|
58
|
+
export const REQUIRES_HOST_CALLBACK = 'the host POSTs webhook deliveries to nine suite-owned subscriptions on the scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL, eight of which the suite holds open';
|
|
59
|
+
|
|
58
60
|
const ID = 'openwop.requirement.0215.no-head-of-line';
|
|
59
61
|
const DOC = 'webhooks.md §Durability (RFC 0215 §A.1, §A.2)';
|
|
60
62
|
const FIXTURE = 'conformance-delay';
|
|
@@ -57,6 +57,8 @@ import { req } from '../lib/requirement-ids.js';
|
|
|
57
57
|
import { era2Gate, seedEra2Log, v1FixtureLog } from '../lib/era2-seed.js';
|
|
58
58
|
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
59
59
|
|
|
60
|
+
export const REQUIRES_HOST_CALLBACK = 'the host POSTs a webhook delivery to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
|
|
61
|
+
|
|
60
62
|
const ID = 'openwop.requirement.0171.webhook-delivery-shape';
|
|
61
63
|
const DOC = 'spec/v2/core/webhooks.md §Delivery';
|
|
62
64
|
const FIXTURE = 'conformance-noop';
|
|
@@ -37,6 +37,8 @@ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
|
37
37
|
import { req } from '../lib/requirement-ids.js';
|
|
38
38
|
import { retryWaitCapMs, retryWaitFor, windowClosedNote } from '../lib/webhook-retry-window.js';
|
|
39
39
|
|
|
40
|
+
export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
|
|
41
|
+
|
|
40
42
|
const FIXTURE = 'conformance-noop';
|
|
41
43
|
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
42
44
|
|
|
@@ -46,6 +46,8 @@ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
|
46
46
|
import { req } from '../lib/requirement-ids.js';
|
|
47
47
|
import { retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
|
|
48
48
|
|
|
49
|
+
export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
|
|
50
|
+
|
|
49
51
|
const ID = 'openwop.requirement.0215.unregister-stops-delivery';
|
|
50
52
|
const DOC = 'webhooks.md §Durability (RFC 0215 §B)';
|
|
51
53
|
const FIXTURE = 'conformance-noop';
|
|
@@ -113,6 +113,8 @@ import { discoverOwnedTenant } from '../lib/webhook-receiver.js';
|
|
|
113
113
|
import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedReceiver } from '../lib/scoped-receiver.js';
|
|
114
114
|
import { req } from '../lib/requirement-ids.js';
|
|
115
115
|
|
|
116
|
+
export const REQUIRES_HOST_CALLBACK = 'the host POSTs a signed webhook delivery to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
|
|
117
|
+
|
|
116
118
|
interface DeliveredRequest {
|
|
117
119
|
readonly headers: Record<string, string>;
|
|
118
120
|
readonly body: string;
|