@openwop/openwop-conformance 1.6.1 → 1.11.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 +44 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +127 -0
- package/api/openapi.yaml +518 -1
- package/coverage.md +44 -2
- package/fixtures/conformance-run-duration-breach.json +33 -0
- package/fixtures/oauth-providers/synthetic.json +38 -0
- package/fixtures.md +29 -0
- package/package.json +1 -1
- package/schemas/README.md +22 -0
- package/schemas/agent-deployment-transition.schema.json +49 -0
- package/schemas/agent-deployment.schema.json +54 -0
- package/schemas/agent-eval-suite.schema.json +140 -0
- package/schemas/agent-inventory-response.schema.json +115 -0
- package/schemas/agent-manifest.schema.json +5 -0
- package/schemas/agent-org-chart.schema.json +82 -0
- package/schemas/agent-ref.schema.json +12 -2
- package/schemas/agent-roster-entry.schema.json +81 -0
- package/schemas/agent-roster-response.schema.json +21 -0
- package/schemas/ai-envelope.schema.json +28 -0
- package/schemas/artifact-type-pack-manifest.schema.json +160 -0
- package/schemas/budget-policy.schema.json +18 -0
- package/schemas/capabilities.schema.json +448 -4
- package/schemas/chat-card-pack-manifest.schema.json +158 -0
- package/schemas/credential-provenance.schema.json +18 -0
- package/schemas/envelopes/media.audio.schema.json +38 -0
- package/schemas/envelopes/media.file.schema.json +37 -0
- package/schemas/envelopes/media.image.schema.json +33 -0
- package/schemas/eval-summary.schema.json +92 -0
- package/schemas/heartbeat-evaluated.schema.json +14 -0
- package/schemas/heartbeat-state-changed.schema.json +14 -0
- package/schemas/node-pack-manifest.schema.json +33 -1
- package/schemas/org-chart-responsibility-view.schema.json +26 -0
- package/schemas/run-event-payloads.schema.json +380 -6
- package/schemas/run-event.schema.json +23 -0
- package/schemas/tool-descriptor.schema.json +63 -0
- package/schemas/trigger-subscription.schema.json +26 -0
- package/schemas/workflow-definition.schema.json +5 -0
- package/schemas/workspace-file-create.schema.json +20 -0
- package/schemas/workspace-file.schema.json +39 -0
- package/src/lib/agentLoop.ts +44 -0
- package/src/lib/agentRoster.ts +76 -0
- package/src/lib/agentRuntime.ts +45 -0
- package/src/lib/artifactTypes.ts +96 -0
- package/src/lib/cardPacks.ts +52 -0
- package/src/lib/discovery-capabilities.ts +50 -0
- package/src/lib/distillation.ts +38 -0
- package/src/lib/feedback.ts +3 -3
- package/src/lib/heartbeat.ts +31 -0
- package/src/lib/liveRuntime.ts +59 -0
- package/src/lib/memoryAttribution.ts +48 -0
- package/src/lib/profiles.ts +157 -0
- package/src/lib/runtimeRequires.ts +38 -0
- package/src/lib/safeFetch.ts +87 -0
- package/src/lib/subRunAttestation.ts +35 -0
- package/src/lib/toolHooks.ts +33 -0
- package/src/scenarios/agent-deployment-shape.test.ts +139 -0
- package/src/scenarios/agent-eval-suite-shape.test.ts +167 -0
- package/src/scenarios/agent-live-allowlist-enforced.test.ts +53 -0
- package/src/scenarios/agent-live-invocation-bracket.test.ts +98 -0
- package/src/scenarios/agent-live-runtime-shape.test.ts +98 -0
- package/src/scenarios/agent-live-structured-output.test.ts +58 -0
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
- package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
- package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
- package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
- package/src/scenarios/agent-org-chart-shape.test.ts +127 -0
- package/src/scenarios/agent-platform-profile.test.ts +158 -0
- package/src/scenarios/agent-roster-attribution.test.ts +179 -0
- package/src/scenarios/agent-roster-shape.test.ts +146 -0
- package/src/scenarios/ai-envelope-shape.test.ts +14 -18
- package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
- package/src/scenarios/approval-gate-flow.test.ts +4 -6
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
- package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
- package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
- package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
- package/src/scenarios/audit-log-integrity.test.ts +3 -2
- package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
- package/src/scenarios/auth-mtls.test.ts +2 -1
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
- package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
- package/src/scenarios/auth-saml-profile.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +2 -1
- package/src/scenarios/authorization-fail-closed.test.ts +2 -1
- package/src/scenarios/authorization-roles-shape.test.ts +2 -1
- package/src/scenarios/budget-policy-shape.test.ts +136 -0
- package/src/scenarios/byok-auth-modes.test.ts +141 -0
- package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
- package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
- package/src/scenarios/commitment-fired.test.ts +83 -0
- package/src/scenarios/credential-payload-redaction.test.ts +2 -1
- package/src/scenarios/credentials-capability-shape.test.ts +2 -1
- package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
- package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
- package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
- package/src/scenarios/distillation-shape.test.ts +41 -0
- package/src/scenarios/distillation-stable-archive.test.ts +37 -0
- package/src/scenarios/distillation-token-budget.test.ts +45 -0
- package/src/scenarios/egress-provenance-shape.test.ts +137 -0
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
- package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
- package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
- package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
- package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
- package/src/scenarios/experimental-tier-shape.test.ts +5 -4
- package/src/scenarios/fs-path-traversal.test.ts +2 -1
- package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
- package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
- package/src/scenarios/http-client-ssrf.test.ts +10 -13
- package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
- package/src/scenarios/media-url-inline-cap.test.ts +167 -0
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
- package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
- package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
- package/src/scenarios/memory-attribution-shape.test.ts +28 -0
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
- package/src/scenarios/memory-capability-model-shape.test.ts +186 -0
- package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
- package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
- package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
- package/src/scenarios/model-capability-substituted.test.ts +2 -1
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
- package/src/scenarios/multi-region-idempotency.test.ts +10 -10
- package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +145 -0
- package/src/scenarios/oauth-capability-shape.test.ts +2 -1
- package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
- package/src/scenarios/pause-resume.test.ts +3 -3
- package/src/scenarios/production-backpressure.test.ts +2 -2
- package/src/scenarios/production-retention-expiry.test.ts +2 -2
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
- package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
- package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
- package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
- package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-pack-install.test.ts +2 -1
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
- package/src/scenarios/prompt-template-shape.test.ts +2 -1
- package/src/scenarios/provider-usage.test.ts +2 -1
- package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
- package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
- package/src/scenarios/replayDeterminism.test.ts +3 -1
- package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
- package/src/scenarios/runtime-requires-install-gate.test.ts +92 -0
- package/src/scenarios/runtime-requires-shape.test.ts +134 -0
- package/src/scenarios/safefetch-behavior.test.ts +99 -0
- package/src/scenarios/safefetch-live-audit.test.ts +175 -0
- package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
- package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
- package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
- package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
- package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
- package/src/scenarios/spec-corpus-validity.test.ts +20 -4
- package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
- package/src/scenarios/subrun-approval-gate.test.ts +35 -0
- package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
- package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
- package/src/scenarios/tool-descriptor-shape.test.ts +133 -0
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
- package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
- package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
- package/src/scenarios/tool-hooks-shape.test.ts +34 -0
- package/src/scenarios/trigger-bridge-shape.test.ts +135 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
- package/src/scenarios/wasm-pack-load.test.ts +2 -2
- package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
- package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
- package/src/scenarios/workspace-behavior.test.ts +134 -0
- package/src/scenarios/workspace-capability-shape.test.ts +73 -0
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
- package/src/scenarios/x-openwop-form-pack-manifest.test.ts +155 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory capability model — reconciled dimensions + degraded-projection shapes (RFC 0080).
|
|
3
|
+
*
|
|
4
|
+
* Always-on, server-free schema-shape probe. Verifies that:
|
|
5
|
+
* - `capabilities.memory` declares the additive `writable` / `search` / `retention`
|
|
6
|
+
* dimensions (RFC 0080 §A), without disturbing the existing
|
|
7
|
+
* `supported` / `compaction` / `distillation` / `attribution` fields.
|
|
8
|
+
* - the `memory.search` / `memory.retention` sub-blocks validate conforming
|
|
9
|
+
* instances and reject malformed ones (`retention.ttl` non-boolean; an
|
|
10
|
+
* unknown `search.modes` enum value; an unknown property under
|
|
11
|
+
* `additionalProperties:false`).
|
|
12
|
+
* - `agent-inventory-response` declares the `memoryDegraded` (bool) +
|
|
13
|
+
* `degradedMemoryDimensions` (closed enum of the eight §A dimension names)
|
|
14
|
+
* inventory fields (RFC 0080 §C), and rejects an out-of-enum dimension.
|
|
15
|
+
* - the eight §A dimension names are stable (the `degradedMemoryDimensions` enum).
|
|
16
|
+
* - `deriveProfiles` surfaces `openwop-memory` for a read/write + long-term
|
|
17
|
+
* payload and withholds it for a `writable:false` payload (the §D predicate).
|
|
18
|
+
*
|
|
19
|
+
* Behavioral assertions (a live `GET /v1/agents` stamping `memoryDegraded` when an
|
|
20
|
+
* agent's `memoryShape` exceeds the host's reconciled model) are gated on
|
|
21
|
+
* `capabilities.agents.manifestRuntime` + `memory` and land in
|
|
22
|
+
* `memory-degraded-projection.test.ts` (deferred per RFC 0080 §Conformance — the
|
|
23
|
+
* degraded projection soft-skips until a reference host computes it). This scenario
|
|
24
|
+
* asserts the wire contract, not host behavior.
|
|
25
|
+
*
|
|
26
|
+
* Spec references:
|
|
27
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md (§"Memory capability model")
|
|
28
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/profiles.md (§`openwop-memory`)
|
|
29
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0080-agent-memory-capability-reconciliation.md
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { describe, it, expect } from 'vitest';
|
|
33
|
+
import { readFileSync } from 'node:fs';
|
|
34
|
+
import { join } from 'node:path';
|
|
35
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
36
|
+
import addFormats from 'ajv-formats';
|
|
37
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
38
|
+
import { deriveProfiles } from '../lib/profiles.js';
|
|
39
|
+
|
|
40
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
41
|
+
|
|
42
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
43
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The canonical eight RFC 0080 §A dimension names, in table order. */
|
|
47
|
+
const DIMENSIONS = [
|
|
48
|
+
'read',
|
|
49
|
+
'write',
|
|
50
|
+
'search',
|
|
51
|
+
'long-term-durability',
|
|
52
|
+
'compaction',
|
|
53
|
+
'attribution',
|
|
54
|
+
'replay-snapshot',
|
|
55
|
+
'retention',
|
|
56
|
+
] as const;
|
|
57
|
+
|
|
58
|
+
describe('memory-capability-model-shape: reconciled dimensions (RFC 0080 §A, server-free)', () => {
|
|
59
|
+
const caps = loadSchema('capabilities.schema.json');
|
|
60
|
+
const memory = (caps.properties as Record<string, { properties?: Record<string, unknown> }>).memory;
|
|
61
|
+
|
|
62
|
+
it('capabilities.memory declares the additive writable / search / retention dimensions', () => {
|
|
63
|
+
for (const dim of ['writable', 'search', 'retention']) {
|
|
64
|
+
expect(
|
|
65
|
+
memory?.properties?.[dim],
|
|
66
|
+
why('agent-memory.md §"Memory capability model"', `capabilities.memory.${dim} MUST be declared (RFC 0080 §A)`),
|
|
67
|
+
).toBeDefined();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('the pre-existing memory fields are untouched (additive, no relocation)', () => {
|
|
72
|
+
for (const dim of ['supported', 'compaction', 'distillation', 'attribution']) {
|
|
73
|
+
expect(
|
|
74
|
+
memory?.properties?.[dim],
|
|
75
|
+
why('COMPATIBILITY.md §2.1', `capabilities.memory.${dim} MUST remain (RFC 0080 is additive)`),
|
|
76
|
+
).toBeDefined();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('memory.search / memory.retention validate conforming instances and reject malformed ones', () => {
|
|
81
|
+
const ajv = addFormats(new Ajv2020({ strict: false }));
|
|
82
|
+
// Wrap the extracted sub-block in a standalone schema (no external $refs in the block).
|
|
83
|
+
const validate = ajv.compile({
|
|
84
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
85
|
+
type: 'object',
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
properties: { memory },
|
|
88
|
+
} as Record<string, unknown>);
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
validate({ memory: { supported: true, writable: false, search: { supported: true, modes: ['semantic', 'filter'] }, retention: { ttl: true, forget: true } } }),
|
|
92
|
+
why('capabilities.md §memory', 'a full reconciled-memory advertisement MUST validate'),
|
|
93
|
+
).toBe(true);
|
|
94
|
+
|
|
95
|
+
expect(
|
|
96
|
+
validate({ memory: { retention: { ttl: 'yes' } } }),
|
|
97
|
+
why('RFC 0080 §A', 'retention.ttl MUST be boolean'),
|
|
98
|
+
).toBe(false);
|
|
99
|
+
|
|
100
|
+
expect(
|
|
101
|
+
validate({ memory: { search: { supported: true, modes: ['fuzzy'] } } }),
|
|
102
|
+
why('RFC 0080 §A', 'search.modes MUST be the closed enum [semantic, filter]'),
|
|
103
|
+
).toBe(false);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
validate({ memory: { search: { supported: true, unknownField: 1 } } }),
|
|
107
|
+
why('RFC 0080 §A', 'memory.search MUST be additionalProperties:false'),
|
|
108
|
+
).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('memory-capability-model-shape: degraded projection (RFC 0080 §C, server-free)', () => {
|
|
113
|
+
const inventory = loadSchema('agent-inventory-response.schema.json');
|
|
114
|
+
|
|
115
|
+
it('agent-inventory-response declares memoryDegraded + degradedMemoryDimensions', () => {
|
|
116
|
+
const entry = ((inventory.$defs as Record<string, { properties?: Record<string, unknown> }>)
|
|
117
|
+
.AgentInventoryEntry).properties;
|
|
118
|
+
expect(
|
|
119
|
+
entry?.memoryDegraded,
|
|
120
|
+
why('agent-memory.md §C-1', 'memoryDegraded MUST be declared on the inventory entry'),
|
|
121
|
+
).toBeDefined();
|
|
122
|
+
expect(
|
|
123
|
+
entry?.degradedMemoryDimensions,
|
|
124
|
+
why('agent-memory.md §C-1', 'degradedMemoryDimensions MUST be declared on the inventory entry'),
|
|
125
|
+
).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('degradedMemoryDimensions enumerates exactly the eight §A dimension names', () => {
|
|
129
|
+
const entry = ((inventory.$defs as Record<string, { properties?: Record<string, { items?: { enum?: string[] } }> }>)
|
|
130
|
+
.AgentInventoryEntry).properties;
|
|
131
|
+
const enumVals = entry?.degradedMemoryDimensions?.items?.enum ?? [];
|
|
132
|
+
expect(
|
|
133
|
+
[...enumVals].sort(),
|
|
134
|
+
why('agent-memory.md §A', 'the degraded-dimension enum MUST be the eight reconciled dimensions'),
|
|
135
|
+
).toEqual([...DIMENSIONS].sort());
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('the inventory schema round-trips a degraded entry and rejects an out-of-enum dimension', () => {
|
|
139
|
+
const ajv = addFormats(new Ajv2020({ strict: false }));
|
|
140
|
+
const validate = ajv.compile(inventory);
|
|
141
|
+
const base = {
|
|
142
|
+
agentId: 'a', persona: 'A', label: 'A', modelClass: 'standard',
|
|
143
|
+
packName: 'p', packVersion: '1.0.0', toolAllowlist: [], hasHandoffSchemas: false,
|
|
144
|
+
};
|
|
145
|
+
expect(
|
|
146
|
+
validate({ total: 1, agents: [{ ...base, memoryDegraded: true, degradedMemoryDimensions: ['write', 'long-term-durability'] }] }),
|
|
147
|
+
why('agent-memory.md §C-1', 'a degraded inventory entry MUST validate'),
|
|
148
|
+
).toBe(true);
|
|
149
|
+
expect(
|
|
150
|
+
validate({ total: 1, agents: [{ ...base, memoryDegraded: true, degradedMemoryDimensions: ['telepathy'] }] }),
|
|
151
|
+
why('agent-memory.md §C-1', 'an out-of-enum degraded dimension MUST be rejected'),
|
|
152
|
+
).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('memory-capability-model-shape: openwop-memory derivation (RFC 0080 §D, server-free)', () => {
|
|
157
|
+
it('deriveProfiles surfaces openwop-memory for a read/write + long-term host', () => {
|
|
158
|
+
const c = {
|
|
159
|
+
protocolVersion: '1.0',
|
|
160
|
+
supportedEnvelopes: ['clarification.request'],
|
|
161
|
+
schemaVersions: {},
|
|
162
|
+
limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 },
|
|
163
|
+
memory: { supported: true },
|
|
164
|
+
agents: { memoryBackends: ['long-term'] },
|
|
165
|
+
} as Record<string, unknown>;
|
|
166
|
+
expect(
|
|
167
|
+
deriveProfiles(c).includes('openwop-memory'),
|
|
168
|
+
why('profiles.md §openwop-memory', 'a read/write + long-term host MUST derive openwop-memory'),
|
|
169
|
+
).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('deriveProfiles withholds openwop-memory from a read-only (writable:false) host', () => {
|
|
173
|
+
const c = {
|
|
174
|
+
protocolVersion: '1.0',
|
|
175
|
+
supportedEnvelopes: ['clarification.request'],
|
|
176
|
+
schemaVersions: {},
|
|
177
|
+
limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 },
|
|
178
|
+
memory: { supported: true, writable: false },
|
|
179
|
+
agents: { memoryBackends: ['long-term'] },
|
|
180
|
+
} as Record<string, unknown>;
|
|
181
|
+
expect(
|
|
182
|
+
deriveProfiles(c).includes('openwop-memory'),
|
|
183
|
+
why('profiles.md §openwop-memory', 'a read-only host MUST NOT derive openwop-memory'),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
29
30
|
|
|
30
31
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-event_longTerm';
|
|
31
32
|
|
|
@@ -35,7 +36,7 @@ interface MemoryCaps {
|
|
|
35
36
|
|
|
36
37
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
37
38
|
const disco = await driver.get('/.well-known/openwop');
|
|
38
|
-
const memory = (disco.json
|
|
39
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
39
40
|
return memory?.compaction?.supported === true;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-tag_longTerm';
|
|
25
26
|
const COMPACTED_FROM_RE = /^compacted-from:[^\s:][^\s]*$/;
|
|
@@ -34,7 +35,7 @@ interface MemoryListResponse {
|
|
|
34
35
|
|
|
35
36
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
36
37
|
const disco = await driver.get('/.well-known/openwop');
|
|
37
|
-
const memory = (disco.json
|
|
38
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
38
39
|
return memory?.compaction?.supported === true;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import { describe, it, expect } from 'vitest';
|
|
30
30
|
import { driver } from '../lib/driver.js';
|
|
31
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
31
32
|
|
|
32
33
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-sr1_longTerm';
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ interface MemoryCaps {
|
|
|
37
38
|
|
|
38
39
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
39
40
|
const disco = await driver.get('/.well-known/openwop');
|
|
40
|
-
const memory = (disco.json
|
|
41
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
41
42
|
return memory?.compaction?.supported === true;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background memory consolidation — idempotence + SR-1 carry-forward
|
|
3
|
+
* (RFC 0068, `Draft`).
|
|
4
|
+
*
|
|
5
|
+
* Gated on `capabilities.agents.memoryConsolidation.supported`. Drives the
|
|
6
|
+
* documented host seam `POST /v1/host/sample/memory/consolidate` (staged
|
|
7
|
+
* per the RFC 0027 §G precedent — soft-skips on 404/501 until a reference
|
|
8
|
+
* host wires it). Asserts:
|
|
9
|
+
* - a consolidation pass emits `agent.memory.consolidated` with
|
|
10
|
+
* `outputCount <= inputCount` (RFC 0068 §D);
|
|
11
|
+
* - a second pass over the unchanged corpus is a no-op
|
|
12
|
+
* (`inputCount == outputCount`) — the idempotence MUST that bounds
|
|
13
|
+
* runaway consolidation;
|
|
14
|
+
* - SR-1 carry-forward — a redacted secret in a source entry stays
|
|
15
|
+
* redacted in a consolidated entry.
|
|
16
|
+
*
|
|
17
|
+
* Hosts that omit the capability skip cleanly.
|
|
18
|
+
*
|
|
19
|
+
* Spec references:
|
|
20
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
|
|
21
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { driver } from '../lib/driver.js';
|
|
26
|
+
|
|
27
|
+
interface ConsolidationCaps {
|
|
28
|
+
agents?: { memoryConsolidation?: { supported?: boolean } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ConsolidateResult {
|
|
32
|
+
event?: { inputCount?: number; outputCount?: number };
|
|
33
|
+
secretLeaked?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function consolidationSupported(): Promise<boolean> {
|
|
37
|
+
const res = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
38
|
+
if (res.status !== 200) return false;
|
|
39
|
+
return Boolean((res.json as ConsolidationCaps).agents?.memoryConsolidation?.supported);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('memory-consolidation-idempotent: pass contract (RFC 0068 §D, capability-gated)', () => {
|
|
43
|
+
it('a consolidation pass reduces or holds entry count and is idempotent on a stable corpus', async () => {
|
|
44
|
+
if (!(await consolidationSupported())) return; // capability absent — gated skip
|
|
45
|
+
|
|
46
|
+
const first = await driver.post('/v1/host/sample/memory/consolidate', {
|
|
47
|
+
memoryRef: 'mem://conformance/consolidation',
|
|
48
|
+
includeSecretCanary: true,
|
|
49
|
+
});
|
|
50
|
+
if (first.status === 404 || first.status === 501) return; // seam not wired — soft-skip
|
|
51
|
+
|
|
52
|
+
expect(first.status, driver.describe('RFC 0068 §D', 'an advertised consolidation seam MUST succeed')).toBe(200);
|
|
53
|
+
const r1 = first.json as ConsolidateResult;
|
|
54
|
+
const in1 = r1.event?.inputCount ?? 0;
|
|
55
|
+
const out1 = r1.event?.outputCount ?? 0;
|
|
56
|
+
expect(out1, driver.describe('RFC 0068 §D.1', 'outputCount MUST be <= inputCount for a merge/dedup pass')).toBeLessThanOrEqual(in1);
|
|
57
|
+
|
|
58
|
+
// §D.2 — a second pass over the unchanged corpus is a no-op.
|
|
59
|
+
const second = await driver.post('/v1/host/sample/memory/consolidate', {
|
|
60
|
+
memoryRef: 'mem://conformance/consolidation',
|
|
61
|
+
});
|
|
62
|
+
if (second.status === 404 || second.status === 501) return;
|
|
63
|
+
const r2 = second.json as ConsolidateResult;
|
|
64
|
+
expect(
|
|
65
|
+
r2.event?.inputCount,
|
|
66
|
+
driver.describe('RFC 0068 §D.2', 'a second pass over an unchanged corpus MUST be a no-op (inputCount == outputCount)'),
|
|
67
|
+
).toBe(r2.event?.outputCount);
|
|
68
|
+
|
|
69
|
+
// §D.3 — SR-1 carry-forward: a redacted secret stays redacted in the consolidated entry.
|
|
70
|
+
if (typeof r1.secretLeaked === 'boolean') {
|
|
71
|
+
expect(
|
|
72
|
+
r1.secretLeaked,
|
|
73
|
+
driver.describe('RFC 0068 §D.3 / agent-memory.md §SR-1', 'a redacted secret MUST NOT re-appear in a consolidated entry'),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-consolidation + commitment event shapes (RFC 0068, `Draft`).
|
|
3
|
+
*
|
|
4
|
+
* Always-on, server-free schema-shape probe. Verifies that:
|
|
5
|
+
* - `capabilities.agents.memoryConsolidation` + `agents.commitments`
|
|
6
|
+
* sub-blocks are declared on the capabilities schema.
|
|
7
|
+
* - the `agent.memory.consolidated` + `commitment.fired` payload $defs
|
|
8
|
+
* validate conforming payloads and reject malformed ones (a
|
|
9
|
+
* `commitment.fired` missing `memoryRef` is rejected — a commitment
|
|
10
|
+
* with no memory provenance is not an *inferred* commitment).
|
|
11
|
+
* - both event names appear in the RunEventType enum.
|
|
12
|
+
*
|
|
13
|
+
* Distinct from RFC 0062 distillation (`memory.compacted`): consolidation
|
|
14
|
+
* reconciles long-term memory; this scenario asserts the new event
|
|
15
|
+
* contract, not host behavior.
|
|
16
|
+
*
|
|
17
|
+
* Spec references:
|
|
18
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
|
|
19
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Inferred commitments"
|
|
20
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
27
|
+
import addFormats from 'ajv-formats';
|
|
28
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
29
|
+
|
|
30
|
+
/** Server-free assertion-message helper (mirrors driver.describe's "spec — requirement" shape without requiring OPENWOP_BASE_URL). */
|
|
31
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
32
|
+
|
|
33
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
34
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('memory-consolidation-shape: capability advertisement (RFC 0068, server-free)', () => {
|
|
38
|
+
it('the capabilities schema declares agents.memoryConsolidation + agents.commitments', () => {
|
|
39
|
+
const caps = loadSchema('capabilities.schema.json');
|
|
40
|
+
const agents = (caps.properties as Record<string, { properties?: Record<string, unknown> }>).agents;
|
|
41
|
+
expect(
|
|
42
|
+
agents?.properties?.memoryConsolidation,
|
|
43
|
+
why('capabilities.md §agents', 'agents.memoryConsolidation MUST be declared'),
|
|
44
|
+
).toBeDefined();
|
|
45
|
+
expect(
|
|
46
|
+
agents?.properties?.commitments,
|
|
47
|
+
why('capabilities.md §agents', 'agents.commitments MUST be declared'),
|
|
48
|
+
).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('memory-consolidation-shape: event payloads (RFC 0068, server-free)', () => {
|
|
53
|
+
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
54
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
55
|
+
addFormats(ajv);
|
|
56
|
+
ajv.addSchema(payloads, 'payloads');
|
|
57
|
+
|
|
58
|
+
const consolidated = ajv.getSchema('payloads#/$defs/agentMemoryConsolidated');
|
|
59
|
+
const fired = ajv.getSchema('payloads#/$defs/commitmentFired');
|
|
60
|
+
|
|
61
|
+
it('agent.memory.consolidated validates a content-free pass summary', () => {
|
|
62
|
+
expect(consolidated, 'the agentMemoryConsolidated $def MUST exist').toBeTruthy();
|
|
63
|
+
expect(
|
|
64
|
+
consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 240, outputCount: 201, trigger: 'host-managed' }),
|
|
65
|
+
why('RFC 0068 §B', 'a conforming agent.memory.consolidated payload MUST validate'),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
// Negative: outputCount as string fails the integer type.
|
|
68
|
+
expect(consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 1, outputCount: 'x' })).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('commitment.fired validates a content-free fire record and requires memoryRef', () => {
|
|
72
|
+
expect(fired, 'the commitmentFired $def MUST exist').toBeTruthy();
|
|
73
|
+
expect(
|
|
74
|
+
fired!({ commitmentId: 'cmt-1', memoryRef: 'mem://a/agent-1', condition: 'predicate', enqueuedRunId: 'run-1' }),
|
|
75
|
+
why('RFC 0068 §C', 'a conforming commitment.fired payload MUST validate'),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
// Negative: missing memoryRef — a commitment with no provenance breaks CTI-1 binding.
|
|
78
|
+
expect(
|
|
79
|
+
fired!({ commitmentId: 'cmt-1', condition: 'time' }),
|
|
80
|
+
why('RFC 0068 §C', 'commitment.fired without memoryRef MUST be rejected'),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('both event names appear in the RunEventType enum', () => {
|
|
85
|
+
const runEvent = loadSchema('run-event.schema.json');
|
|
86
|
+
const enumVals = (runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum ?? [];
|
|
87
|
+
expect(enumVals).toContain('agent.memory.consolidated');
|
|
88
|
+
expect(enumVals).toContain('commitment.fired');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from 'vitest';
|
|
19
19
|
import { driver } from '../lib/driver.js';
|
|
20
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
20
21
|
|
|
21
22
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
22
23
|
|
|
@@ -64,7 +65,7 @@ describe.skipIf(HTTP_SKIP)('model-capability-substituted: advertisement shape (R
|
|
|
64
65
|
it('capabilities.modelCapabilities (when present) conforms to RFC 0031 §E', async () => {
|
|
65
66
|
const d = await readDiscovery();
|
|
66
67
|
if (d === null) return;
|
|
67
|
-
const mc = d
|
|
68
|
+
const mc = capabilityFamily(d, 'modelCapabilities');
|
|
68
69
|
if (mc === undefined) return;
|
|
69
70
|
expect(
|
|
70
71
|
typeof mc.supported,
|
|
@@ -50,6 +50,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
50
50
|
import { driver } from '../lib/driver.js';
|
|
51
51
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
52
52
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
53
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
53
54
|
|
|
54
55
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
55
56
|
const FIXTURE = 'conformance-multi-agent-confidence-escalation';
|
|
@@ -84,7 +85,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-confidence-escalation: capability shape
|
|
|
84
85
|
it('confidenceEscalationFloor (when advertised) MUST be in [0.5, 1.0]', async () => {
|
|
85
86
|
const d = await readDiscovery();
|
|
86
87
|
if (d === null) return;
|
|
87
|
-
const em = d
|
|
88
|
+
const em = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
|
|
88
89
|
if (em === undefined) return;
|
|
89
90
|
const floor = em.confidenceEscalationFloor;
|
|
90
91
|
if (floor === undefined) return;
|
|
@@ -101,8 +102,8 @@ describe.skipIf(HTTP_SKIP)('multi-agent-confidence-escalation: capability shape
|
|
|
101
102
|
describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-confidence-escalation: behavioral (RFC 0039 §A)', () => {
|
|
102
103
|
it('happy-path: low-confidence decision → confidence-escalated event + clarification interrupt + zero dispatch events', async () => {
|
|
103
104
|
const d = await readDiscovery();
|
|
104
|
-
const supported = d
|
|
105
|
-
const versionRaw = d
|
|
105
|
+
const supported = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.supported === true;
|
|
106
|
+
const versionRaw = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version;
|
|
106
107
|
const version = typeof versionRaw === 'number' ? versionRaw : 0;
|
|
107
108
|
if (!supported || version < 2) return; // soft-skip — `version: 1` hosts pass via this absence
|
|
108
109
|
|
|
@@ -125,7 +126,7 @@ describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-confidence-escalation: behavioral
|
|
|
125
126
|
// status — the host's own interrupt.md mapping determines the suffix).
|
|
126
127
|
// When the host does NOT advertise the field, fall back to the canonical
|
|
127
128
|
// either-status check.
|
|
128
|
-
const advertisedKind = d
|
|
129
|
+
const advertisedKind = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.confidenceEscalationInterruptKind;
|
|
129
130
|
const isVendorKind = typeof advertisedKind === 'string' && /^x-host-[a-z][a-z0-9-]*-[a-z][a-z0-9-]*$/.test(advertisedKind);
|
|
130
131
|
const isCanonicalKind = advertisedKind === 'clarification' || advertisedKind === 'approval';
|
|
131
132
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Asserts (Phase 1 — execution-loop + handoff state machine per spec/v1/multi-agent-execution.md):
|
|
11
11
|
*
|
|
12
12
|
* 1. Advertisement shape: when capabilities.multiAgent.executionModel.supported
|
|
13
|
-
* is present, version MUST be integer in [1,
|
|
13
|
+
* is present, version MUST be integer in [1, 5]; supported MUST be boolean.
|
|
14
14
|
*
|
|
15
15
|
* 2. Behavioral (gated on supported: true + fixture availability): a
|
|
16
16
|
* supervisor → next-worker → child-completed run emits the 4 expected
|
|
@@ -34,6 +34,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
34
34
|
import { driver } from '../lib/driver.js';
|
|
35
35
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
36
36
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
37
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
37
38
|
|
|
38
39
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
39
40
|
|
|
@@ -62,7 +63,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-handoff-state-machine: advertisement sha
|
|
|
62
63
|
it('capabilities.multiAgent.executionModel (when present) conforms to RFC 0037 §C', async () => {
|
|
63
64
|
const d = await readDiscovery();
|
|
64
65
|
if (d === null) return; // discovery unavailable — skip
|
|
65
|
-
const executionModel = d
|
|
66
|
+
const executionModel = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
|
|
66
67
|
if (executionModel === undefined) return; // host doesn't advertise — soft-skip
|
|
67
68
|
expect(
|
|
68
69
|
typeof executionModel.supported,
|
|
@@ -80,10 +81,10 @@ describe.skipIf(HTTP_SKIP)('multi-agent-handoff-state-machine: advertisement sha
|
|
|
80
81
|
).toBe('number');
|
|
81
82
|
const v = executionModel.version as number;
|
|
82
83
|
expect(
|
|
83
|
-
Number.isInteger(v) && v >= 1 && v <=
|
|
84
|
+
Number.isInteger(v) && v >= 1 && v <= 5,
|
|
84
85
|
driver.describe(
|
|
85
86
|
'RFCS/0037-multi-agent-execution-model.md §C',
|
|
86
|
-
'version MUST be an integer in [1,
|
|
87
|
+
'version MUST be an integer in [1, 5] (1 = Phase 1 only; Phases 2-5 lift the ceiling additively — Phase 5 = RFC 0061 stateful agent-loop lifecycle, matching `capabilities.schema.json` §multiAgent.executionModel.version maximum)',
|
|
87
88
|
),
|
|
88
89
|
).toBe(true);
|
|
89
90
|
});
|
|
@@ -104,7 +105,7 @@ const BEHAVIORAL_SKIP = HTTP_SKIP || !isFixtureAdvertised(PARENT_FIXTURE) || !is
|
|
|
104
105
|
describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-handoff-state-machine: behavioral 4-event causation chain (RFC 0037 §"Handoff state machine")', () => {
|
|
105
106
|
it('happy-path: dispatch.began → dispatch.succeeded → child.completed → output.harvested fire in causation order', async () => {
|
|
106
107
|
const d = await readDiscovery();
|
|
107
|
-
const advertised = d
|
|
108
|
+
const advertised = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.supported === true;
|
|
108
109
|
if (!advertised) return; // soft-skip — host honest about not implementing
|
|
109
110
|
|
|
110
111
|
const create = await driver.post('/v1/runs', { workflowId: PARENT_FIXTURE });
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
import { describe, it, expect } from 'vitest';
|
|
50
50
|
import { driver } from '../lib/driver.js';
|
|
51
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
51
52
|
|
|
52
53
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
53
54
|
|
|
@@ -81,7 +82,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-memory-lifecycle: advertisement shape (R
|
|
|
81
82
|
ctx.skip();
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
|
-
const ccmc = d
|
|
85
|
+
const ccmc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossChildMemoryConcurrency;
|
|
85
86
|
if (ccmc === undefined) {
|
|
86
87
|
ctx.skip(); // optional advertisement — host hasn't opted in
|
|
87
88
|
return;
|
|
@@ -135,8 +136,8 @@ describe.skipIf(HTTP_SKIP)('multi-agent-memory-lifecycle: behavioral (RFC 0039
|
|
|
135
136
|
ctx.skip();
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
138
|
-
const v = d
|
|
139
|
-
const memorySupported = d
|
|
139
|
+
const v = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version;
|
|
140
|
+
const memorySupported = capabilityFamily<{ supported?: unknown }>(d, 'memory')?.supported;
|
|
140
141
|
const phase2OrLater = typeof v === 'number' && v >= 2;
|
|
141
142
|
const expiredRunId = process.env.OPENWOP_TEST_EXPIRED_REPLAY_RUN_ID;
|
|
142
143
|
if (!phase2OrLater || memorySupported !== true || !expiredRunId) {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
const ALLOWED = new Set(['single-region', 'best-effort', 'strict']);
|
|
25
26
|
const REQUIRED_METRICS_WHEN_MULTI_REGION = [
|
|
@@ -40,9 +41,7 @@ interface ObservabilityCaps {
|
|
|
40
41
|
describe('multi-region-idempotency: capability shape', () => {
|
|
41
42
|
it('idempotency.crossRegion (when advertised) MUST be one of the closed enum', async () => {
|
|
42
43
|
const disco = await driver.get('/.well-known/openwop');
|
|
43
|
-
const idem =
|
|
44
|
-
(disco.json as { capabilities?: { idempotency?: IdempotencyCaps } }).capabilities
|
|
45
|
-
?.idempotency;
|
|
44
|
+
const idem = capabilityFamily<IdempotencyCaps>(disco.json, 'idempotency');
|
|
46
45
|
|
|
47
46
|
if (!idem || idem.crossRegion === undefined) {
|
|
48
47
|
// eslint-disable-next-line no-console
|
|
@@ -67,16 +66,16 @@ describe('multi-region-idempotency: capability shape', () => {
|
|
|
67
66
|
|
|
68
67
|
it('multi-region hosts SHOULD expose the cross-region conflict counter per §"Operator surface"', async () => {
|
|
69
68
|
const disco = await driver.get('/.well-known/openwop');
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const crossRegion =
|
|
69
|
+
const idem = capabilityFamily<IdempotencyCaps>(disco.json, 'idempotency');
|
|
70
|
+
const observability = capabilityFamily<ObservabilityCaps>(disco.json, 'observability');
|
|
71
|
+
const crossRegion = idem?.crossRegion;
|
|
73
72
|
|
|
74
73
|
if (crossRegion !== 'best-effort' && crossRegion !== 'strict') {
|
|
75
74
|
// Single-region hosts have no conflicts to count — skip.
|
|
76
75
|
return;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
const advertised = new Set(
|
|
78
|
+
const advertised = new Set(observability?.metrics?.names ?? []);
|
|
80
79
|
for (const name of REQUIRED_METRICS_WHEN_MULTI_REGION) {
|
|
81
80
|
expect(advertised.has(name), driver.describe(
|
|
82
81
|
'idempotency.md §"Operator surface"',
|
|
@@ -103,9 +102,10 @@ interface MultiRegionCaps {
|
|
|
103
102
|
describe('multi-region-idempotency: granular multiRegion advertisement shape (RFC 0036 §A)', () => {
|
|
104
103
|
it('capabilities.idempotency.multiRegion (when present) conforms to RFC 0036 §A', async () => {
|
|
105
104
|
const disco = await driver.get('/.well-known/openwop');
|
|
106
|
-
const idem =
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const idem = capabilityFamily<IdempotencyCaps & { multiRegion?: MultiRegionCaps }>(
|
|
106
|
+
disco.json,
|
|
107
|
+
'idempotency',
|
|
108
|
+
);
|
|
109
109
|
const mr = idem?.multiRegion;
|
|
110
110
|
if (mr === undefined) return; // host doesn't advertise the granular block — soft-skip
|
|
111
111
|
|